2403 lines
80 KiB
TypeScript
2403 lines
80 KiB
TypeScript
// Copyright 2021 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import type { AudioDevice } from '@signalapp/ringrtc';
|
|
import React, {
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
useId,
|
|
} from 'react';
|
|
import { isNumber, noop, partition } from 'lodash';
|
|
import classNames from 'classnames';
|
|
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
|
import type { MutableRefObject, ReactNode } from 'react';
|
|
import { Button, ButtonVariant } from './Button';
|
|
import { ChatColorPicker } from './ChatColorPicker';
|
|
import { Checkbox } from './Checkbox';
|
|
import { WidthBreakpoint } from './_util';
|
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
|
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
|
|
import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
|
|
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
|
|
import { Select } from './Select';
|
|
import { Spinner } from './Spinner';
|
|
import { getCustomColorStyle } from '../util/getCustomColorStyle';
|
|
import {
|
|
DEFAULT_DURATIONS_IN_SECONDS,
|
|
DEFAULT_DURATIONS_SET,
|
|
format as formatExpirationTimer,
|
|
} from '../util/expirationTimer';
|
|
import { DurationInSeconds } from '../util/durations';
|
|
import { focusableSelector } from '../util/focusableSelectors';
|
|
import { Modal } from './Modal';
|
|
import { SearchInput } from './SearchInput';
|
|
import { removeDiacritics } from '../util/removeDiacritics';
|
|
import { assertDev, strictAssert } from '../util/assert';
|
|
import { I18n } from './I18n';
|
|
import { FunSkinTonesList } from './fun/FunSkinTones';
|
|
import { emojiParentKeyConstant, type EmojiSkinTone } from './fun/data/emojis';
|
|
import {
|
|
SettingsControl as Control,
|
|
FlowingSettingsControl as FlowingControl,
|
|
SettingsRadio,
|
|
SettingsRow,
|
|
} from './PreferencesUtil';
|
|
import { PreferencesBackups } from './PreferencesBackups';
|
|
import { PreferencesInternal } from './PreferencesInternal';
|
|
import { FunEmojiLocalizationProvider } from './fun/FunEmojiLocalizationProvider';
|
|
import { Avatar, AvatarSize } from './Avatar';
|
|
import { NavSidebar } from './NavSidebar';
|
|
|
|
import type { MediaDeviceSettings } from '../types/Calling';
|
|
import type { ValidationResultType as BackupValidationResultType } from '../services/backups';
|
|
import type {
|
|
AutoDownloadAttachmentType,
|
|
NotificationSettingType,
|
|
SentMediaQualitySettingType,
|
|
ZoomFactorType,
|
|
} from '../types/Storage.d';
|
|
import type { ThemeSettingType } from '../types/StorageUIKeys';
|
|
import type { AnyToast } from '../types/Toast';
|
|
import { ToastType } from '../types/Toast';
|
|
import type { ConversationType } from '../state/ducks/conversations';
|
|
import type {
|
|
ConversationColorType,
|
|
CustomColorType,
|
|
DefaultConversationColorType,
|
|
} from '../types/Colors';
|
|
import type {
|
|
LocalizerType,
|
|
SentMediaQualityType,
|
|
ThemeType,
|
|
} from '../types/Util';
|
|
import type {
|
|
BackupsSubscriptionType,
|
|
BackupStatusType,
|
|
} from '../types/backups';
|
|
import type { UnreadStats } from '../util/countUnreadStats';
|
|
import type { BadgeType } from '../badges/types';
|
|
import type { MessageCountBySchemaVersionType } from '../sql/Interface';
|
|
import type { MessageAttributesType } from '../model-types';
|
|
import { isBackupPage } from '../types/PreferencesBackupPage';
|
|
import type { PreferencesBackupPage } from '../types/PreferencesBackupPage';
|
|
import type {
|
|
PromptOSAuthReasonType,
|
|
PromptOSAuthResultType,
|
|
} from '../util/os/promptOSAuthMain';
|
|
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
|
import { EditChatFoldersPage } from './preferences/EditChatFoldersPage';
|
|
import { ChatFoldersPage } from './preferences/ChatFoldersPage';
|
|
import type {
|
|
ChatFolderId,
|
|
ChatFolderParams,
|
|
ChatFolderRecord,
|
|
} from '../types/ChatFolder';
|
|
import {
|
|
CHAT_FOLDER_DEFAULTS,
|
|
isChatFoldersEnabled,
|
|
} from '../types/ChatFolder';
|
|
import type { GetConversationByIdType } from '../state/selectors/conversations';
|
|
|
|
type CheckboxChangeHandlerType = (value: boolean) => unknown;
|
|
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
|
|
|
|
export type PropsDataType = {
|
|
conversations: ReadonlyArray<ConversationType>;
|
|
conversationSelector: GetConversationByIdType;
|
|
|
|
// Settings
|
|
accountEntropyPool: string | undefined;
|
|
autoDownloadAttachment: AutoDownloadAttachmentType;
|
|
backupFeatureEnabled: boolean;
|
|
backupKeyViewed: boolean;
|
|
backupLocalBackupsEnabled: boolean;
|
|
localBackupFolder: string | undefined;
|
|
cloudBackupStatus?: BackupStatusType;
|
|
backupSubscriptionStatus: BackupsSubscriptionType;
|
|
blockedCount: number;
|
|
customColors: Record<string, CustomColorType>;
|
|
defaultConversationColor: DefaultConversationColorType;
|
|
deviceName?: string;
|
|
donationsFeatureEnabled: boolean;
|
|
emojiSkinToneDefault: EmojiSkinTone;
|
|
hasAudioNotifications?: boolean;
|
|
hasAutoConvertEmoji: boolean;
|
|
hasAutoDownloadUpdate: boolean;
|
|
hasAutoLaunch: boolean | undefined;
|
|
hasCallNotifications: boolean;
|
|
hasCallRingtoneNotification: boolean;
|
|
hasContentProtection: boolean | undefined;
|
|
hasCountMutedConversations: boolean;
|
|
hasHideMenuBar?: boolean;
|
|
hasIncomingCallNotifications: boolean;
|
|
hasLinkPreviews: boolean;
|
|
hasMediaCameraPermissions: boolean | undefined;
|
|
hasMediaPermissions: boolean | undefined;
|
|
hasMessageAudio: boolean;
|
|
hasMinimizeToAndStartInSystemTray: boolean | undefined;
|
|
hasMinimizeToSystemTray: boolean | undefined;
|
|
hasNotificationAttention: boolean;
|
|
hasNotifications: boolean;
|
|
hasReadReceipts: boolean;
|
|
hasRelayCalls?: boolean;
|
|
hasSpellCheck: boolean | undefined;
|
|
hasStoriesDisabled: boolean;
|
|
hasTextFormatting: boolean;
|
|
hasTypingIndicators: boolean;
|
|
page: Page;
|
|
lastSyncTime?: number;
|
|
notificationContent: NotificationSettingType;
|
|
phoneNumber: string | undefined;
|
|
selectedCamera?: string;
|
|
selectedMicrophone?: AudioDevice;
|
|
selectedSpeaker?: AudioDevice;
|
|
sentMediaQualitySetting: SentMediaQualitySettingType;
|
|
themeSetting: ThemeSettingType | undefined;
|
|
universalExpireTimer: DurationInSeconds;
|
|
whoCanFindMe: PhoneNumberDiscoverability;
|
|
whoCanSeeMe: PhoneNumberSharingMode;
|
|
zoomFactor: ZoomFactorType | undefined;
|
|
|
|
// Localization
|
|
availableLocales: ReadonlyArray<string>;
|
|
localeOverride: string | null | undefined;
|
|
preferredSystemLocales: ReadonlyArray<string>;
|
|
resolvedLocale: string;
|
|
|
|
// Other props
|
|
badge: BadgeType | undefined;
|
|
hasFailedStorySends: boolean;
|
|
initialSpellCheckSetting: boolean;
|
|
me: ConversationType;
|
|
navTabsCollapsed: boolean;
|
|
otherTabsUnreadStats: UnreadStats;
|
|
preferredWidthFromStorage: number;
|
|
shouldShowUpdateDialog: boolean;
|
|
theme: ThemeType;
|
|
|
|
// Limited support features
|
|
isAutoDownloadUpdatesSupported: boolean;
|
|
isAutoLaunchSupported: boolean;
|
|
isContentProtectionNeeded: boolean;
|
|
isContentProtectionSupported: boolean;
|
|
isHideMenuBarSupported: boolean;
|
|
isNotificationAttentionSupported: boolean;
|
|
isSyncSupported: boolean;
|
|
isSystemTraySupported: boolean;
|
|
isMinimizeToAndStartInSystemTraySupported: boolean;
|
|
isInternalUser: boolean;
|
|
|
|
// Devices
|
|
availableCameras: Array<
|
|
Pick<MediaDeviceInfo, 'deviceId' | 'groupId' | 'kind' | 'label'>
|
|
>;
|
|
} & Omit<MediaDeviceSettings, 'availableCameras'>;
|
|
|
|
type PropsFunctionType = {
|
|
// Render props
|
|
renderDonationsPane: (options: {
|
|
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
|
}) => JSX.Element;
|
|
renderProfileEditor: (options: {
|
|
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
|
}) => JSX.Element;
|
|
renderToastManager: (
|
|
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
|
) => JSX.Element;
|
|
renderUpdateDialog: (
|
|
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
|
) => JSX.Element;
|
|
|
|
// Other props
|
|
addCustomColor: (color: CustomColorType) => unknown;
|
|
doDeleteAllData: () => unknown;
|
|
editCustomColor: (colorId: string, color: CustomColorType) => unknown;
|
|
exportLocalBackup: () => Promise<BackupValidationResultType>;
|
|
getMessageCountBySchemaVersion: () => Promise<MessageCountBySchemaVersionType>;
|
|
getMessageSampleForSchemaVersion: (
|
|
version: number
|
|
) => Promise<Array<MessageAttributesType>>;
|
|
getConversationsWithCustomColor: (colorId: string) => Array<ConversationType>;
|
|
getPreferredBadge: PreferredBadgeSelectorType;
|
|
makeSyncRequest: () => unknown;
|
|
onStartUpdate: () => unknown;
|
|
pickLocalBackupFolder: () => Promise<string | undefined>;
|
|
refreshCloudBackupStatus: () => void;
|
|
refreshBackupSubscriptionStatus: () => void;
|
|
removeCustomColor: (colorId: string) => unknown;
|
|
removeCustomColorOnConversations: (colorId: string) => unknown;
|
|
promptOSAuth: (
|
|
reason: PromptOSAuthReasonType
|
|
) => Promise<PromptOSAuthResultType>;
|
|
resetAllChatColors: () => unknown;
|
|
resetDefaultChatColor: () => unknown;
|
|
savePreferredLeftPaneWidth: (_: number) => void;
|
|
setGlobalDefaultConversationColor: (
|
|
color: ConversationColorType,
|
|
customColorData?: {
|
|
id: string;
|
|
value: CustomColorType;
|
|
}
|
|
) => unknown;
|
|
setPage: (page: Page) => unknown;
|
|
showToast: (toast: AnyToast) => unknown;
|
|
validateBackup: () => Promise<BackupValidationResultType>;
|
|
|
|
// Change handlers
|
|
onAudioNotificationsChange: CheckboxChangeHandlerType;
|
|
onAutoConvertEmojiChange: CheckboxChangeHandlerType;
|
|
onAutoDownloadAttachmentChange: (
|
|
setting: AutoDownloadAttachmentType
|
|
) => unknown;
|
|
onAutoDownloadUpdateChange: CheckboxChangeHandlerType;
|
|
onAutoLaunchChange: CheckboxChangeHandlerType;
|
|
onBackupKeyViewedChange: (keyViewed: boolean) => void;
|
|
onCallNotificationsChange: CheckboxChangeHandlerType;
|
|
onCallRingtoneNotificationChange: CheckboxChangeHandlerType;
|
|
onContentProtectionChange: CheckboxChangeHandlerType;
|
|
onCountMutedConversationsChange: CheckboxChangeHandlerType;
|
|
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void;
|
|
onHasStoriesDisabledChanged: SelectChangeHandlerType<boolean>;
|
|
onHideMenuBarChange: CheckboxChangeHandlerType;
|
|
onIncomingCallNotificationsChange: CheckboxChangeHandlerType;
|
|
onLastSyncTimeChange: (time: number) => unknown;
|
|
onLocaleChange: (locale: string | null | undefined) => void;
|
|
onMediaCameraPermissionsChange: CheckboxChangeHandlerType;
|
|
onMediaPermissionsChange: CheckboxChangeHandlerType;
|
|
onMessageAudioChange: CheckboxChangeHandlerType;
|
|
onMinimizeToAndStartInSystemTrayChange: CheckboxChangeHandlerType;
|
|
onMinimizeToSystemTrayChange: CheckboxChangeHandlerType;
|
|
onNotificationAttentionChange: CheckboxChangeHandlerType;
|
|
onNotificationContentChange: SelectChangeHandlerType<NotificationSettingType>;
|
|
onNotificationsChange: CheckboxChangeHandlerType;
|
|
onRelayCallsChange: CheckboxChangeHandlerType;
|
|
onSelectedCameraChange: SelectChangeHandlerType<string | undefined>;
|
|
onSelectedMicrophoneChange: SelectChangeHandlerType<AudioDevice | undefined>;
|
|
onSelectedSpeakerChange: SelectChangeHandlerType<AudioDevice | undefined>;
|
|
onSentMediaQualityChange: SelectChangeHandlerType<SentMediaQualityType>;
|
|
onSpellCheckChange: CheckboxChangeHandlerType;
|
|
onTextFormattingChange: CheckboxChangeHandlerType;
|
|
onThemeChange: SelectChangeHandlerType<ThemeType>;
|
|
onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
|
|
onUniversalExpireTimerChange: SelectChangeHandlerType<number>;
|
|
onWhoCanSeeMeChange: SelectChangeHandlerType<PhoneNumberSharingMode>;
|
|
onWhoCanFindMeChange: SelectChangeHandlerType<PhoneNumberDiscoverability>;
|
|
onZoomFactorChange: SelectChangeHandlerType<ZoomFactorType>;
|
|
|
|
// Localization
|
|
i18n: LocalizerType;
|
|
};
|
|
|
|
export type PropsType = PropsDataType & PropsFunctionType;
|
|
|
|
export type PropsPreloadType = Omit<PropsType, 'i18n'>;
|
|
|
|
export enum Page {
|
|
// Accessible through left nav
|
|
Profile = 'Profile',
|
|
General = 'General',
|
|
Donations = 'Donations',
|
|
Appearance = 'Appearance',
|
|
Chats = 'Chats',
|
|
Calls = 'Calls',
|
|
Notifications = 'Notifications',
|
|
Privacy = 'Privacy',
|
|
DataUsage = 'DataUsage',
|
|
Backups = 'Backups',
|
|
Internal = 'Internal',
|
|
|
|
// Sub pages
|
|
ChatColor = 'ChatColor',
|
|
ChatFolders = 'ChatFolders',
|
|
EditChatFolder = 'EditChatFolder',
|
|
PNP = 'PNP',
|
|
BackupsDetails = 'BackupsDetails',
|
|
LocalBackups = 'LocalBackups',
|
|
LocalBackupsSetupFolder = 'LocalBackupsSetupFolder',
|
|
LocalBackupsSetupKey = 'LocalBackupsSetupKey',
|
|
LocalBackupsKeyReference = 'LocalBackupsKeyReference',
|
|
}
|
|
|
|
enum LanguageDialog {
|
|
Selection,
|
|
Confirmation,
|
|
}
|
|
|
|
const DEFAULT_ZOOM_FACTORS = [
|
|
{
|
|
text: '75%',
|
|
value: 0.75,
|
|
},
|
|
{
|
|
text: '100%',
|
|
value: 1,
|
|
},
|
|
{
|
|
text: '125%',
|
|
value: 1.25,
|
|
},
|
|
{
|
|
text: '150%',
|
|
value: 1.5,
|
|
},
|
|
{
|
|
text: '200%',
|
|
value: 2,
|
|
},
|
|
];
|
|
|
|
export function Preferences({
|
|
conversations,
|
|
conversationSelector,
|
|
accountEntropyPool,
|
|
addCustomColor,
|
|
autoDownloadAttachment,
|
|
availableCameras,
|
|
availableLocales,
|
|
availableMicrophones,
|
|
availableSpeakers,
|
|
backupFeatureEnabled,
|
|
backupKeyViewed,
|
|
backupSubscriptionStatus,
|
|
backupLocalBackupsEnabled,
|
|
badge,
|
|
blockedCount,
|
|
cloudBackupStatus,
|
|
customColors,
|
|
defaultConversationColor,
|
|
deviceName = '',
|
|
doDeleteAllData,
|
|
donationsFeatureEnabled,
|
|
editCustomColor,
|
|
emojiSkinToneDefault,
|
|
exportLocalBackup,
|
|
getConversationsWithCustomColor,
|
|
getMessageCountBySchemaVersion,
|
|
getMessageSampleForSchemaVersion,
|
|
getPreferredBadge,
|
|
hasAudioNotifications,
|
|
hasAutoConvertEmoji,
|
|
hasAutoDownloadUpdate,
|
|
hasAutoLaunch,
|
|
hasCallNotifications,
|
|
hasCallRingtoneNotification,
|
|
hasContentProtection,
|
|
hasCountMutedConversations,
|
|
hasFailedStorySends,
|
|
hasHideMenuBar,
|
|
hasIncomingCallNotifications,
|
|
hasLinkPreviews,
|
|
hasMediaCameraPermissions,
|
|
hasMediaPermissions,
|
|
hasMessageAudio,
|
|
hasMinimizeToAndStartInSystemTray,
|
|
hasMinimizeToSystemTray,
|
|
hasNotificationAttention,
|
|
hasNotifications,
|
|
hasReadReceipts,
|
|
hasRelayCalls,
|
|
hasSpellCheck,
|
|
hasStoriesDisabled,
|
|
hasTextFormatting,
|
|
hasTypingIndicators,
|
|
i18n,
|
|
initialSpellCheckSetting,
|
|
isAutoDownloadUpdatesSupported,
|
|
isAutoLaunchSupported,
|
|
isContentProtectionNeeded,
|
|
isContentProtectionSupported,
|
|
isHideMenuBarSupported,
|
|
isNotificationAttentionSupported,
|
|
isSyncSupported,
|
|
isSystemTraySupported,
|
|
isMinimizeToAndStartInSystemTraySupported,
|
|
isInternalUser,
|
|
lastSyncTime,
|
|
localBackupFolder,
|
|
makeSyncRequest,
|
|
me,
|
|
navTabsCollapsed,
|
|
notificationContent,
|
|
onAudioNotificationsChange,
|
|
onAutoConvertEmojiChange,
|
|
onAutoDownloadAttachmentChange,
|
|
onAutoDownloadUpdateChange,
|
|
onAutoLaunchChange,
|
|
onBackupKeyViewedChange,
|
|
onCallNotificationsChange,
|
|
onCallRingtoneNotificationChange,
|
|
onContentProtectionChange,
|
|
onCountMutedConversationsChange,
|
|
onEmojiSkinToneDefaultChange,
|
|
onHasStoriesDisabledChanged,
|
|
onHideMenuBarChange,
|
|
onIncomingCallNotificationsChange,
|
|
onLastSyncTimeChange,
|
|
onLocaleChange,
|
|
onMediaCameraPermissionsChange,
|
|
onMediaPermissionsChange,
|
|
onMessageAudioChange,
|
|
onMinimizeToAndStartInSystemTrayChange,
|
|
onMinimizeToSystemTrayChange,
|
|
onNotificationAttentionChange,
|
|
onNotificationContentChange,
|
|
onNotificationsChange,
|
|
onRelayCallsChange,
|
|
onSelectedCameraChange,
|
|
onSelectedMicrophoneChange,
|
|
onSelectedSpeakerChange,
|
|
onSentMediaQualityChange,
|
|
onSpellCheckChange,
|
|
onTextFormattingChange,
|
|
onThemeChange,
|
|
onToggleNavTabsCollapse,
|
|
onUniversalExpireTimerChange,
|
|
onWhoCanSeeMeChange,
|
|
onWhoCanFindMeChange,
|
|
onZoomFactorChange,
|
|
otherTabsUnreadStats,
|
|
page,
|
|
phoneNumber = '',
|
|
pickLocalBackupFolder,
|
|
preferredSystemLocales,
|
|
preferredWidthFromStorage,
|
|
refreshCloudBackupStatus,
|
|
refreshBackupSubscriptionStatus,
|
|
removeCustomColor,
|
|
removeCustomColorOnConversations,
|
|
renderDonationsPane,
|
|
renderProfileEditor,
|
|
renderToastManager,
|
|
renderUpdateDialog,
|
|
promptOSAuth,
|
|
resetAllChatColors,
|
|
resetDefaultChatColor,
|
|
resolvedLocale,
|
|
savePreferredLeftPaneWidth,
|
|
selectedCamera,
|
|
selectedMicrophone,
|
|
selectedSpeaker,
|
|
sentMediaQualitySetting,
|
|
setGlobalDefaultConversationColor,
|
|
setPage,
|
|
shouldShowUpdateDialog,
|
|
showToast,
|
|
localeOverride,
|
|
theme,
|
|
themeSetting,
|
|
universalExpireTimer = DurationInSeconds.ZERO,
|
|
validateBackup,
|
|
whoCanFindMe,
|
|
whoCanSeeMe,
|
|
zoomFactor,
|
|
}: PropsType): JSX.Element {
|
|
const storiesId = useId();
|
|
const themeSelectId = useId();
|
|
const zoomSelectId = useId();
|
|
const languageId = useId();
|
|
|
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
const [confirmStoriesOff, setConfirmStoriesOff] = useState(false);
|
|
const [confirmContentProtection, setConfirmContentProtection] =
|
|
useState(false);
|
|
const [showSyncFailed, setShowSyncFailed] = useState(false);
|
|
const [nowSyncing, setNowSyncing] = useState(false);
|
|
const [showDisappearingTimerDialog, setShowDisappearingTimerDialog] =
|
|
useState(false);
|
|
const [languageDialog, setLanguageDialog] = useState<LanguageDialog | null>(
|
|
null
|
|
);
|
|
const [selectedLanguageLocale, setSelectedLanguageLocale] = useState<
|
|
string | null | undefined
|
|
>(localeOverride);
|
|
const [languageSearchInput, setLanguageSearchInput] = useState('');
|
|
const [confirmPnpNotDiscoverable, setConfirmPnpNoDiscoverable] =
|
|
useState(false);
|
|
|
|
const [chatFolders, setChatFolders] = useState<
|
|
ReadonlyArray<ChatFolderRecord>
|
|
>([]);
|
|
|
|
const [editChatFolderPageId, setEditChatFolderPageId] =
|
|
useState<ChatFolderId | null>(null);
|
|
|
|
const handleOpenEditChatFoldersPage = useCallback(
|
|
(chatFolderId: ChatFolderId | null) => {
|
|
setPage(Page.EditChatFolder);
|
|
setEditChatFolderPageId(chatFolderId);
|
|
},
|
|
[setPage]
|
|
);
|
|
|
|
const handleCloseEditChatFoldersPage = useCallback(() => {
|
|
setPage(Page.ChatFolders);
|
|
setEditChatFolderPageId(null);
|
|
}, [setPage]);
|
|
|
|
const handleCreateChatFolder = useCallback((params: ChatFolderParams) => {
|
|
setChatFolders(prev => {
|
|
return [...prev, { ...params, id: String(prev.length) as ChatFolderId }];
|
|
});
|
|
}, []);
|
|
|
|
const handleUpdateChatFolder = useCallback(
|
|
(chatFolderId: ChatFolderId, chatFolderParams: ChatFolderParams) => {
|
|
setChatFolders(prev => {
|
|
return prev.map(chatFolder => {
|
|
if (chatFolder.id === chatFolderId) {
|
|
return { id: chatFolderId, ...chatFolderParams };
|
|
}
|
|
return chatFolder;
|
|
});
|
|
});
|
|
},
|
|
[]
|
|
);
|
|
|
|
const handleDeleteChatFolder = useCallback((chatFolderId: ChatFolderId) => {
|
|
setChatFolders(prev => {
|
|
return prev.filter(chatFolder => {
|
|
return chatFolder.id !== chatFolderId;
|
|
});
|
|
});
|
|
}, []);
|
|
|
|
function closeLanguageDialog() {
|
|
setLanguageDialog(null);
|
|
setSelectedLanguageLocale(localeOverride);
|
|
}
|
|
const shouldShowBackupsPage =
|
|
backupFeatureEnabled || backupLocalBackupsEnabled;
|
|
|
|
if (page === Page.Backups && !shouldShowBackupsPage) {
|
|
setPage(Page.General);
|
|
}
|
|
if (page === Page.Internal && !isInternalUser) {
|
|
setPage(Page.General);
|
|
}
|
|
|
|
let maybeUpdateDialog: JSX.Element | undefined;
|
|
if (shouldShowUpdateDialog) {
|
|
maybeUpdateDialog = renderUpdateDialog({
|
|
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
|
});
|
|
}
|
|
|
|
const onZoomSelectChange = useCallback(
|
|
(value: string) => {
|
|
const number = parseFloat(value);
|
|
onZoomFactorChange(number as unknown as ZoomFactorType);
|
|
},
|
|
[onZoomFactorChange]
|
|
);
|
|
|
|
const onAudioInputSelectChange = useCallback(
|
|
(value: string) => {
|
|
if (value === 'undefined') {
|
|
onSelectedMicrophoneChange(undefined);
|
|
} else {
|
|
onSelectedMicrophoneChange(availableMicrophones[parseInt(value, 10)]);
|
|
}
|
|
},
|
|
[onSelectedMicrophoneChange, availableMicrophones]
|
|
);
|
|
|
|
const handleContentProtectionChange = useCallback(
|
|
(value: boolean) => {
|
|
if (value === true || !isContentProtectionNeeded) {
|
|
onContentProtectionChange(value);
|
|
} else {
|
|
setConfirmContentProtection(true);
|
|
}
|
|
},
|
|
[onContentProtectionChange, isContentProtectionNeeded]
|
|
);
|
|
|
|
const settingsPaneRef = useRef<HTMLDivElement | null>(null);
|
|
useEffect(() => {
|
|
const settingsPane = settingsPaneRef.current;
|
|
if (!settingsPane) {
|
|
return;
|
|
}
|
|
|
|
const elements = settingsPane.querySelectorAll<
|
|
| HTMLAnchorElement
|
|
| HTMLButtonElement
|
|
| HTMLInputElement
|
|
| HTMLSelectElement
|
|
| HTMLTextAreaElement
|
|
>(focusableSelector);
|
|
if (!elements.length) {
|
|
return;
|
|
}
|
|
elements[0]?.focus();
|
|
}, [page]);
|
|
|
|
const onAudioOutputSelectChange = useCallback(
|
|
(value: string) => {
|
|
if (value === 'undefined') {
|
|
onSelectedSpeakerChange(undefined);
|
|
} else {
|
|
onSelectedSpeakerChange(availableSpeakers[parseInt(value, 10)]);
|
|
}
|
|
},
|
|
[onSelectedSpeakerChange, availableSpeakers]
|
|
);
|
|
|
|
const localeDisplayNames = window.SignalContext.getLocaleDisplayNames();
|
|
|
|
const getLocaleDisplayName = useCallback(
|
|
(inLocale: string, ofLocale: string): string => {
|
|
const displayName = localeDisplayNames[inLocale]?.[ofLocale];
|
|
assertDev(
|
|
displayName != null,
|
|
`Locale display name in ${inLocale} of ${ofLocale} does not exist`
|
|
);
|
|
return (
|
|
displayName ??
|
|
new Intl.DisplayNames(inLocale, {
|
|
type: 'language',
|
|
languageDisplay: 'standard',
|
|
style: 'long',
|
|
fallback: 'code',
|
|
}).of(ofLocale)
|
|
);
|
|
},
|
|
[localeDisplayNames]
|
|
);
|
|
|
|
const localeSearchOptions = useMemo(() => {
|
|
const collator = new Intl.Collator('en', { usage: 'sort' });
|
|
|
|
const availableLocalesOptions = availableLocales
|
|
.map(locale => {
|
|
const currentLocaleLabel = getLocaleDisplayName(resolvedLocale, locale);
|
|
const matchingLocaleLabel = getLocaleDisplayName(locale, locale);
|
|
return { locale, currentLocaleLabel, matchingLocaleLabel };
|
|
})
|
|
.sort((a, b) => {
|
|
return collator.compare(a.locale, b.locale);
|
|
});
|
|
|
|
const [localeOverrideMatches, localeOverrideNonMatches] = partition(
|
|
availableLocalesOptions,
|
|
option => {
|
|
return option.locale === localeOverride;
|
|
}
|
|
);
|
|
|
|
const preferredSystemLocaleMatch = LocaleMatcher.match(
|
|
preferredSystemLocales as Array<string>, // bad types
|
|
availableLocales as Array<string>, // bad types
|
|
'en',
|
|
{ algorithm: 'best fit' }
|
|
);
|
|
|
|
return [
|
|
...localeOverrideMatches,
|
|
{
|
|
locale: null,
|
|
currentLocaleLabel: i18n('icu:Preferences__Language__SystemLanguage'),
|
|
matchingLocaleLabel: getLocaleDisplayName(
|
|
preferredSystemLocaleMatch,
|
|
preferredSystemLocaleMatch
|
|
),
|
|
},
|
|
...localeOverrideNonMatches,
|
|
];
|
|
}, [
|
|
i18n,
|
|
availableLocales,
|
|
resolvedLocale,
|
|
localeOverride,
|
|
preferredSystemLocales,
|
|
getLocaleDisplayName,
|
|
]);
|
|
|
|
const localeSearchResults = useMemo(() => {
|
|
return localeSearchOptions.filter(option => {
|
|
const input = removeDiacritics(languageSearchInput.trim().toLowerCase());
|
|
|
|
if (input === '') {
|
|
return true;
|
|
}
|
|
|
|
function isMatch(value: string) {
|
|
return removeDiacritics(value.toLowerCase()).includes(input);
|
|
}
|
|
|
|
return (
|
|
isMatch(option.currentLocaleLabel) ||
|
|
(option.matchingLocaleLabel && isMatch(option.matchingLocaleLabel))
|
|
);
|
|
});
|
|
}, [localeSearchOptions, languageSearchInput]);
|
|
|
|
let content: JSX.Element | undefined;
|
|
|
|
if (page === Page.Profile) {
|
|
content = renderProfileEditor({
|
|
contentsRef: settingsPaneRef,
|
|
});
|
|
} else if (page === Page.General) {
|
|
const pageContents = (
|
|
<>
|
|
<SettingsRow>
|
|
<FlowingControl>
|
|
<div className="Preferences__half-flow">
|
|
{i18n('icu:Preferences--phone-number')}
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
'Preferences__flow-value',
|
|
'Preferences__half-flow',
|
|
'Preferences__half-flow--align-right'
|
|
)}
|
|
>
|
|
{phoneNumber}
|
|
</div>
|
|
</FlowingControl>
|
|
<FlowingControl>
|
|
<div className="Preferences__half-flow">
|
|
{i18n('icu:Preferences--device-name')}
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
'Preferences__flow-value',
|
|
'Preferences__half-flow',
|
|
'Preferences__half-flow--align-right'
|
|
)}
|
|
>
|
|
{deviceName}
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
'Preferences__device-name-description',
|
|
'Preferences__description',
|
|
'Preferences__full-flow'
|
|
)}
|
|
>
|
|
{i18n('icu:Preferences--device-name__description')}
|
|
</div>
|
|
</FlowingControl>
|
|
</SettingsRow>
|
|
<SettingsRow title={i18n('icu:Preferences--system')}>
|
|
{isAutoLaunchSupported && (
|
|
<Checkbox
|
|
checked={hasAutoLaunch}
|
|
disabled={hasAutoLaunch === undefined}
|
|
label={i18n('icu:autoLaunchDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="autoLaunch"
|
|
onChange={onAutoLaunchChange}
|
|
/>
|
|
)}
|
|
{isHideMenuBarSupported && (
|
|
<Checkbox
|
|
checked={hasHideMenuBar}
|
|
label={i18n('icu:hideMenuBar')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="hideMenuBar"
|
|
onChange={onHideMenuBarChange}
|
|
/>
|
|
)}
|
|
{isSystemTraySupported && (
|
|
<>
|
|
<Checkbox
|
|
checked={hasMinimizeToSystemTray}
|
|
disabled={hasMinimizeToSystemTray === undefined}
|
|
label={i18n('icu:SystemTraySetting__minimize-to-system-tray')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="system-tray-setting-minimize-to-system-tray"
|
|
onChange={onMinimizeToSystemTrayChange}
|
|
/>
|
|
{isMinimizeToAndStartInSystemTraySupported && (
|
|
<Checkbox
|
|
checked={hasMinimizeToAndStartInSystemTray}
|
|
disabled={
|
|
!hasMinimizeToSystemTray ||
|
|
hasMinimizeToAndStartInSystemTray === undefined
|
|
}
|
|
label={i18n(
|
|
'icu:SystemTraySetting__minimize-to-and-start-in-system-tray'
|
|
)}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="system-tray-setting-minimize-to-and-start-in-system-tray"
|
|
onChange={onMinimizeToAndStartInSystemTrayChange}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</SettingsRow>
|
|
<SettingsRow title={i18n('icu:permissions')}>
|
|
<Checkbox
|
|
checked={hasMediaPermissions}
|
|
disabled={hasMediaPermissions === undefined}
|
|
label={i18n('icu:mediaPermissionsDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="mediaPermissions"
|
|
onChange={onMediaPermissionsChange}
|
|
/>
|
|
<Checkbox
|
|
checked={hasMediaCameraPermissions ?? false}
|
|
disabled={hasMediaCameraPermissions === undefined}
|
|
label={i18n('icu:mediaCameraPermissionsDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="mediaCameraPermissions"
|
|
onChange={onMediaCameraPermissionsChange}
|
|
/>
|
|
</SettingsRow>
|
|
{isAutoDownloadUpdatesSupported && (
|
|
<SettingsRow title={i18n('icu:Preferences--updates')}>
|
|
<Checkbox
|
|
checked={hasAutoDownloadUpdate}
|
|
label={i18n('icu:Preferences__download-update')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="autoDownloadUpdate"
|
|
onChange={onAutoDownloadUpdateChange}
|
|
/>
|
|
</SettingsRow>
|
|
)}
|
|
</>
|
|
);
|
|
content = (
|
|
<PreferencesContent
|
|
contents={pageContents}
|
|
contentsRef={settingsPaneRef}
|
|
title={i18n('icu:Preferences__button--general')}
|
|
/>
|
|
);
|
|
} else if (page === Page.Donations) {
|
|
content = renderDonationsPane({
|
|
contentsRef: settingsPaneRef,
|
|
});
|
|
} else if (page === Page.Appearance) {
|
|
let zoomFactors = DEFAULT_ZOOM_FACTORS;
|
|
|
|
if (
|
|
isNumber(zoomFactor) &&
|
|
!zoomFactors.some(({ value }) => value === zoomFactor)
|
|
) {
|
|
zoomFactors = [
|
|
...zoomFactors,
|
|
{
|
|
text: `${Math.round(zoomFactor * 100)}%`,
|
|
value: zoomFactor,
|
|
},
|
|
].sort((a, b) => a.value - b.value);
|
|
}
|
|
let localeText = '';
|
|
if (localeOverride !== undefined) {
|
|
localeText =
|
|
localeOverride != null
|
|
? getLocaleDisplayName(resolvedLocale, localeOverride)
|
|
: i18n('icu:Preferences__Language__SystemLanguage');
|
|
}
|
|
|
|
const pageContents = (
|
|
<SettingsRow>
|
|
<Control
|
|
icon="Preferences__LanguageIcon"
|
|
left={i18n('icu:Preferences__Language__Label')}
|
|
right={
|
|
<span
|
|
className="Preferences__LanguageButton"
|
|
lang={localeOverride ?? resolvedLocale}
|
|
>
|
|
{localeText}
|
|
</span>
|
|
}
|
|
onClick={() => {
|
|
// We haven't loaded the user's setting yet
|
|
if (localeOverride === undefined) {
|
|
return;
|
|
}
|
|
setLanguageDialog(LanguageDialog.Selection);
|
|
}}
|
|
/>
|
|
{languageDialog === LanguageDialog.Selection && (
|
|
<Modal
|
|
i18n={i18n}
|
|
modalName="Preferences__LanguageModal"
|
|
moduleClassName="Preferences__LanguageModal"
|
|
padded={false}
|
|
onClose={closeLanguageDialog}
|
|
title={i18n('icu:Preferences__Language__ModalTitle')}
|
|
modalHeaderChildren={
|
|
<SearchInput
|
|
i18n={i18n}
|
|
value={languageSearchInput}
|
|
placeholder={i18n('icu:Preferences__Language__SearchLanguages')}
|
|
moduleClassName="Preferences__LanguageModal__SearchInput"
|
|
onChange={event => {
|
|
setLanguageSearchInput(event.currentTarget.value);
|
|
}}
|
|
/>
|
|
}
|
|
modalFooter={
|
|
<>
|
|
<Button
|
|
variant={ButtonVariant.Secondary}
|
|
onClick={closeLanguageDialog}
|
|
>
|
|
{i18n('icu:cancel')}
|
|
</Button>
|
|
<Button
|
|
variant={ButtonVariant.Primary}
|
|
disabled={selectedLanguageLocale === localeOverride}
|
|
onClick={() => {
|
|
setLanguageDialog(LanguageDialog.Confirmation);
|
|
}}
|
|
>
|
|
{i18n('icu:Preferences__LanguageModal__Set')}
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
{localeSearchResults.length === 0 && (
|
|
<div className="Preferences__LanguageModal__NoResults">
|
|
{i18n('icu:Preferences__Language__NoResults', {
|
|
searchTerm: languageSearchInput.trim(),
|
|
})}
|
|
</div>
|
|
)}
|
|
{localeSearchResults.map(option => {
|
|
const id = `${languageId}:${option.locale ?? 'system'}`;
|
|
const isSelected = option.locale === selectedLanguageLocale;
|
|
return (
|
|
<button
|
|
key={id}
|
|
type="button"
|
|
className="Preferences__LanguageModal__Item"
|
|
onClick={() => {
|
|
setSelectedLanguageLocale(option.locale);
|
|
}}
|
|
aria-pressed={isSelected}
|
|
>
|
|
<span className="Preferences__LanguageModal__Item__Inner">
|
|
<span className="Preferences__LanguageModal__Item__Label">
|
|
<span className="Preferences__LanguageModal__Item__Current">
|
|
{option.currentLocaleLabel}
|
|
</span>
|
|
{option.matchingLocaleLabel != null && (
|
|
<span
|
|
lang={option.locale ?? resolvedLocale}
|
|
className="Preferences__LanguageModal__Item__Matching"
|
|
>
|
|
{option.matchingLocaleLabel}
|
|
</span>
|
|
)}
|
|
</span>
|
|
{isSelected && (
|
|
<span className="Preferences__LanguageModal__Item__Check" />
|
|
)}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</Modal>
|
|
)}
|
|
{languageDialog === LanguageDialog.Confirmation && (
|
|
<ConfirmationDialog
|
|
dialogName="Preferences__Language"
|
|
i18n={i18n}
|
|
title={i18n('icu:Preferences__LanguageModal__Restart__Title')}
|
|
onCancel={closeLanguageDialog}
|
|
onClose={closeLanguageDialog}
|
|
cancelText={i18n('icu:cancel')}
|
|
actions={[
|
|
{
|
|
text: i18n('icu:Preferences__LanguageModal__Restart__Button'),
|
|
style: 'affirmative',
|
|
action: () => {
|
|
onLocaleChange(selectedLanguageLocale);
|
|
},
|
|
},
|
|
]}
|
|
>
|
|
{i18n('icu:Preferences__LanguageModal__Restart__Description')}
|
|
</ConfirmationDialog>
|
|
)}
|
|
<Control
|
|
icon
|
|
left={
|
|
<label htmlFor={themeSelectId}>
|
|
{i18n('icu:Preferences--theme')}
|
|
</label>
|
|
}
|
|
right={
|
|
<Select
|
|
id={themeSelectId}
|
|
disabled={themeSetting === undefined}
|
|
onChange={onThemeChange}
|
|
options={[
|
|
{
|
|
text: i18n('icu:themeSystem'),
|
|
value: 'system',
|
|
},
|
|
{
|
|
text: i18n('icu:themeLight'),
|
|
value: 'light',
|
|
},
|
|
{
|
|
text: i18n('icu:themeDark'),
|
|
value: 'dark',
|
|
},
|
|
]}
|
|
value={themeSetting}
|
|
/>
|
|
}
|
|
/>
|
|
<Control
|
|
icon
|
|
left={i18n('icu:showChatColorEditor')}
|
|
onClick={() => {
|
|
setPage(Page.ChatColor);
|
|
}}
|
|
right={
|
|
<div
|
|
className={`ConversationDetails__chat-color ConversationDetails__chat-color--${defaultConversationColor.color}`}
|
|
style={{
|
|
...getCustomColorStyle(
|
|
defaultConversationColor.customColorData?.value
|
|
),
|
|
}}
|
|
/>
|
|
}
|
|
/>
|
|
<Control
|
|
icon
|
|
left={
|
|
<label htmlFor={zoomSelectId}>
|
|
{i18n('icu:Preferences--zoom')}
|
|
</label>
|
|
}
|
|
right={
|
|
<Select
|
|
id={zoomSelectId}
|
|
disabled={zoomFactor === undefined}
|
|
onChange={onZoomSelectChange}
|
|
options={zoomFactor === undefined ? [] : zoomFactors}
|
|
value={zoomFactor}
|
|
/>
|
|
}
|
|
/>
|
|
</SettingsRow>
|
|
);
|
|
content = (
|
|
<PreferencesContent
|
|
contents={pageContents}
|
|
contentsRef={settingsPaneRef}
|
|
title={i18n('icu:Preferences__button--appearance')}
|
|
/>
|
|
);
|
|
} else if (page === Page.Chats) {
|
|
let spellCheckDirtyText: string | undefined;
|
|
if (
|
|
hasSpellCheck !== undefined &&
|
|
initialSpellCheckSetting !== hasSpellCheck
|
|
) {
|
|
spellCheckDirtyText = hasSpellCheck
|
|
? i18n('icu:spellCheckWillBeEnabled')
|
|
: i18n('icu:spellCheckWillBeDisabled');
|
|
}
|
|
|
|
const lastSyncDate = new Date(lastSyncTime || 0);
|
|
|
|
const pageContents = (
|
|
<>
|
|
<SettingsRow title={i18n('icu:Preferences__button--chats')}>
|
|
<Checkbox
|
|
checked={hasSpellCheck}
|
|
disabled={hasSpellCheck === undefined}
|
|
description={spellCheckDirtyText}
|
|
label={i18n('icu:spellCheckDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="spellcheck"
|
|
onChange={onSpellCheckChange}
|
|
/>
|
|
<Checkbox
|
|
checked={hasTextFormatting}
|
|
label={i18n('icu:textFormattingDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="textFormatting"
|
|
onChange={onTextFormattingChange}
|
|
/>
|
|
<Checkbox
|
|
checked={hasLinkPreviews}
|
|
description={i18n('icu:Preferences__link-previews--description')}
|
|
disabled
|
|
label={i18n('icu:Preferences__link-previews--title')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="linkPreviews"
|
|
onChange={noop}
|
|
/>
|
|
<Checkbox
|
|
checked={hasAutoConvertEmoji}
|
|
description={
|
|
<I18n
|
|
i18n={i18n}
|
|
id="icu:Preferences__auto-convert-emoji--description"
|
|
/>
|
|
}
|
|
label={i18n('icu:Preferences__auto-convert-emoji--title')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="autoConvertEmoji"
|
|
onChange={onAutoConvertEmojiChange}
|
|
/>
|
|
<SettingsRow>
|
|
<Control
|
|
left={i18n('icu:Preferences__EmojiSkinToneDefaultSetting__Label')}
|
|
right={
|
|
<FunSkinTonesList
|
|
i18n={i18n}
|
|
// Raised Hand
|
|
emoji={emojiParentKeyConstant('\u{270B}')}
|
|
skinTone={emojiSkinToneDefault}
|
|
onSelectSkinTone={onEmojiSkinToneDefaultChange}
|
|
/>
|
|
}
|
|
/>
|
|
</SettingsRow>
|
|
</SettingsRow>
|
|
{isChatFoldersEnabled() && (
|
|
<SettingsRow
|
|
title={i18n(
|
|
'icu:Preferences__ChatsPage__ChatFoldersSection__Title'
|
|
)}
|
|
>
|
|
<Control
|
|
left={
|
|
<>
|
|
<div>
|
|
{i18n(
|
|
'icu:Preferences__ChatsPage__ChatFoldersSection__AddChatFolderItem__Title'
|
|
)}
|
|
</div>
|
|
<div className="Preferences__description">
|
|
{i18n(
|
|
'icu:Preferences__ChatsPage__ChatFoldersSection__AddChatFolderItem__Description'
|
|
)}
|
|
</div>
|
|
</>
|
|
}
|
|
right={null}
|
|
onClick={() => setPage(Page.ChatFolders)}
|
|
/>
|
|
</SettingsRow>
|
|
)}
|
|
|
|
{isSyncSupported && (
|
|
<SettingsRow>
|
|
<Control
|
|
left={
|
|
<>
|
|
<div>{i18n('icu:sync')}</div>
|
|
<div className="Preferences__description">
|
|
{i18n('icu:syncExplanation')}{' '}
|
|
{i18n('icu:Preferences--lastSynced', {
|
|
date: lastSyncDate.toLocaleDateString(),
|
|
time: lastSyncDate.toLocaleTimeString(),
|
|
})}
|
|
</div>
|
|
{showSyncFailed && (
|
|
<div className="Preferences__description Preferences__description--error">
|
|
{i18n('icu:syncFailed')}
|
|
</div>
|
|
)}
|
|
</>
|
|
}
|
|
right={
|
|
<div className="Preferences__right-button">
|
|
<Button
|
|
aria-label={
|
|
nowSyncing ? i18n('icu:syncing') : i18n('icu:syncNow')
|
|
}
|
|
aria-live="polite"
|
|
disabled={nowSyncing}
|
|
onClick={async () => {
|
|
setShowSyncFailed(false);
|
|
setNowSyncing(true);
|
|
try {
|
|
await makeSyncRequest();
|
|
onLastSyncTimeChange(Date.now());
|
|
} catch (err) {
|
|
setShowSyncFailed(true);
|
|
} finally {
|
|
setNowSyncing(false);
|
|
}
|
|
}}
|
|
variant={ButtonVariant.SecondaryAffirmative}
|
|
>
|
|
{nowSyncing ? (
|
|
<Spinner svgSize="small" />
|
|
) : (
|
|
i18n('icu:syncNow')
|
|
)}
|
|
</Button>
|
|
</div>
|
|
}
|
|
/>
|
|
</SettingsRow>
|
|
)}
|
|
</>
|
|
);
|
|
content = (
|
|
<PreferencesContent
|
|
contents={pageContents}
|
|
contentsRef={settingsPaneRef}
|
|
title={i18n('icu:Preferences__button--chats')}
|
|
/>
|
|
);
|
|
} else if (page === Page.Calls) {
|
|
const pageContents = (
|
|
<>
|
|
<SettingsRow title={i18n('icu:calling')}>
|
|
<Checkbox
|
|
checked={hasIncomingCallNotifications}
|
|
label={i18n('icu:incomingCallNotificationDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="incomingCallNotification"
|
|
onChange={onIncomingCallNotificationsChange}
|
|
/>
|
|
<Checkbox
|
|
checked={hasCallRingtoneNotification}
|
|
label={i18n('icu:callRingtoneNotificationDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="callRingtoneNotification"
|
|
onChange={onCallRingtoneNotificationChange}
|
|
/>
|
|
</SettingsRow>
|
|
<SettingsRow title={i18n('icu:Preferences__devices')}>
|
|
<Control
|
|
left={
|
|
<>
|
|
<label className="Preferences__select-title" htmlFor="video">
|
|
{i18n('icu:callingDeviceSelection__label--video')}
|
|
</label>
|
|
<Select
|
|
ariaLabel={i18n('icu:callingDeviceSelection__label--video')}
|
|
disabled={!availableCameras.length}
|
|
moduleClassName="Preferences__select"
|
|
name="video"
|
|
onChange={onSelectedCameraChange}
|
|
options={
|
|
availableCameras.length
|
|
? availableCameras.map(device => ({
|
|
text: localizeDefault(i18n, device.label),
|
|
value: device.deviceId,
|
|
}))
|
|
: [
|
|
{
|
|
text: i18n(
|
|
'icu:callingDeviceSelection__select--no-device'
|
|
),
|
|
value: 'undefined',
|
|
},
|
|
]
|
|
}
|
|
value={selectedCamera}
|
|
/>
|
|
</>
|
|
}
|
|
right={<div />}
|
|
/>
|
|
<Control
|
|
left={
|
|
<>
|
|
<label
|
|
className="Preferences__select-title"
|
|
htmlFor="audio-input"
|
|
>
|
|
{i18n('icu:callingDeviceSelection__label--audio-input')}
|
|
</label>
|
|
<Select
|
|
ariaLabel={i18n(
|
|
'icu:callingDeviceSelection__label--audio-input'
|
|
)}
|
|
disabled={!availableMicrophones.length}
|
|
moduleClassName="Preferences__select"
|
|
name="audio-input"
|
|
onChange={onAudioInputSelectChange}
|
|
options={
|
|
availableMicrophones.length
|
|
? availableMicrophones.map(device => ({
|
|
text: localizeDefault(i18n, device.name),
|
|
value: device.index,
|
|
}))
|
|
: [
|
|
{
|
|
text: i18n(
|
|
'icu:callingDeviceSelection__select--no-device'
|
|
),
|
|
value: 'undefined',
|
|
},
|
|
]
|
|
}
|
|
value={selectedMicrophone?.index}
|
|
/>
|
|
</>
|
|
}
|
|
right={<div />}
|
|
/>
|
|
<Control
|
|
left={
|
|
<>
|
|
<label
|
|
className="Preferences__select-title"
|
|
htmlFor="audio-output"
|
|
>
|
|
{i18n('icu:callingDeviceSelection__label--audio-output')}
|
|
</label>
|
|
<Select
|
|
ariaLabel={i18n(
|
|
'icu:callingDeviceSelection__label--audio-output'
|
|
)}
|
|
disabled={!availableSpeakers.length}
|
|
moduleClassName="Preferences__select"
|
|
name="audio-output"
|
|
onChange={onAudioOutputSelectChange}
|
|
options={
|
|
availableSpeakers.length
|
|
? availableSpeakers.map(device => ({
|
|
text: localizeDefault(i18n, device.name),
|
|
value: device.index,
|
|
}))
|
|
: [
|
|
{
|
|
text: i18n(
|
|
'icu:callingDeviceSelection__select--no-device'
|
|
),
|
|
value: 'undefined',
|
|
},
|
|
]
|
|
}
|
|
value={selectedSpeaker?.index}
|
|
/>
|
|
</>
|
|
}
|
|
right={<div />}
|
|
/>
|
|
</SettingsRow>
|
|
<SettingsRow title={i18n('icu:Preferences--advanced')}>
|
|
<Checkbox
|
|
checked={hasRelayCalls}
|
|
description={i18n('icu:alwaysRelayCallsDetail')}
|
|
label={i18n('icu:alwaysRelayCallsDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="relayCalls"
|
|
onChange={onRelayCallsChange}
|
|
/>
|
|
</SettingsRow>
|
|
</>
|
|
);
|
|
content = (
|
|
<PreferencesContent
|
|
contents={pageContents}
|
|
contentsRef={settingsPaneRef}
|
|
title={i18n('icu:Preferences__button--calls')}
|
|
/>
|
|
);
|
|
} else if (page === Page.Notifications) {
|
|
const pageContents = (
|
|
<>
|
|
<SettingsRow>
|
|
<Checkbox
|
|
checked={hasNotifications}
|
|
label={i18n('icu:Preferences__enable-notifications')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="notifications"
|
|
onChange={onNotificationsChange}
|
|
/>
|
|
<Checkbox
|
|
checked={hasCallNotifications}
|
|
label={i18n('icu:callSystemNotificationDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="callSystemNotification"
|
|
onChange={onCallNotificationsChange}
|
|
/>
|
|
{isNotificationAttentionSupported && (
|
|
<Checkbox
|
|
checked={hasNotificationAttention}
|
|
label={i18n('icu:notificationDrawAttention')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="notificationDrawAttention"
|
|
onChange={onNotificationAttentionChange}
|
|
/>
|
|
)}
|
|
<Checkbox
|
|
checked={hasCountMutedConversations}
|
|
label={i18n('icu:countMutedConversationsDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="countMutedConversations"
|
|
onChange={onCountMutedConversationsChange}
|
|
/>
|
|
</SettingsRow>
|
|
<SettingsRow>
|
|
<Control
|
|
left={i18n('icu:Preferences--notification-content')}
|
|
right={
|
|
<Select
|
|
ariaLabel={i18n('icu:Preferences--notification-content')}
|
|
disabled={!hasNotifications}
|
|
onChange={onNotificationContentChange}
|
|
options={[
|
|
{
|
|
text: i18n('icu:nameAndMessage'),
|
|
value: 'message',
|
|
},
|
|
{
|
|
text: i18n('icu:nameOnly'),
|
|
value: 'name',
|
|
},
|
|
{
|
|
text: i18n('icu:noNameOrMessage'),
|
|
value: 'count',
|
|
},
|
|
]}
|
|
value={notificationContent}
|
|
/>
|
|
}
|
|
/>
|
|
</SettingsRow>
|
|
<SettingsRow>
|
|
<Checkbox
|
|
checked={hasAudioNotifications}
|
|
label={i18n('icu:audioNotificationDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="audioNotification"
|
|
onChange={onAudioNotificationsChange}
|
|
/>
|
|
<Checkbox
|
|
checked={hasMessageAudio}
|
|
description={i18n('icu:Preferences__message-audio-description')}
|
|
label={i18n('icu:Preferences__message-audio-title')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="messageAudio"
|
|
onChange={onMessageAudioChange}
|
|
/>
|
|
</SettingsRow>
|
|
</>
|
|
);
|
|
content = (
|
|
<PreferencesContent
|
|
contents={pageContents}
|
|
contentsRef={settingsPaneRef}
|
|
title={i18n('icu:Preferences__button--notifications')}
|
|
/>
|
|
);
|
|
} else if (page === Page.Privacy) {
|
|
const isCustomDisappearingMessageValue =
|
|
!DEFAULT_DURATIONS_SET.has(universalExpireTimer);
|
|
const pageContents = (
|
|
<>
|
|
<SettingsRow>
|
|
<FlowingControl>
|
|
<div
|
|
className={classNames(
|
|
'Preferences__pnp',
|
|
'Preferences__two-thirds-flow'
|
|
)}
|
|
>
|
|
<h3>{i18n('icu:Preferences__pnp__row--title')}</h3>
|
|
<div className="Preferences__description">
|
|
{i18n('icu:Preferences__pnp__row--body')}
|
|
</div>
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
'Preferences__pnp',
|
|
'Preferences__flow-button',
|
|
'Preferences__one-third-flow',
|
|
'Preferences__one-third-flow--align-right'
|
|
)}
|
|
>
|
|
<Button
|
|
onClick={() => setPage(Page.PNP)}
|
|
variant={ButtonVariant.Secondary}
|
|
>
|
|
{i18n('icu:Preferences__pnp__row--button')}
|
|
</Button>
|
|
</div>
|
|
</FlowingControl>
|
|
</SettingsRow>
|
|
<SettingsRow>
|
|
<Control
|
|
left={i18n('icu:Preferences--blocked')}
|
|
right={i18n('icu:Preferences--blocked-count', {
|
|
num: blockedCount,
|
|
})}
|
|
/>
|
|
</SettingsRow>
|
|
<SettingsRow title={i18n('icu:Preferences--messaging')}>
|
|
<Checkbox
|
|
checked={hasReadReceipts}
|
|
disabled
|
|
label={i18n('icu:Preferences--read-receipts')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="readReceipts"
|
|
onChange={noop}
|
|
/>
|
|
<Checkbox
|
|
checked={hasTypingIndicators}
|
|
disabled
|
|
label={i18n('icu:Preferences--typing-indicators')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="typingIndicators"
|
|
onChange={noop}
|
|
/>
|
|
<div className="Preferences__padding">
|
|
<div className="Preferences__description">
|
|
{i18n('icu:Preferences__privacy--description')}
|
|
</div>
|
|
</div>
|
|
</SettingsRow>
|
|
{showDisappearingTimerDialog && (
|
|
<DisappearingTimeDialog
|
|
i18n={i18n}
|
|
initialValue={universalExpireTimer}
|
|
onClose={() => setShowDisappearingTimerDialog(false)}
|
|
onSubmit={onUniversalExpireTimerChange}
|
|
/>
|
|
)}
|
|
<SettingsRow title={i18n('icu:disappearingMessages')}>
|
|
<FlowingControl>
|
|
<div className="Preferences__two-thirds-flow">
|
|
<div>
|
|
{i18n('icu:settings__DisappearingMessages__timer__label')}
|
|
</div>
|
|
<div className="Preferences__description">
|
|
{i18n('icu:settings__DisappearingMessages__footer')}
|
|
</div>
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
'Preferences__flow-button',
|
|
'Preferences__one-third-flow',
|
|
'Preferences__one-third-flow--align-right'
|
|
)}
|
|
>
|
|
<Select
|
|
ariaLabel={i18n(
|
|
'icu:settings__DisappearingMessages__timer__label'
|
|
)}
|
|
onChange={value => {
|
|
if (
|
|
value === String(universalExpireTimer) ||
|
|
value === '-1'
|
|
) {
|
|
setShowDisappearingTimerDialog(true);
|
|
return;
|
|
}
|
|
|
|
onUniversalExpireTimerChange(parseInt(value, 10));
|
|
}}
|
|
options={DEFAULT_DURATIONS_IN_SECONDS.map(seconds => {
|
|
const text = formatExpirationTimer(i18n, seconds, {
|
|
capitalizeOff: true,
|
|
});
|
|
return {
|
|
value: seconds,
|
|
text,
|
|
};
|
|
}).concat([
|
|
{
|
|
value: isCustomDisappearingMessageValue
|
|
? universalExpireTimer
|
|
: DurationInSeconds.fromSeconds(-1),
|
|
text: isCustomDisappearingMessageValue
|
|
? formatExpirationTimer(i18n, universalExpireTimer)
|
|
: i18n('icu:selectedCustomDisappearingTimeOption'),
|
|
},
|
|
])}
|
|
value={universalExpireTimer}
|
|
/>
|
|
</div>
|
|
</FlowingControl>
|
|
</SettingsRow>
|
|
{isContentProtectionSupported && (
|
|
<SettingsRow title={i18n('icu:Preferences__Privacy__Application')}>
|
|
<Checkbox
|
|
checked={hasContentProtection}
|
|
disabled={hasContentProtection === undefined}
|
|
description={i18n(
|
|
'icu:Preferences__content-protection--description'
|
|
)}
|
|
label={i18n('icu:Preferences__content-protection--label')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="contentProtection"
|
|
onChange={handleContentProtectionChange}
|
|
/>
|
|
</SettingsRow>
|
|
)}
|
|
{confirmContentProtection ? (
|
|
<ConfirmationDialog
|
|
dialogName="Preference.confirmContentProtection"
|
|
actions={[
|
|
{
|
|
action: () => onContentProtectionChange(false),
|
|
style: 'negative',
|
|
text: i18n(
|
|
'icu:Preferences__content-protection__modal--disable'
|
|
),
|
|
},
|
|
]}
|
|
i18n={i18n}
|
|
onClose={() => {
|
|
setConfirmContentProtection(false);
|
|
}}
|
|
title={i18n('icu:Preferences__content-protection__modal--title')}
|
|
>
|
|
{i18n('icu:Preferences__content-protection__modal--body')}
|
|
</ConfirmationDialog>
|
|
) : null}
|
|
<SettingsRow title={i18n('icu:Stories__title')}>
|
|
<FlowingControl>
|
|
<div className="Preferences__two-thirds-flow">
|
|
<label htmlFor={storiesId}>
|
|
<div>{i18n('icu:Stories__settings-toggle--title')}</div>
|
|
<div className="Preferences__description">
|
|
{i18n('icu:Stories__settings-toggle--description')}
|
|
</div>
|
|
</label>
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
'Preferences__flow-button',
|
|
'Preferences__one-third-flow',
|
|
'Preferences__one-third-flow--align-right'
|
|
)}
|
|
>
|
|
{hasStoriesDisabled ? (
|
|
<Button
|
|
onClick={() => onHasStoriesDisabledChanged(false)}
|
|
variant={ButtonVariant.Secondary}
|
|
>
|
|
{i18n('icu:Preferences__turn-stories-on')}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
className="Preferences__stories-off"
|
|
onClick={() => setConfirmStoriesOff(true)}
|
|
variant={ButtonVariant.SecondaryDestructive}
|
|
>
|
|
{i18n('icu:Preferences__turn-stories-off')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</FlowingControl>
|
|
</SettingsRow>
|
|
<SettingsRow>
|
|
<FlowingControl>
|
|
<div
|
|
className={classNames(
|
|
'Preferences__pnp',
|
|
'Preferences__two-thirds-flow'
|
|
)}
|
|
>
|
|
<div>{i18n('icu:clearDataHeader')}</div>
|
|
<div className="Preferences__description">
|
|
{i18n('icu:clearDataExplanation')}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className={classNames(
|
|
'Preferences__pnp',
|
|
'Preferences__flow-button',
|
|
'Preferences__one-third-flow',
|
|
'Preferences__one-third-flow--align-right'
|
|
)}
|
|
>
|
|
<Button
|
|
onClick={() => setConfirmDelete(true)}
|
|
variant={ButtonVariant.SecondaryDestructive}
|
|
>
|
|
{i18n('icu:clearDataButton')}
|
|
</Button>
|
|
</div>
|
|
</FlowingControl>
|
|
</SettingsRow>
|
|
{confirmDelete ? (
|
|
<ConfirmationDialog
|
|
dialogName="Preference.deleteAllData"
|
|
actions={[
|
|
{
|
|
action: doDeleteAllData,
|
|
style: 'negative',
|
|
text: i18n('icu:clearDataButton'),
|
|
},
|
|
]}
|
|
i18n={i18n}
|
|
onClose={() => {
|
|
setConfirmDelete(false);
|
|
}}
|
|
title={i18n('icu:deleteAllDataHeader')}
|
|
>
|
|
{i18n('icu:deleteAllDataBody')}
|
|
</ConfirmationDialog>
|
|
) : null}
|
|
{confirmStoriesOff ? (
|
|
<ConfirmationDialog
|
|
dialogName="Preference.turnStoriesOff"
|
|
actions={[
|
|
{
|
|
action: () => onHasStoriesDisabledChanged(true),
|
|
style: 'negative',
|
|
text: i18n('icu:Preferences__turn-stories-off--action'),
|
|
},
|
|
]}
|
|
i18n={i18n}
|
|
onClose={() => {
|
|
setConfirmStoriesOff(false);
|
|
}}
|
|
>
|
|
{i18n('icu:Preferences__turn-stories-off--body')}
|
|
</ConfirmationDialog>
|
|
) : null}
|
|
</>
|
|
);
|
|
content = (
|
|
<PreferencesContent
|
|
contents={pageContents}
|
|
contentsRef={settingsPaneRef}
|
|
title={i18n('icu:Preferences__button--privacy')}
|
|
/>
|
|
);
|
|
} else if (page === Page.DataUsage) {
|
|
const pageContents = (
|
|
<>
|
|
<SettingsRow title={i18n('icu:Preferences__media-auto-download')}>
|
|
<Checkbox
|
|
checked={autoDownloadAttachment.photos !== false}
|
|
label={i18n('icu:Preferences__media-auto-download__photos')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="autoLaunch"
|
|
onChange={(newValue: boolean) =>
|
|
onAutoDownloadAttachmentChange({
|
|
...autoDownloadAttachment,
|
|
photos: newValue,
|
|
})
|
|
}
|
|
/>
|
|
<Checkbox
|
|
checked={autoDownloadAttachment.videos !== false}
|
|
label={i18n('icu:Preferences__media-auto-download__videos')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="autoLaunch"
|
|
onChange={(newValue: boolean) =>
|
|
onAutoDownloadAttachmentChange({
|
|
...autoDownloadAttachment,
|
|
videos: newValue,
|
|
})
|
|
}
|
|
/>
|
|
<Checkbox
|
|
checked={autoDownloadAttachment.audio !== false}
|
|
label={i18n('icu:Preferences__media-auto-download__audio')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="autoLaunch"
|
|
onChange={(newValue: boolean) =>
|
|
onAutoDownloadAttachmentChange({
|
|
...autoDownloadAttachment,
|
|
audio: newValue,
|
|
})
|
|
}
|
|
/>
|
|
<Checkbox
|
|
checked={autoDownloadAttachment.documents !== false}
|
|
label={i18n('icu:Preferences__media-auto-download__documents')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="autoLaunch"
|
|
onChange={(newValue: boolean) =>
|
|
onAutoDownloadAttachmentChange({
|
|
...autoDownloadAttachment,
|
|
documents: newValue,
|
|
})
|
|
}
|
|
/>
|
|
<div className="Preferences__padding">
|
|
<div
|
|
className={classNames(
|
|
'Preferences__description',
|
|
'Preferences__description--medium'
|
|
)}
|
|
>
|
|
{i18n('icu:Preferences__media-auto-download__description')}
|
|
</div>
|
|
</div>
|
|
</SettingsRow>
|
|
<SettingsRow>
|
|
<FlowingControl>
|
|
<div className="Preferences__two-thirds-flow">
|
|
<div className="Preferences__option-name">
|
|
{i18n('icu:Preferences__sent-media-quality')}
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
'Preferences__description',
|
|
'Preferences__description--medium'
|
|
)}
|
|
>
|
|
{i18n('icu:Preferences__sent-media-quality__description')}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className={classNames(
|
|
'Preferences__flow-button',
|
|
'Preferences__one-third-flow',
|
|
'Preferences__one-third-flow--align-right'
|
|
)}
|
|
>
|
|
<Select
|
|
onChange={onSentMediaQualityChange}
|
|
options={[
|
|
{
|
|
text: i18n('icu:sentMediaQualityStandard'),
|
|
value: 'standard',
|
|
},
|
|
{
|
|
text: i18n('icu:sentMediaQualityHigh'),
|
|
value: 'high',
|
|
},
|
|
]}
|
|
value={sentMediaQualitySetting}
|
|
/>
|
|
</div>
|
|
</FlowingControl>
|
|
</SettingsRow>
|
|
</>
|
|
);
|
|
content = (
|
|
<PreferencesContent
|
|
contents={pageContents}
|
|
contentsRef={settingsPaneRef}
|
|
title={i18n('icu:Preferences__button--data-usage')}
|
|
/>
|
|
);
|
|
} else if (page === Page.ChatColor) {
|
|
const backButton = (
|
|
<button
|
|
aria-label={i18n('icu:goBack')}
|
|
className="Preferences__back-icon"
|
|
onClick={() => setPage(Page.Appearance)}
|
|
type="button"
|
|
/>
|
|
);
|
|
const pageContents = (
|
|
<ChatColorPicker
|
|
customColors={customColors}
|
|
getConversationsWithCustomColor={getConversationsWithCustomColor}
|
|
i18n={i18n}
|
|
isGlobal
|
|
selectedColor={defaultConversationColor.color}
|
|
selectedCustomColor={defaultConversationColor.customColorData || {}}
|
|
// actions
|
|
addCustomColor={addCustomColor}
|
|
colorSelected={noop}
|
|
editCustomColor={editCustomColor}
|
|
removeCustomColor={removeCustomColor}
|
|
removeCustomColorOnConversations={removeCustomColorOnConversations}
|
|
resetAllChatColors={resetAllChatColors}
|
|
resetDefaultChatColor={resetDefaultChatColor}
|
|
setGlobalDefaultConversationColor={setGlobalDefaultConversationColor}
|
|
/>
|
|
);
|
|
content = (
|
|
<PreferencesContent
|
|
backButton={backButton}
|
|
contents={pageContents}
|
|
contentsRef={settingsPaneRef}
|
|
title={i18n('icu:ChatColorPicker__menu-title')}
|
|
/>
|
|
);
|
|
} else if (page === Page.ChatFolders) {
|
|
content = (
|
|
<ChatFoldersPage
|
|
i18n={i18n}
|
|
settingsPaneRef={settingsPaneRef}
|
|
onBack={() => setPage(Page.Chats)}
|
|
onOpenEditChatFoldersPage={handleOpenEditChatFoldersPage}
|
|
chatFolders={chatFolders}
|
|
onCreateChatFolder={handleCreateChatFolder}
|
|
/>
|
|
);
|
|
} else if (page === Page.EditChatFolder) {
|
|
let initChatFolderParam: ChatFolderParams;
|
|
if (editChatFolderPageId != null) {
|
|
const found = chatFolders.find(chatFolder => {
|
|
return chatFolder.id === editChatFolderPageId;
|
|
});
|
|
strictAssert(found, 'Missing chat folder');
|
|
initChatFolderParam = found;
|
|
} else {
|
|
initChatFolderParam = CHAT_FOLDER_DEFAULTS;
|
|
}
|
|
content = (
|
|
<EditChatFoldersPage
|
|
i18n={i18n}
|
|
settingsPaneRef={settingsPaneRef}
|
|
onBack={handleCloseEditChatFoldersPage}
|
|
conversations={conversations}
|
|
getPreferredBadge={getPreferredBadge}
|
|
theme={theme}
|
|
existingChatFolderId={editChatFolderPageId}
|
|
initChatFolderParams={initChatFolderParam}
|
|
conversationSelector={conversationSelector}
|
|
onCreateChatFolder={handleCreateChatFolder}
|
|
onUpdateChatFolder={handleUpdateChatFolder}
|
|
onDeleteChatFolder={handleDeleteChatFolder}
|
|
/>
|
|
);
|
|
} else if (page === Page.PNP) {
|
|
let sharingDescription: string;
|
|
|
|
if (whoCanSeeMe === PhoneNumberSharingMode.Everybody) {
|
|
sharingDescription = i18n(
|
|
'icu:Preferences__pnp__sharing--description--everyone'
|
|
);
|
|
} else if (whoCanFindMe === PhoneNumberDiscoverability.Discoverable) {
|
|
sharingDescription = i18n(
|
|
'icu:Preferences__pnp__sharing--description--nobody'
|
|
);
|
|
} else {
|
|
sharingDescription = i18n(
|
|
'icu:Preferences__pnp__sharing--description--nobody--not-discoverable'
|
|
);
|
|
}
|
|
|
|
const backButton = (
|
|
<button
|
|
aria-label={i18n('icu:goBack')}
|
|
className="Preferences__back-icon"
|
|
onClick={() => setPage(Page.Privacy)}
|
|
type="button"
|
|
/>
|
|
);
|
|
const pageContents = (
|
|
<>
|
|
<SettingsRow
|
|
title={i18n('icu:Preferences__pnp__sharing--title')}
|
|
className={classNames('Preferences__settings-row--pnp-sharing', {
|
|
'Preferences__settings-row--pnp-sharing--nobody':
|
|
whoCanSeeMe === PhoneNumberSharingMode.Nobody,
|
|
})}
|
|
>
|
|
<SettingsRadio
|
|
onChange={onWhoCanSeeMeChange}
|
|
options={[
|
|
{
|
|
text: i18n('icu:Preferences__pnp__sharing__everyone'),
|
|
value: PhoneNumberSharingMode.Everybody,
|
|
},
|
|
{
|
|
text: i18n('icu:Preferences__pnp__sharing__nobody'),
|
|
value: PhoneNumberSharingMode.Nobody,
|
|
},
|
|
]}
|
|
value={whoCanSeeMe}
|
|
/>
|
|
<div className="Preferences__padding">
|
|
<div className="Preferences__description">{sharingDescription}</div>
|
|
</div>
|
|
</SettingsRow>
|
|
|
|
<SettingsRow
|
|
title={i18n('icu:Preferences__pnp__discoverability--title')}
|
|
>
|
|
<SettingsRadio
|
|
onChange={value => {
|
|
if (value === PhoneNumberDiscoverability.NotDiscoverable) {
|
|
setConfirmPnpNoDiscoverable(true);
|
|
} else {
|
|
onWhoCanFindMeChange(value);
|
|
}
|
|
}}
|
|
options={[
|
|
{
|
|
text: i18n('icu:Preferences__pnp__discoverability__everyone'),
|
|
value: PhoneNumberDiscoverability.Discoverable,
|
|
},
|
|
{
|
|
text: i18n('icu:Preferences__pnp__discoverability__nobody'),
|
|
value: PhoneNumberDiscoverability.NotDiscoverable,
|
|
readOnly: whoCanSeeMe === PhoneNumberSharingMode.Everybody,
|
|
onClick:
|
|
whoCanSeeMe === PhoneNumberSharingMode.Everybody
|
|
? () =>
|
|
showToast({ toastType: ToastType.WhoCanFindMeReadOnly })
|
|
: noop,
|
|
},
|
|
]}
|
|
value={whoCanFindMe}
|
|
/>
|
|
<div className="Preferences__padding">
|
|
<div className="Preferences__description">
|
|
{whoCanFindMe === PhoneNumberDiscoverability.Discoverable
|
|
? i18n(
|
|
'icu:Preferences__pnp__discoverability--description--everyone'
|
|
)
|
|
: i18n(
|
|
'icu:Preferences__pnp__discoverability--description--nobody'
|
|
)}
|
|
</div>
|
|
</div>
|
|
</SettingsRow>
|
|
{confirmPnpNotDiscoverable && (
|
|
<ConfirmationDialog
|
|
i18n={i18n}
|
|
title={i18n(
|
|
'icu:Preferences__pnp__discoverability__nobody__confirmModal__title'
|
|
)}
|
|
dialogName="Preference.turnPnpDiscoveryOff"
|
|
onClose={() => {
|
|
setConfirmPnpNoDiscoverable(false);
|
|
}}
|
|
actions={[
|
|
{
|
|
action: () =>
|
|
onWhoCanFindMeChange(
|
|
PhoneNumberDiscoverability.NotDiscoverable
|
|
),
|
|
style: 'affirmative',
|
|
text: i18n('icu:ok'),
|
|
},
|
|
]}
|
|
>
|
|
{i18n(
|
|
'icu:Preferences__pnp__discoverability__nobody__confirmModal__description',
|
|
{
|
|
// This is a rare instance where we want to interpolate the exact
|
|
// text of the string into quotes in the translation as an
|
|
// explanation.
|
|
settingTitle: i18n(
|
|
'icu:Preferences__pnp__discoverability--title'
|
|
),
|
|
nobodyLabel: i18n(
|
|
'icu:Preferences__pnp__discoverability__nobody'
|
|
),
|
|
}
|
|
)}
|
|
</ConfirmationDialog>
|
|
)}
|
|
</>
|
|
);
|
|
content = (
|
|
<PreferencesContent
|
|
backButton={backButton}
|
|
contents={pageContents}
|
|
contentsRef={settingsPaneRef}
|
|
title={i18n('icu:Preferences__pnp--page-title')}
|
|
/>
|
|
);
|
|
} else if (isBackupPage(page)) {
|
|
let pageTitle: string | undefined;
|
|
if (page === Page.Backups || page === Page.BackupsDetails) {
|
|
pageTitle = i18n('icu:Preferences__button--backups');
|
|
} else if (page === Page.LocalBackups) {
|
|
pageTitle = i18n('icu:Preferences__local-backups');
|
|
}
|
|
// Local backups setup page titles intentionally left blank
|
|
|
|
let backPage: PreferencesBackupPage | undefined;
|
|
if (page === Page.LocalBackupsKeyReference) {
|
|
backPage = Page.LocalBackups;
|
|
} else if (page !== Page.Backups) {
|
|
backPage = Page.Backups;
|
|
}
|
|
let backButton: JSX.Element | undefined;
|
|
if (backPage) {
|
|
backButton = (
|
|
<button
|
|
aria-label={i18n('icu:goBack')}
|
|
className="Preferences__back-icon"
|
|
onClick={() => setPage(backPage)}
|
|
type="button"
|
|
/>
|
|
);
|
|
}
|
|
const pageContents = (
|
|
<PreferencesBackups
|
|
accountEntropyPool={accountEntropyPool}
|
|
backupKeyViewed={backupKeyViewed}
|
|
backupSubscriptionStatus={backupSubscriptionStatus}
|
|
cloudBackupStatus={cloudBackupStatus}
|
|
i18n={i18n}
|
|
locale={resolvedLocale}
|
|
localBackupFolder={localBackupFolder}
|
|
onBackupKeyViewedChange={onBackupKeyViewedChange}
|
|
pickLocalBackupFolder={pickLocalBackupFolder}
|
|
page={page}
|
|
promptOSAuth={promptOSAuth}
|
|
refreshCloudBackupStatus={refreshCloudBackupStatus}
|
|
refreshBackupSubscriptionStatus={refreshBackupSubscriptionStatus}
|
|
setPage={setPage}
|
|
showToast={showToast}
|
|
/>
|
|
);
|
|
content = (
|
|
<PreferencesContent
|
|
backButton={backButton}
|
|
contents={pageContents}
|
|
contentsRef={settingsPaneRef}
|
|
title={pageTitle}
|
|
/>
|
|
);
|
|
} else if (page === Page.Internal) {
|
|
content = (
|
|
<PreferencesContent
|
|
contents={
|
|
<PreferencesInternal
|
|
i18n={i18n}
|
|
exportLocalBackup={exportLocalBackup}
|
|
validateBackup={validateBackup}
|
|
getMessageCountBySchemaVersion={getMessageCountBySchemaVersion}
|
|
getMessageSampleForSchemaVersion={getMessageSampleForSchemaVersion}
|
|
/>
|
|
}
|
|
contentsRef={settingsPaneRef}
|
|
title={i18n('icu:Preferences__button--internal')}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<FunEmojiLocalizationProvider i18n={i18n}>
|
|
<div className="module-title-bar-drag-area" />
|
|
<div className="Preferences">
|
|
<NavSidebar
|
|
title={i18n('icu:Preferences--header')}
|
|
i18n={i18n}
|
|
otherTabsUnreadStats={otherTabsUnreadStats}
|
|
hasFailedStorySends={hasFailedStorySends}
|
|
hasPendingUpdate={false}
|
|
navTabsCollapsed={navTabsCollapsed}
|
|
onToggleNavTabsCollapse={onToggleNavTabsCollapse}
|
|
preferredLeftPaneWidth={preferredWidthFromStorage}
|
|
requiresFullWidth
|
|
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
|
renderToastManager={renderToastManager}
|
|
>
|
|
<div className="Preferences__page-selector">
|
|
{maybeUpdateDialog ? (
|
|
<div className="Preferences__dialog-container">
|
|
<div className="module-left-pane__dialogs">
|
|
{maybeUpdateDialog}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
<div className="Preferences__scroll-area">
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
'Preferences__profile-chip': true,
|
|
'Preferences__profile-chip--selected': page === Page.Profile,
|
|
})}
|
|
onClick={() => setPage(Page.Profile)}
|
|
>
|
|
<div className="Preferences__profile-chip__avatar">
|
|
<Avatar
|
|
avatarUrl={me.avatarUrl}
|
|
badge={badge}
|
|
className="module-main-header__avatar"
|
|
color={me.color}
|
|
conversationType="direct"
|
|
i18n={i18n}
|
|
phoneNumber={me.phoneNumber}
|
|
profileName={me.profileName}
|
|
theme={theme}
|
|
title={me.title}
|
|
// `sharedGroupNames` makes no sense for yourself, but
|
|
// `<Avatar>` needs it to determine blurring.
|
|
sharedGroupNames={[]}
|
|
size={AvatarSize.FORTY_EIGHT}
|
|
/>
|
|
</div>
|
|
<div className="Preferences__profile-chip__text-container">
|
|
<div className="Preferences__profile-chip__name">
|
|
{me.title}
|
|
</div>
|
|
<div className="Preferences__profile-chip__number">
|
|
{me.phoneNumber}
|
|
</div>
|
|
{me.username && (
|
|
<div className="Preferences__profile-chip__username">
|
|
{me.username}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="Preferences__profile-chip__qr-icon-container">
|
|
<div className="Preferences__profile-chip__qr-icon" />
|
|
</div>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
Preferences__button: true,
|
|
'Preferences__button--general': true,
|
|
'Preferences__button--selected': page === Page.General,
|
|
})}
|
|
onClick={() => setPage(Page.General)}
|
|
>
|
|
{i18n('icu:Preferences__button--general')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
Preferences__button: true,
|
|
'Preferences__button--appearance': true,
|
|
'Preferences__button--selected':
|
|
page === Page.Appearance || page === Page.ChatColor,
|
|
})}
|
|
onClick={() => setPage(Page.Appearance)}
|
|
>
|
|
{i18n('icu:Preferences__button--appearance')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
Preferences__button: true,
|
|
'Preferences__button--chats': true,
|
|
'Preferences__button--selected': page === Page.Chats,
|
|
})}
|
|
onClick={() => setPage(Page.Chats)}
|
|
>
|
|
{i18n('icu:Preferences__button--chats')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
Preferences__button: true,
|
|
'Preferences__button--calls': true,
|
|
'Preferences__button--selected': page === Page.Calls,
|
|
})}
|
|
onClick={() => setPage(Page.Calls)}
|
|
>
|
|
{i18n('icu:Preferences__button--calls')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
Preferences__button: true,
|
|
'Preferences__button--notifications': true,
|
|
'Preferences__button--selected': page === Page.Notifications,
|
|
})}
|
|
onClick={() => setPage(Page.Notifications)}
|
|
>
|
|
{i18n('icu:Preferences__button--notifications')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
Preferences__button: true,
|
|
'Preferences__button--privacy': true,
|
|
'Preferences__button--selected':
|
|
page === Page.Privacy || page === Page.PNP,
|
|
})}
|
|
onClick={() => setPage(Page.Privacy)}
|
|
>
|
|
{i18n('icu:Preferences__button--privacy')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
Preferences__button: true,
|
|
'Preferences__button--data-usage': true,
|
|
'Preferences__button--selected': page === Page.DataUsage,
|
|
})}
|
|
onClick={() => setPage(Page.DataUsage)}
|
|
>
|
|
{i18n('icu:Preferences__button--data-usage')}
|
|
</button>
|
|
{shouldShowBackupsPage ? (
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
Preferences__button: true,
|
|
'Preferences__button--backups': true,
|
|
'Preferences__button--selected': isBackupPage(page),
|
|
})}
|
|
onClick={() => setPage(Page.Backups)}
|
|
>
|
|
{i18n('icu:Preferences__button--backups')}
|
|
</button>
|
|
) : null}
|
|
{donationsFeatureEnabled && (
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
Preferences__button: true,
|
|
'Preferences__button--appearance': true,
|
|
'Preferences__button--selected': page === Page.Donations,
|
|
})}
|
|
onClick={() => setPage(Page.Donations)}
|
|
>
|
|
{i18n('icu:Preferences__button--donate')}
|
|
</button>
|
|
)}
|
|
{isInternalUser ? (
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
Preferences__button: true,
|
|
'Preferences__button--internal': true,
|
|
'Preferences__button--selected': page === Page.Internal,
|
|
})}
|
|
onClick={() => setPage(Page.Internal)}
|
|
>
|
|
{i18n('icu:Preferences__button--internal')}
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</NavSidebar>
|
|
{content}
|
|
</div>
|
|
</FunEmojiLocalizationProvider>
|
|
);
|
|
}
|
|
|
|
function localizeDefault(i18n: LocalizerType, deviceLabel: string): string {
|
|
return deviceLabel.toLowerCase().startsWith('default')
|
|
? deviceLabel.replace(
|
|
/default/i,
|
|
i18n('icu:callingDeviceSelection__select--default')
|
|
)
|
|
: deviceLabel;
|
|
}
|
|
|
|
export function PreferencesContent({
|
|
backButton,
|
|
contents,
|
|
contentsRef,
|
|
title,
|
|
actions,
|
|
}: {
|
|
backButton?: JSX.Element | undefined;
|
|
contents: JSX.Element | undefined;
|
|
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
|
title: string | undefined;
|
|
actions?: ReactNode;
|
|
}): JSX.Element {
|
|
return (
|
|
<div className="Preferences__content">
|
|
<div className="Preferences__title">
|
|
{backButton}
|
|
<div className="Preferences__title--header">{title}</div>
|
|
</div>
|
|
<div className="Preferences__page">
|
|
<div className="Preferences__settings-pane-spacer" />
|
|
<div className="Preferences__settings-pane" ref={contentsRef}>
|
|
{contents}
|
|
</div>
|
|
<div className="Preferences__settings-pane-spacer" />
|
|
</div>
|
|
{actions && <div className="Preferences__actions">{actions}</div>}
|
|
</div>
|
|
);
|
|
}
|