// Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { StrictMode, useCallback, useEffect } from 'react'; import { useSelector } from 'react-redux'; import type { AudioDevice } from '@signalapp/ringrtc'; import type { MutableRefObject } from 'react'; import { useItemsActions } from '../ducks/items.preload.js'; import { useConversationsActions } from '../ducks/conversations.preload.js'; import { getConversationsWithCustomColorSelector, getMe, getOtherTabsUnreadStats, } from '../selectors/conversations.dom.js'; import { getCustomColors, getItems, getNavTabsCollapsed, getPreferredLeftPaneWidth, } from '../selectors/items.dom.js'; import { itemStorage, DEFAULT_AUTO_DOWNLOAD_ATTACHMENT, } from '../../textsecure/Storage.preload.js'; import { onHasStoriesDisabledChange, setPhoneNumberDiscoverability, } from '../../textsecure/WebAPI.preload.js'; import { DEFAULT_CONVERSATION_COLOR } from '../../types/Colors.std.js'; import { saveAttachmentToDisk } from '../../util/migrations.preload.js'; import { format } from '../../types/PhoneNumber.std.js'; import { getIntl, getTheme, getUser, getUserDeviceId, getUserNumber, } from '../selectors/user.std.js'; import { EmojiSkinTone } from '../../components/fun/data/emojis.std.js'; import { renderClearingDataView } from '../../shims/renderClearingDataView.preload.js'; import OS from '../../util/os/osPreload.preload.js'; import { themeChanged } from '../../shims/themeChanged.dom.js'; import * as Settings from '../../types/Settings.std.js'; import * as universalExpireTimerUtil from '../../util/universalExpireTimer.preload.js'; import { parseSystemTraySetting, shouldMinimizeToSystemTray, SystemTraySetting, } from '../../types/SystemTraySetting.std.js'; import { calling } from '../../services/calling.preload.js'; import { drop } from '../../util/drop.std.js'; import { assertDev } from '../../util/assert.std.js'; import { backupsService } from '../../services/backups/index.preload.js'; import { DurationInSeconds } from '../../util/durations/duration-in-seconds.std.js'; import { PhoneNumberDiscoverability } from '../../util/phoneNumberDiscoverability.std.js'; import { PhoneNumberSharingMode } from '../../types/PhoneNumberSharingMode.std.js'; import { writeProfile } from '../../services/writeProfile.preload.js'; import { keyTransparency } from '../../services/keyTransparency.preload.js'; import { getConversation } from '../../util/getConversation.preload.js'; import { waitForEvent } from '../../shims/events.dom.js'; import { DAY } from '../../util/durations/index.std.js'; import { sendSyncRequests } from '../../textsecure/syncRequests.preload.js'; import { SmartUpdateDialog } from './UpdateDialog.preload.js'; import { Preferences } from '../../components/Preferences.dom.js'; import { useUpdatesActions } from '../ducks/updates.preload.js'; import { getUpdateDialogType } from '../selectors/updates.std.js'; import { getHasAnyFailedStorySends } from '../selectors/stories.preload.js'; import { getSelectedConversationId, getSelectedLocation, } from '../selectors/nav.std.js'; import { getPreferredBadgeSelector } from '../selectors/badges.preload.js'; import { SmartProfileEditor } from './ProfileEditor.preload.js'; import { useNavActions } from '../ducks/nav.std.js'; import { NavTab } from '../../types/Nav.std.js'; import { renderToastManagerWithoutMegaphone } from './ToastManager.preload.js'; import { useToastActions } from '../ducks/toast.preload.js'; import { DataReader, DataWriter } from '../../sql/Client.preload.js'; import { deleteAllMyStories } from '../../util/deleteAllMyStories.preload.js'; import { SmartPreferencesDonations } from './PreferencesDonations.preload.js'; import { useDonationsActions } from '../ducks/donations.preload.js'; import { generateDonationReceiptBlob } from '../../util/generateDonationReceipt.dom.js'; import { getProfiles } from '../selectors/notificationProfiles.dom.js'; import { backupLevelFromNumber } from '../../services/backups/types.std.js'; import { getMessageQueueTime } from '../../util/getMessageQueueTime.dom.js'; import { useBackupActions } from '../ducks/backups.preload.js'; import { isFeaturedEnabledSelector } from '../../util/isFeatureEnabled.dom.js'; import { SmartPreferencesChatFoldersPage } from './PreferencesChatFoldersPage.preload.js'; import { SmartPreferencesEditChatFolderPage } from './PreferencesEditChatFolderPage.preload.js'; import { AxoProvider } from '../../axo/AxoProvider.dom.js'; import { getCurrentChatFoldersCount, getHasAnyCurrentCustomChatFolders, } from '../selectors/chatFolders.std.js'; import { SmartNotificationProfilesCreateFlow, SmartNotificationProfilesHome, } from './PreferencesNotificationProfiles.preload.js'; import type { SettingsLocation } from '../../types/Nav.std.js'; import type { StorageAccessType } from '../../types/Storage.d.ts'; import type { ThemeType } from '../../util/preload.preload.js'; import type { WidthBreakpoint } from '../../components/_util.std.js'; import { DialogType } from '../../types/Dialogs.std.js'; import { promptOSAuth } from '../../util/promptOSAuth.preload.js'; import type { StateType } from '../reducer.preload.js'; import { pauseBackupMediaDownload, resumeBackupMediaDownload, cancelBackupMediaDownload, } from '../../util/backupMediaDownload.preload.js'; import { DonationsErrorBoundary } from '../../components/DonationsErrorBoundary.dom.js'; import type { SmartPreferencesChatFoldersPageProps } from './PreferencesChatFoldersPage.preload.js'; import type { SmartPreferencesEditChatFolderPageProps } from './PreferencesEditChatFolderPage.preload.js'; import type { ExternalProps as SmartNotificationProfilesProps } from './PreferencesNotificationProfiles.preload.js'; import { useMegaphonesActions } from '../ducks/megaphones.preload.js'; import type { ZoomFactorType } from '../../types/StorageKeys.std.js'; import { isLocalBackupsEnabled } from '../../util/isLocalBackupsEnabled.preload.js'; const DEFAULT_NOTIFICATION_SETTING = 'message'; function renderUpdateDialog( props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }> ): React.JSX.Element { return ; } function renderPreferencesChatFoldersPage( props: SmartPreferencesChatFoldersPageProps ): React.JSX.Element { return ; } function renderPreferencesEditChatFolderPage( props: SmartPreferencesEditChatFolderPageProps ): React.JSX.Element { return ; } function renderNotificationProfilesHome( props: SmartNotificationProfilesProps ): React.JSX.Element { return ; } function renderNotificationProfilesCreateFlow( props: SmartNotificationProfilesProps ): React.JSX.Element { return ; } function renderProfileEditor(options: { contentsRef: MutableRefObject; }): React.JSX.Element { return ; } function renderDonationsPane({ contentsRef, settingsLocation, setSettingsLocation, }: { contentsRef: MutableRefObject; settingsLocation: SettingsLocation; setSettingsLocation: (settingsLocation: SettingsLocation) => void; }): React.JSX.Element { return ( ); } function getSystemTraySettingValues( systemTraySetting: SystemTraySetting | undefined ): { hasMinimizeToAndStartInSystemTray: boolean | undefined; hasMinimizeToSystemTray: boolean | undefined; } { if (systemTraySetting === undefined) { return { hasMinimizeToAndStartInSystemTray: undefined, hasMinimizeToSystemTray: undefined, }; } const parsedSystemTraySetting = parseSystemTraySetting(systemTraySetting); const hasMinimizeToAndStartInSystemTray = parsedSystemTraySetting === SystemTraySetting.MinimizeToAndStartInSystemTray; const hasMinimizeToSystemTray = shouldMinimizeToSystemTray( parsedSystemTraySetting ); return { hasMinimizeToAndStartInSystemTray, hasMinimizeToSystemTray, }; } export function SmartPreferences(): React.JSX.Element | null { const { addCustomColor, editCustomColor, putItem, removeCustomColor, resetDefaultChatColor, savePreferredLeftPaneWidth, setEmojiSkinToneDefault: onEmojiSkinToneDefaultChange, setGlobalDefaultConversationColor, toggleNavTabsCollapse, } = useItemsActions(); const { removeCustomColorOnConversations, resetAllChatColors } = useConversationsActions(); const { startUpdate } = useUpdatesActions(); const { changeLocation } = useNavActions(); const { showToast, openFileInFolder } = useToastActions(); const { internalAddDonationReceipt } = useDonationsActions(); const { startPlaintextExport, startLocalBackupExport } = useBackupActions(); const { addVisibleMegaphone } = useMegaphonesActions(); // Selectors const currentLocation = useSelector(getSelectedLocation); const customColors = useSelector(getCustomColors) ?? {}; const getConversationsWithCustomColor = useSelector( getConversationsWithCustomColorSelector ); const i18n = useSelector(getIntl); const dialogType = useSelector(getUpdateDialogType); const items = useSelector(getItems); const hasFailedStorySends = useSelector(getHasAnyFailedStorySends); const me = useSelector(getMe); const navTabsCollapsed = useSelector(getNavTabsCollapsed); const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats); const preferredWidthFromStorage = useSelector(getPreferredLeftPaneWidth); const getPreferredBadge = useSelector(getPreferredBadgeSelector); const theme = useSelector(getTheme); const donationReceipts = useSelector( (state: StateType) => state.donations.receipts ); const notificationProfileCount = useSelector(getProfiles).length; const shouldShowUpdateDialog = dialogType !== DialogType.None; const badge = getPreferredBadge(me.badges); const currentChatFoldersCount = useSelector(getCurrentChatFoldersCount); const hasAnyCurrentCustomChatFolders = useSelector( getHasAnyCurrentCustomChatFolders ); const { osName } = useSelector(getUser); // The weird ones const makeSyncRequest = async () => { const contactSyncComplete = waitForEvent('contactSync:complete'); return Promise.all([sendSyncRequests(), contactSyncComplete]); }; const universalExpireTimer = universalExpireTimerUtil.getForRedux(items); const onUniversalExpireTimerChange = async (newValue: number) => { await universalExpireTimerUtil.set(DurationInSeconds.fromSeconds(newValue)); // Update account in Storage Service const account = window.ConversationController.getOurConversationOrThrow(); account.captureChange('universalExpireTimer'); // Add a notification to the currently open conversation const selectedId = getSelectedConversationId(window.reduxStore.getState()); if (selectedId) { const conversation = window.ConversationController.get(selectedId); assertDev(conversation, "Conversation wasn't found"); await conversation.updateLastMessage(); } }; const validateBackup = () => backupsService._internalValidate(); const pickLocalBackupFolder = () => backupsService.pickLocalBackupFolder(); const doDeleteAllData = () => renderClearingDataView(); const refreshCloudBackupStatus = backupsService.throttledFetchCloudBackupStatus; const refreshBackupSubscriptionStatus = backupsService.throttledFetchSubscriptionStatus; // Context - these don't change per startup const version = window.SignalContext.getVersion(); const availableLocales = window.SignalContext.getI18nAvailableLocales(); const resolvedLocale = window.SignalContext.getI18nLocale(); const preferredSystemLocales = window.SignalContext.getPreferredSystemLocales(); const initialSpellCheckSetting = window.SignalContext.config.appStartInitialSpellcheckSetting; // Settings - these capabilities are unchanging const isAutoDownloadUpdatesSupported = Settings.isAutoDownloadUpdatesSupported(OS, version); const isAutoLaunchSupported = Settings.isAutoLaunchSupported(OS); const isHideMenuBarSupported = Settings.isHideMenuBarSupported(OS); const isMinimizeToAndStartInSystemTraySupported = Settings.isMinimizeToAndStartInSystemTraySupported(OS); const isNotificationAttentionSupported = Settings.isDrawAttentionSupported(OS); const isSystemTraySupported = Settings.isSystemTraySupported(OS); // Textsecure - user can change number and change this device's name const phoneNumber = format(useSelector(getUserNumber) ?? '', {}); const isPrimary = useSelector(getUserDeviceId) === 1; const isSyncSupported = !isPrimary; const [deviceName, setDeviceName] = React.useState( itemStorage.user.getDeviceName() ); useEffect(() => { let canceled = false; const onDeviceNameChanged = () => { const value = itemStorage.user.getDeviceName(); if (canceled) { return; } setDeviceName(value); }; window.Whisper.events.on('deviceNameChanged', onDeviceNameChanged); return () => { canceled = true; window.Whisper.events.off('deviceNameChanged', onDeviceNameChanged); }; }, []); // RingRTC - the list of devices is unchanging while settings window is open // The select boxes for devices are disabled while these arrays have zero length const [availableCameras, setAvailableCameras] = React.useState< Array >([]); const [availableMicrophones, setAvailableMicrophones] = React.useState< Array >([]); const [availableSpeakers, setAvailableSpeakers] = React.useState< Array >([]); useEffect(() => { let canceled = false; const loadDevices = async () => { const { availableCameras: cameras, availableMicrophones: microphones, availableSpeakers: speakers, } = await calling.getAvailableIODevices(); if (canceled) { return; } setAvailableCameras(cameras); setAvailableMicrophones(microphones); setAvailableSpeakers(speakers); }; drop(loadDevices()); return () => { canceled = true; }; }, []); // Ephemeral settings, via async IPC, all can be modiified const [localeOverride, setLocaleOverride] = React.useState(); const [systemTraySettings, setSystemTraySettings] = React.useState(); const [hasContentProtection, setContentProtection] = React.useState(); const [hasSpellCheck, setSpellCheck] = React.useState(); const [themeSetting, setThemeSetting] = React.useState(); useEffect(() => { let canceled = false; const loadOverride = async () => { const value = await window.Events.getLocaleOverride(); if (canceled) { return; } setLocaleOverride(value); }; drop(loadOverride()); const loadSystemTraySettings = async () => { const value = await window.Events.getSystemTraySetting(); if (canceled) { return; } setSystemTraySettings(value); }; drop(loadSystemTraySettings()); const loadSpellCheck = async () => { const value = await window.Events.getSpellCheck(); if (canceled) { return; } setSpellCheck(value); }; drop(loadSpellCheck()); const loadContentProtection = async () => { const value = await window.Events.getContentProtection(); setContentProtection(value); }; drop(loadContentProtection()); const loadThemeSetting = async () => { const value = await window.Events.getThemeSetting(); if (canceled) { return; } setThemeSetting(value); }; drop(loadThemeSetting()); return () => { canceled = true; }; }, []); const onLocaleChange = async (locale: string | null | undefined) => { setLocaleOverride(locale); await window.Events.setLocaleOverride(locale ?? null); }; const { hasMinimizeToAndStartInSystemTray, hasMinimizeToSystemTray } = getSystemTraySettingValues(systemTraySettings); const onMinimizeToSystemTrayChange = async (value: boolean) => { const newSetting = value ? SystemTraySetting.MinimizeToSystemTray : SystemTraySetting.DoNotUseSystemTray; setSystemTraySettings(newSetting); await window.Events.setSystemTraySetting(newSetting); }; const onMinimizeToAndStartInSystemTrayChange = async (value: boolean) => { const newSetting = value ? SystemTraySetting.MinimizeToAndStartInSystemTray : SystemTraySetting.MinimizeToSystemTray; setSystemTraySettings(newSetting); await window.Events.setSystemTraySetting(newSetting); }; const onSpellCheckChange = async (value: boolean) => { setSpellCheck(value); await window.Events.setSpellCheck(value); }; const onContentProtectionChange = async (value: boolean) => { setContentProtection(value); await window.Events.setContentProtection(value); }; const onThemeChange = (value: ThemeType) => { setThemeSetting(value); drop(window.Events.setThemeSetting(value)); drop(themeChanged()); }; // Async IPC for electron configuration, all can be modified const [hasAutoLaunch, setAutoLaunch] = React.useState(); const [hasMediaCameraPermissions, setMediaCameraPermissions] = React.useState(); const [hasMediaPermissions, setMediaPermissions] = React.useState(); const [zoomFactor, setZoomFactor] = React.useState(); useEffect(() => { let canceled = false; const loadAutoLaunch = async () => { const value = await window.Events.getAutoLaunch(); if (canceled) { return; } setAutoLaunch(value); }; drop(loadAutoLaunch()); const loadMediaCameraPermissions = async () => { const value = await window.Events.getMediaCameraPermissions(); if (canceled) { return; } setMediaCameraPermissions(value); }; drop(loadMediaCameraPermissions()); const loadMediaPermissions = async () => { const value = await window.Events.getMediaPermissions(); if (canceled) { return; } setMediaPermissions(value); }; drop(loadMediaPermissions()); const loadZoomFactor = async () => { const value = await window.Events.getZoomFactor(); if (canceled) { return; } setZoomFactor(value); }; drop(loadZoomFactor()); // We need to be ready for zoom changes from the keyboard const updateZoomFactorFromIpc = (value: ZoomFactorType) => { if (canceled) { return; } setZoomFactor(value); }; window.Events.onZoomFactorChange(updateZoomFactorFromIpc); return () => { canceled = true; window.Events.offZoomFactorChange(updateZoomFactorFromIpc); }; }, []); const onAutoLaunchChange = async (value: boolean) => { setAutoLaunch(value); await window.Events.setAutoLaunch(value); }; const onZoomFactorChange = async (value: ZoomFactorType) => { setZoomFactor(value); await window.Events.setZoomFactor(value); }; const onMediaCameraPermissionsChange = async (value: boolean) => { setMediaCameraPermissions(value); await window.IPC.setMediaCameraPermissions(value); }; const onMediaPermissionsChange = async (value: boolean) => { setMediaPermissions(value); await window.IPC.setMediaPermissions(value); }; // Simple, one-way items const { backupSubscriptionStatus, backupTier, cloudBackupStatus, lastLocalBackup, localBackupFolder, backupMediaDownloadCompletedBytes, backupMediaDownloadTotalBytes, attachmentDownloadManagerIdled, backupMediaDownloadPaused, } = items; const defaultConversationColor = items.defaultConversationColor || DEFAULT_CONVERSATION_COLOR; const hasLinkPreviews = items.linkPreviews ?? false; const hasReadReceipts = items['read-receipt-setting'] ?? false; const hasTypingIndicators = items.typingIndicators ?? false; const blockedCount = (items['blocked-groups']?.length ?? 0) + (items['blocked-uuids']?.length ?? 0); const emojiSkinToneDefault = items.emojiSkinToneDefault ?? EmojiSkinTone.None; const isInternalUser = items.remoteConfig?.['desktop.internalUser']?.enabled ?? false; const isContentProtectionSupported = Settings.isContentProtectionSupported(OS); const isContentProtectionNeeded = Settings.isContentProtectionNeeded(OS); const backupLocalBackupsEnabled = isLocalBackupsEnabled({ currentVersion: version, remoteConfig: items.remoteConfig, }); const backupFreeMediaDays = getMessageQueueTime(items.remoteConfig) / DAY; const isPlaintextExportEnabled = isFeaturedEnabledSelector({ betaKey: 'desktop.plaintextExport.beta', currentVersion: version, remoteConfig: items.remoteConfig, prodKey: 'desktop.plaintextExport.prod', }); const isKeyTransparencyAvailable = isFeaturedEnabledSelector({ betaKey: 'desktop.keyTransparency.beta', prodKey: 'desktop.keyTransparency.prod', currentVersion: version, remoteConfig: items.remoteConfig, }); // Two-way items function createItemsAccess( key: K, defaultValue: StorageAccessType[K], callback?: (value: StorageAccessType[K]) => void ): [StorageAccessType[K], (value: StorageAccessType[K]) => void] { const value = (items[key] as StorageAccessType[K] | undefined) ?? defaultValue; const setter = (newValue: StorageAccessType[K]) => { putItem(key, newValue); callback?.(newValue); }; return [value, setter]; } const [autoDownloadAttachment, onAutoDownloadAttachmentChange] = createItemsAccess( 'auto-download-attachment', DEFAULT_AUTO_DOWNLOAD_ATTACHMENT ); const [backupKeyViewed, onBackupKeyViewedChange] = createItemsAccess( 'backupKeyViewed', false ); const [hasAudioNotifications, onAudioNotificationsChange] = createItemsAccess( 'audio-notification', false ); const [hasAutoConvertEmoji, onAutoConvertEmojiChange] = createItemsAccess( 'autoConvertEmoji', true ); const [hasKeepMutedChatsArchived, onKeepMutedChatsArchivedChange] = createItemsAccess('keepMutedChatsArchived', false, () => { const account = window.ConversationController.getOurConversationOrThrow(); account.captureChange('keepMutedChatsArchived'); }); const [hasAutoDownloadUpdate, onAutoDownloadUpdateChange] = createItemsAccess( 'auto-download-update', true ); const [hasCallNotifications, onCallNotificationsChange] = createItemsAccess( 'call-system-notification', true ); const [hasIncomingCallNotifications, onIncomingCallNotificationsChange] = createItemsAccess('incoming-call-notification', true); const [hasCallRingtoneNotification, onCallRingtoneNotificationChange] = createItemsAccess('call-ringtone-notification', true); const [hasCountMutedConversations, onCountMutedConversationsChange] = createItemsAccess('badge-count-muted-conversations', false, () => { window.Whisper.events.emit('updateUnreadCount'); }); const [hasHideMenuBar, onHideMenuBarChange] = createItemsAccess( 'hide-menu-bar', false, value => { window.IPC.setAutoHideMenuBar(value); window.IPC.setMenuBarVisibility(!value); } ); const [hasMessageAudio, onMessageAudioChange] = createItemsAccess( 'audioMessage', false ); const [hasNotificationAttention, onNotificationAttentionChange] = createItemsAccess('notification-draw-attention', false); const [notificationContent, onNotificationContentChange] = createItemsAccess( 'notification-setting', 'message' ); const hasNotifications = notificationContent !== 'off'; const onNotificationsChange = (value: boolean) => { putItem( 'notification-setting', value ? DEFAULT_NOTIFICATION_SETTING : 'off' ); }; const [hasRelayCalls, onRelayCallsChange] = createItemsAccess( 'always-relay-calls', false ); const [hasStoriesDisabled, onHasStoriesDisabledChanged] = createItemsAccess( 'hasStoriesDisabled', false, async value => { const account = window.ConversationController.getOurConversationOrThrow(); account.captureChange('hasStoriesDisabled'); onHasStoriesDisabledChange(value); if (!value) { await deleteAllMyStories(); } } ); const [hasKeyTransparencyDisabled, onHasKeyTransparencyDisabledChanged] = createItemsAccess('hasKeyTransparencyDisabled', false, async value => { const account = window.ConversationController.getOurConversationOrThrow(); account.captureChange('hasKeyTransparencyDisabled'); if (value) { await keyTransparency.disable(); } }); const [hasTextFormatting, onTextFormattingChange] = createItemsAccess( 'textFormatting', true ); const [lastSyncTime, onLastSyncTimeChange] = createItemsAccess( 'synced_at', undefined ); const [selectedCamera, onSelectedCameraChange] = createItemsAccess( 'preferred-video-input-device', undefined ); const [selectedMicrophone, onSelectedMicrophoneChange] = createItemsAccess( 'preferred-audio-input-device', undefined ); const [selectedSpeaker, onSelectedSpeakerChange] = createItemsAccess( 'preferred-audio-output-device', undefined ); const [sentMediaQualitySetting, onSentMediaQualityChange] = createItemsAccess( 'sent-media-quality', 'standard' ); const [whoCanFindMe, onWhoCanFindMeChange] = createItemsAccess( 'phoneNumberDiscoverability', PhoneNumberDiscoverability.NotDiscoverable, async (newValue: PhoneNumberDiscoverability) => { await setPhoneNumberDiscoverability( newValue === PhoneNumberDiscoverability.Discoverable ); const account = window.ConversationController.getOurConversationOrThrow(); account.captureChange('phoneNumberDiscoverability'); } ); const [whoCanSeeMe, onWhoCanSeeMeChange] = createItemsAccess( 'phoneNumberSharingMode', PhoneNumberSharingMode.Nobody, async (newValue: PhoneNumberSharingMode) => { const account = window.ConversationController.getOurConversationOrThrow(); if (newValue === PhoneNumberSharingMode.Everybody) { onWhoCanFindMeChange(PhoneNumberDiscoverability.Discoverable); } account.captureChange('phoneNumberSharingMode'); // Write profile after updating storage so that the write has up-to-date // information. await writeProfile(getConversation(account), { keepAvatar: true, }); } ); const internalDeleteAllMegaphones = useCallback(() => { return DataWriter.internalDeleteAllMegaphones(); }, []); const __dangerouslyRunAbitraryReadOnlySqlQuery = useCallback( (readOnlySqlQuery: string) => { return DataReader.__dangerouslyRunAbitraryReadOnlySqlQuery( readOnlySqlQuery ); }, [] ); const cqsTestMode = items.cqsTestMode ?? false; const setCqsTestMode = useCallback((value: boolean) => { drop(itemStorage.put('cqsTestMode', value)); }, []); const setDredDuration = useCallback((value: number | undefined) => { drop(itemStorage.put('dredDuration', value)); }, []); const setIsDirectVp9Enabled = useCallback((value: boolean | undefined) => { drop(itemStorage.put('isDirectVp9Enabled', value)); }, []); const setDirectMaxBitrate = useCallback((value: number | undefined) => { drop(itemStorage.put('directMaxBitrate', value)); }, []); const setIsGroupVp9Enabled = useCallback((value: boolean | undefined) => { drop(itemStorage.put('isGroupVp9Enabled', value)); }, []); const setGroupMaxBitrate = useCallback((value: number | undefined) => { drop(itemStorage.put('groupMaxBitrate', value)); }, []); const setSfuUrl = useCallback((value: string | undefined) => { drop(itemStorage.put('sfuUrl', value)); }, []); if (currentLocation.tab !== NavTab.Settings) { return null; } const settingsLocation = currentLocation.details; const setSettingsLocation = (location: SettingsLocation) => { changeLocation({ tab: NavTab.Settings, details: location, }); }; const accountEntropyPool = itemStorage.get('accountEntropyPool'); return ( ); }