1260 lines
41 KiB
TypeScript
1260 lines
41 KiB
TypeScript
// Copyright 2021 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import type { ReactNode } from 'react';
|
|
import React, { useEffect, useState, useCallback } from 'react';
|
|
import { noop } from 'lodash';
|
|
import classNames from 'classnames';
|
|
import type { AudioDevice } from '@signalapp/ringrtc';
|
|
|
|
import type { MediaDeviceSettings } from '../types/Calling';
|
|
import type {
|
|
NotificationSettingType,
|
|
SentMediaQualitySettingType,
|
|
ZoomFactorType,
|
|
} from '../types/Storage.d';
|
|
import type { ThemeSettingType } from '../types/StorageUIKeys';
|
|
import { Button, ButtonVariant } from './Button';
|
|
import { ChatColorPicker } from './ChatColorPicker';
|
|
import { Checkbox } from './Checkbox';
|
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
|
import type { ConversationType } from '../state/ducks/conversations';
|
|
import type {
|
|
ConversationColorType,
|
|
CustomColorType,
|
|
DefaultConversationColorType,
|
|
} from '../types/Colors';
|
|
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
|
|
import type {
|
|
LocalizerType,
|
|
SentMediaQualityType,
|
|
ThemeType,
|
|
} from '../types/Util';
|
|
import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
|
|
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
|
|
import { Select } from './Select';
|
|
import { Spinner } from './Spinner';
|
|
import { TitleBarContainer } from './TitleBarContainer';
|
|
import type { ExecuteMenuRoleType } from './TitleBarContainer';
|
|
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 { useEscapeHandling } from '../hooks/useEscapeHandling';
|
|
import { useUniqueId } from '../hooks/useUniqueId';
|
|
import { useTheme } from '../hooks/useTheme';
|
|
|
|
type CheckboxChangeHandlerType = (value: boolean) => unknown;
|
|
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
|
|
|
|
export type PropsDataType = {
|
|
// Settings
|
|
blockedCount: number;
|
|
customColors: Record<string, CustomColorType>;
|
|
defaultConversationColor: DefaultConversationColorType;
|
|
deviceName?: string;
|
|
hasAudioNotifications?: boolean;
|
|
hasAutoDownloadUpdate: boolean;
|
|
hasAutoLaunch: boolean;
|
|
hasCallNotifications: boolean;
|
|
hasCallRingtoneNotification: boolean;
|
|
hasCountMutedConversations: boolean;
|
|
hasHideMenuBar?: boolean;
|
|
hasIncomingCallNotifications: boolean;
|
|
hasLinkPreviews: boolean;
|
|
hasMediaCameraPermissions: boolean;
|
|
hasMediaPermissions: boolean;
|
|
hasMinimizeToAndStartInSystemTray: boolean;
|
|
hasMinimizeToSystemTray: boolean;
|
|
hasNotificationAttention: boolean;
|
|
hasNotifications: boolean;
|
|
hasReadReceipts: boolean;
|
|
hasRelayCalls?: boolean;
|
|
hasSpellCheck: boolean;
|
|
hasStoriesDisabled: boolean;
|
|
hasTypingIndicators: boolean;
|
|
lastSyncTime?: number;
|
|
notificationContent: NotificationSettingType;
|
|
selectedCamera?: string;
|
|
selectedMicrophone?: AudioDevice;
|
|
selectedSpeaker?: AudioDevice;
|
|
sentMediaQualitySetting: SentMediaQualitySettingType;
|
|
themeSetting: ThemeSettingType;
|
|
universalExpireTimer: DurationInSeconds;
|
|
whoCanFindMe: PhoneNumberDiscoverability;
|
|
whoCanSeeMe: PhoneNumberSharingMode;
|
|
zoomFactor: ZoomFactorType;
|
|
|
|
// Other props
|
|
hasCustomTitleBar: boolean;
|
|
initialSpellCheckSetting: boolean;
|
|
shouldShowStoriesSettings: boolean;
|
|
|
|
// Limited support features
|
|
isAudioNotificationsSupported: boolean;
|
|
isAutoDownloadUpdatesSupported: boolean;
|
|
isAutoLaunchSupported: boolean;
|
|
isHideMenuBarSupported: boolean;
|
|
isNotificationAttentionSupported: boolean;
|
|
isPhoneNumberSharingSupported: boolean;
|
|
isSyncSupported: boolean;
|
|
isSystemTraySupported: boolean;
|
|
isMinimizeToAndStartInSystemTraySupported: boolean;
|
|
|
|
availableCameras: Array<
|
|
Pick<MediaDeviceInfo, 'deviceId' | 'groupId' | 'kind' | 'label'>
|
|
>;
|
|
} & Omit<MediaDeviceSettings, 'availableCameras'>;
|
|
|
|
type PropsFunctionType = {
|
|
// Other props
|
|
addCustomColor: (color: CustomColorType) => unknown;
|
|
closeSettings: () => unknown;
|
|
doDeleteAllData: () => unknown;
|
|
doneRendering: () => unknown;
|
|
editCustomColor: (colorId: string, color: CustomColorType) => unknown;
|
|
executeMenuRole: ExecuteMenuRoleType;
|
|
getConversationsWithCustomColor: (
|
|
colorId: string
|
|
) => Promise<Array<ConversationType>>;
|
|
makeSyncRequest: () => unknown;
|
|
removeCustomColor: (colorId: string) => unknown;
|
|
removeCustomColorOnConversations: (colorId: string) => unknown;
|
|
resetAllChatColors: () => unknown;
|
|
resetDefaultChatColor: () => unknown;
|
|
setGlobalDefaultConversationColor: (
|
|
color: ConversationColorType,
|
|
customColorData?: {
|
|
id: string;
|
|
value: CustomColorType;
|
|
}
|
|
) => unknown;
|
|
|
|
// Change handlers
|
|
onAudioNotificationsChange: CheckboxChangeHandlerType;
|
|
onAutoDownloadUpdateChange: CheckboxChangeHandlerType;
|
|
onAutoLaunchChange: CheckboxChangeHandlerType;
|
|
onCallNotificationsChange: CheckboxChangeHandlerType;
|
|
onCallRingtoneNotificationChange: CheckboxChangeHandlerType;
|
|
onCountMutedConversationsChange: CheckboxChangeHandlerType;
|
|
onHasStoriesDisabledChanged: SelectChangeHandlerType<boolean>;
|
|
onHideMenuBarChange: CheckboxChangeHandlerType;
|
|
onIncomingCallNotificationsChange: CheckboxChangeHandlerType;
|
|
onLastSyncTimeChange: (time: number) => unknown;
|
|
onMediaCameraPermissionsChange: CheckboxChangeHandlerType;
|
|
onMediaPermissionsChange: 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;
|
|
onThemeChange: SelectChangeHandlerType<ThemeType>;
|
|
onUniversalExpireTimerChange: SelectChangeHandlerType<number>;
|
|
onZoomFactorChange: SelectChangeHandlerType<ZoomFactorType>;
|
|
|
|
// Localization
|
|
i18n: LocalizerType;
|
|
};
|
|
|
|
export type PropsType = PropsDataType & PropsFunctionType;
|
|
|
|
enum Page {
|
|
// Accessible through left nav
|
|
General = 'General',
|
|
Appearance = 'Appearance',
|
|
Chats = 'Chats',
|
|
Calls = 'Calls',
|
|
Notifications = 'Notifications',
|
|
Privacy = 'Privacy',
|
|
|
|
// Sub pages
|
|
ChatColor = 'ChatColor',
|
|
}
|
|
|
|
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({
|
|
addCustomColor,
|
|
availableCameras,
|
|
availableMicrophones,
|
|
availableSpeakers,
|
|
blockedCount,
|
|
closeSettings,
|
|
customColors,
|
|
defaultConversationColor,
|
|
deviceName = '',
|
|
doDeleteAllData,
|
|
doneRendering,
|
|
editCustomColor,
|
|
executeMenuRole,
|
|
getConversationsWithCustomColor,
|
|
hasAudioNotifications,
|
|
hasAutoDownloadUpdate,
|
|
hasAutoLaunch,
|
|
hasCallNotifications,
|
|
hasCallRingtoneNotification,
|
|
hasCountMutedConversations,
|
|
hasHideMenuBar,
|
|
hasIncomingCallNotifications,
|
|
hasLinkPreviews,
|
|
hasMediaCameraPermissions,
|
|
hasMediaPermissions,
|
|
hasMinimizeToAndStartInSystemTray,
|
|
hasMinimizeToSystemTray,
|
|
hasNotificationAttention,
|
|
hasNotifications,
|
|
hasReadReceipts,
|
|
hasRelayCalls,
|
|
hasSpellCheck,
|
|
hasStoriesDisabled,
|
|
hasTypingIndicators,
|
|
i18n,
|
|
initialSpellCheckSetting,
|
|
isAudioNotificationsSupported,
|
|
isAutoDownloadUpdatesSupported,
|
|
isAutoLaunchSupported,
|
|
isHideMenuBarSupported,
|
|
isPhoneNumberSharingSupported,
|
|
isNotificationAttentionSupported,
|
|
isSyncSupported,
|
|
isSystemTraySupported,
|
|
isMinimizeToAndStartInSystemTraySupported,
|
|
hasCustomTitleBar,
|
|
lastSyncTime,
|
|
makeSyncRequest,
|
|
notificationContent,
|
|
onAudioNotificationsChange,
|
|
onAutoDownloadUpdateChange,
|
|
onAutoLaunchChange,
|
|
onCallNotificationsChange,
|
|
onCallRingtoneNotificationChange,
|
|
onCountMutedConversationsChange,
|
|
onHasStoriesDisabledChanged,
|
|
onHideMenuBarChange,
|
|
onIncomingCallNotificationsChange,
|
|
onLastSyncTimeChange,
|
|
onMediaCameraPermissionsChange,
|
|
onMediaPermissionsChange,
|
|
onMinimizeToAndStartInSystemTrayChange,
|
|
onMinimizeToSystemTrayChange,
|
|
onNotificationAttentionChange,
|
|
onNotificationContentChange,
|
|
onNotificationsChange,
|
|
onRelayCallsChange,
|
|
onSelectedCameraChange,
|
|
onSelectedMicrophoneChange,
|
|
onSelectedSpeakerChange,
|
|
onSentMediaQualityChange,
|
|
onSpellCheckChange,
|
|
onThemeChange,
|
|
onUniversalExpireTimerChange,
|
|
onZoomFactorChange,
|
|
removeCustomColor,
|
|
removeCustomColorOnConversations,
|
|
resetAllChatColors,
|
|
resetDefaultChatColor,
|
|
selectedCamera,
|
|
selectedMicrophone,
|
|
selectedSpeaker,
|
|
sentMediaQualitySetting,
|
|
setGlobalDefaultConversationColor,
|
|
shouldShowStoriesSettings,
|
|
themeSetting,
|
|
universalExpireTimer = DurationInSeconds.ZERO,
|
|
whoCanFindMe,
|
|
whoCanSeeMe,
|
|
zoomFactor,
|
|
}: PropsType): JSX.Element {
|
|
const storiesId = useUniqueId();
|
|
const themeSelectId = useUniqueId();
|
|
const zoomSelectId = useUniqueId();
|
|
|
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
const [confirmStoriesOff, setConfirmStoriesOff] = useState(false);
|
|
const [page, setPage] = useState<Page>(Page.General);
|
|
const [showSyncFailed, setShowSyncFailed] = useState(false);
|
|
const [nowSyncing, setNowSyncing] = useState(false);
|
|
const [showDisappearingTimerDialog, setShowDisappearingTimerDialog] =
|
|
useState(false);
|
|
const theme = useTheme();
|
|
|
|
useEffect(() => {
|
|
doneRendering();
|
|
}, [doneRendering]);
|
|
|
|
useEscapeHandling(closeSettings);
|
|
|
|
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 onAudioOutputSelectChange = useCallback(
|
|
(value: string) => {
|
|
if (value === 'undefined') {
|
|
onSelectedSpeakerChange(undefined);
|
|
} else {
|
|
onSelectedSpeakerChange(availableSpeakers[parseInt(value, 10)]);
|
|
}
|
|
},
|
|
[onSelectedSpeakerChange, availableSpeakers]
|
|
);
|
|
|
|
let settings: JSX.Element | undefined;
|
|
if (page === Page.General) {
|
|
settings = (
|
|
<>
|
|
<div className="Preferences__title">
|
|
<div className="Preferences__title--header">
|
|
{i18n('Preferences__button--general')}
|
|
</div>
|
|
</div>
|
|
<SettingsRow>
|
|
<Control left={i18n('Preferences--device-name')} right={deviceName} />
|
|
</SettingsRow>
|
|
<SettingsRow title={i18n('Preferences--system')}>
|
|
{isAutoLaunchSupported && (
|
|
<Checkbox
|
|
checked={hasAutoLaunch}
|
|
label={i18n('autoLaunchDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="autoLaunch"
|
|
onChange={onAutoLaunchChange}
|
|
/>
|
|
)}
|
|
{isHideMenuBarSupported && (
|
|
<Checkbox
|
|
checked={hasHideMenuBar}
|
|
label={i18n('hideMenuBar')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="hideMenuBar"
|
|
onChange={onHideMenuBarChange}
|
|
/>
|
|
)}
|
|
{isSystemTraySupported && (
|
|
<>
|
|
<Checkbox
|
|
checked={hasMinimizeToSystemTray}
|
|
label={i18n('SystemTraySetting__minimize-to-system-tray')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="system-tray-setting-minimize-to-system-tray"
|
|
onChange={onMinimizeToSystemTrayChange}
|
|
/>
|
|
{isMinimizeToAndStartInSystemTraySupported && (
|
|
<Checkbox
|
|
checked={hasMinimizeToAndStartInSystemTray}
|
|
disabled={!hasMinimizeToSystemTray}
|
|
label={i18n(
|
|
'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('permissions')}>
|
|
<Checkbox
|
|
checked={hasMediaPermissions}
|
|
label={i18n('mediaPermissionsDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="mediaPermissions"
|
|
onChange={onMediaPermissionsChange}
|
|
/>
|
|
<Checkbox
|
|
checked={hasMediaCameraPermissions}
|
|
label={i18n('mediaCameraPermissionsDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="mediaCameraPermissions"
|
|
onChange={onMediaCameraPermissionsChange}
|
|
/>
|
|
</SettingsRow>
|
|
{isAutoDownloadUpdatesSupported && (
|
|
<SettingsRow title={i18n('Preferences--updates')}>
|
|
<Checkbox
|
|
checked={hasAutoDownloadUpdate}
|
|
label={i18n('Preferences__download-update')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="autoDownloadUpdate"
|
|
onChange={onAutoDownloadUpdateChange}
|
|
/>
|
|
</SettingsRow>
|
|
)}
|
|
</>
|
|
);
|
|
} else if (page === Page.Appearance) {
|
|
let zoomFactors = DEFAULT_ZOOM_FACTORS;
|
|
|
|
if (!zoomFactors.some(({ value }) => value === zoomFactor)) {
|
|
zoomFactors = [
|
|
...zoomFactors,
|
|
{
|
|
text: `${Math.round(zoomFactor * 100)}%`,
|
|
value: zoomFactor,
|
|
},
|
|
].sort((a, b) => a.value - b.value);
|
|
}
|
|
|
|
settings = (
|
|
<>
|
|
<div className="Preferences__title">
|
|
<div className="Preferences__title--header">
|
|
{i18n('Preferences__button--appearance')}
|
|
</div>
|
|
</div>
|
|
<SettingsRow>
|
|
<Control
|
|
left={
|
|
<label htmlFor={themeSelectId}>
|
|
{i18n('Preferences--theme')}
|
|
</label>
|
|
}
|
|
right={
|
|
<Select
|
|
id={themeSelectId}
|
|
onChange={onThemeChange}
|
|
options={[
|
|
{
|
|
text: i18n('themeSystem'),
|
|
value: 'system',
|
|
},
|
|
{
|
|
text: i18n('themeLight'),
|
|
value: 'light',
|
|
},
|
|
{
|
|
text: i18n('themeDark'),
|
|
value: 'dark',
|
|
},
|
|
]}
|
|
value={themeSetting}
|
|
/>
|
|
}
|
|
/>
|
|
<Control
|
|
left={i18n('showChatColorEditor')}
|
|
onClick={() => {
|
|
setPage(Page.ChatColor);
|
|
}}
|
|
right={
|
|
<div
|
|
className={`ConversationDetails__chat-color ConversationDetails__chat-color--${defaultConversationColor.color}`}
|
|
style={{
|
|
...getCustomColorStyle(
|
|
defaultConversationColor.customColorData?.value
|
|
),
|
|
}}
|
|
/>
|
|
}
|
|
/>
|
|
<Control
|
|
left={
|
|
<label htmlFor={zoomSelectId}>{i18n('Preferences--zoom')}</label>
|
|
}
|
|
right={
|
|
<Select
|
|
id={zoomSelectId}
|
|
onChange={onZoomSelectChange}
|
|
options={zoomFactors}
|
|
value={zoomFactor}
|
|
/>
|
|
}
|
|
/>
|
|
</SettingsRow>
|
|
</>
|
|
);
|
|
} else if (page === Page.Chats) {
|
|
let spellCheckDirtyText: string | undefined;
|
|
if (initialSpellCheckSetting !== hasSpellCheck) {
|
|
spellCheckDirtyText = hasSpellCheck
|
|
? i18n('spellCheckWillBeEnabled')
|
|
: i18n('spellCheckWillBeDisabled');
|
|
}
|
|
|
|
const lastSyncDate = new Date(lastSyncTime || 0);
|
|
|
|
settings = (
|
|
<>
|
|
<div className="Preferences__title">
|
|
<div className="Preferences__title--header">
|
|
{i18n('Preferences__button--chats')}
|
|
</div>
|
|
</div>
|
|
<SettingsRow title={i18n('Preferences__button--chats')}>
|
|
<Checkbox
|
|
checked={hasSpellCheck}
|
|
description={spellCheckDirtyText}
|
|
label={i18n('spellCheckDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="spellcheck"
|
|
onChange={onSpellCheckChange}
|
|
/>
|
|
<Checkbox
|
|
checked={hasLinkPreviews}
|
|
description={i18n('Preferences__link-previews--description')}
|
|
disabled
|
|
label={i18n('Preferences__link-previews--title')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="linkPreviews"
|
|
onChange={noop}
|
|
/>
|
|
<Control
|
|
left={i18n('Preferences__sent-media-quality')}
|
|
right={
|
|
<Select
|
|
onChange={onSentMediaQualityChange}
|
|
options={[
|
|
{
|
|
text: i18n('sentMediaQualityStandard'),
|
|
value: 'standard',
|
|
},
|
|
{
|
|
text: i18n('sentMediaQualityHigh'),
|
|
value: 'high',
|
|
},
|
|
]}
|
|
value={sentMediaQualitySetting}
|
|
/>
|
|
}
|
|
/>
|
|
</SettingsRow>
|
|
{isSyncSupported && (
|
|
<SettingsRow>
|
|
<Control
|
|
left={
|
|
<>
|
|
<div>{i18n('sync')}</div>
|
|
<div className="Preferences__description">
|
|
{i18n('syncExplanation')}{' '}
|
|
{i18n('Preferences--lastSynced', {
|
|
date: lastSyncDate.toLocaleDateString(),
|
|
time: lastSyncDate.toLocaleTimeString(),
|
|
})}
|
|
</div>
|
|
{showSyncFailed && (
|
|
<div className="Preferences__description Preferences__description--error">
|
|
{i18n('syncFailed')}
|
|
</div>
|
|
)}
|
|
</>
|
|
}
|
|
right={
|
|
<div className="Preferences__right-button">
|
|
<Button
|
|
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('syncNow')}
|
|
</Button>
|
|
</div>
|
|
}
|
|
/>
|
|
</SettingsRow>
|
|
)}
|
|
</>
|
|
);
|
|
} else if (page === Page.Calls) {
|
|
settings = (
|
|
<>
|
|
<div className="Preferences__title">
|
|
<div className="Preferences__title--header">
|
|
{i18n('Preferences__button--calls')}
|
|
</div>
|
|
</div>
|
|
<SettingsRow title={i18n('calling')}>
|
|
<Checkbox
|
|
checked={hasIncomingCallNotifications}
|
|
label={i18n('incomingCallNotificationDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="incomingCallNotification"
|
|
onChange={onIncomingCallNotificationsChange}
|
|
/>
|
|
<Checkbox
|
|
checked={hasCallRingtoneNotification}
|
|
label={i18n('callRingtoneNotificationDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="callRingtoneNotification"
|
|
onChange={onCallRingtoneNotificationChange}
|
|
/>
|
|
</SettingsRow>
|
|
<SettingsRow title={i18n('Preferences__devices')}>
|
|
<Control
|
|
left={
|
|
<>
|
|
<label className="Preferences__select-title" htmlFor="video">
|
|
{i18n('callingDeviceSelection__label--video')}
|
|
</label>
|
|
<Select
|
|
ariaLabel={i18n('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(
|
|
'callingDeviceSelection__select--no-device'
|
|
),
|
|
value: 'undefined',
|
|
},
|
|
]
|
|
}
|
|
value={selectedCamera}
|
|
/>
|
|
</>
|
|
}
|
|
right={<div />}
|
|
/>
|
|
<Control
|
|
left={
|
|
<>
|
|
<label
|
|
className="Preferences__select-title"
|
|
htmlFor="audio-input"
|
|
>
|
|
{i18n('callingDeviceSelection__label--audio-input')}
|
|
</label>
|
|
<Select
|
|
ariaLabel={i18n('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(
|
|
'callingDeviceSelection__select--no-device'
|
|
),
|
|
value: 'undefined',
|
|
},
|
|
]
|
|
}
|
|
value={selectedMicrophone?.index}
|
|
/>
|
|
</>
|
|
}
|
|
right={<div />}
|
|
/>
|
|
<Control
|
|
left={
|
|
<>
|
|
<label
|
|
className="Preferences__select-title"
|
|
htmlFor="audio-output"
|
|
>
|
|
{i18n('callingDeviceSelection__label--audio-output')}
|
|
</label>
|
|
<Select
|
|
ariaLabel={i18n(
|
|
'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(
|
|
'callingDeviceSelection__select--no-device'
|
|
),
|
|
value: 'undefined',
|
|
},
|
|
]
|
|
}
|
|
value={selectedSpeaker?.index}
|
|
/>
|
|
</>
|
|
}
|
|
right={<div />}
|
|
/>
|
|
</SettingsRow>
|
|
<SettingsRow title={i18n('Preferences--advanced')}>
|
|
<Checkbox
|
|
checked={hasRelayCalls}
|
|
description={i18n('alwaysRelayCallsDetail')}
|
|
label={i18n('alwaysRelayCallsDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="relayCalls"
|
|
onChange={onRelayCallsChange}
|
|
/>
|
|
</SettingsRow>
|
|
</>
|
|
);
|
|
} else if (page === Page.Notifications) {
|
|
settings = (
|
|
<>
|
|
<div className="Preferences__title">
|
|
<div className="Preferences__title--header">
|
|
{i18n('Preferences__button--notifications')}
|
|
</div>
|
|
</div>
|
|
<SettingsRow>
|
|
<Checkbox
|
|
checked={hasNotifications}
|
|
label={i18n('Preferences__enable-notifications')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="notifications"
|
|
onChange={onNotificationsChange}
|
|
/>
|
|
<Checkbox
|
|
checked={hasCallNotifications}
|
|
label={i18n('callSystemNotificationDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="callSystemNotification"
|
|
onChange={onCallNotificationsChange}
|
|
/>
|
|
{isNotificationAttentionSupported && (
|
|
<Checkbox
|
|
checked={hasNotificationAttention}
|
|
label={i18n('notificationDrawAttention')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="notificationDrawAttention"
|
|
onChange={onNotificationAttentionChange}
|
|
/>
|
|
)}
|
|
{isAudioNotificationsSupported && (
|
|
<Checkbox
|
|
checked={hasAudioNotifications}
|
|
label={i18n('audioNotificationDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="audioNotification"
|
|
onChange={onAudioNotificationsChange}
|
|
/>
|
|
)}
|
|
<Checkbox
|
|
checked={hasCountMutedConversations}
|
|
label={i18n('countMutedConversationsDescription')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="countMutedConversations"
|
|
onChange={onCountMutedConversationsChange}
|
|
/>
|
|
</SettingsRow>
|
|
<SettingsRow>
|
|
<Control
|
|
left={i18n('Preferences--notification-content')}
|
|
right={
|
|
<Select
|
|
ariaLabel={i18n('Preferences--notification-content')}
|
|
disabled={!hasNotifications}
|
|
onChange={onNotificationContentChange}
|
|
options={[
|
|
{
|
|
text: i18n('nameAndMessage'),
|
|
value: 'message',
|
|
},
|
|
{
|
|
text: i18n('nameOnly'),
|
|
value: 'name',
|
|
},
|
|
{
|
|
text: i18n('noNameOrMessage'),
|
|
value: 'count',
|
|
},
|
|
]}
|
|
value={notificationContent}
|
|
/>
|
|
}
|
|
/>
|
|
</SettingsRow>
|
|
</>
|
|
);
|
|
} else if (page === Page.Privacy) {
|
|
const isCustomDisappearingMessageValue =
|
|
!DEFAULT_DURATIONS_SET.has(universalExpireTimer);
|
|
|
|
settings = (
|
|
<>
|
|
<div className="Preferences__title">
|
|
<div className="Preferences__title--header">
|
|
{i18n('Preferences__button--privacy')}
|
|
</div>
|
|
</div>
|
|
<SettingsRow>
|
|
<Control
|
|
left={i18n('Preferences--blocked')}
|
|
right={
|
|
blockedCount === 1
|
|
? i18n('Preferences--blocked-count-singular', [
|
|
String(blockedCount),
|
|
])
|
|
: i18n('Preferences--blocked-count-plural', [
|
|
String(blockedCount || 0),
|
|
])
|
|
}
|
|
/>
|
|
</SettingsRow>
|
|
{isPhoneNumberSharingSupported ? (
|
|
<SettingsRow title={i18n('Preferences__who-can--title')}>
|
|
<Control
|
|
left={i18n('Preferences--see-me')}
|
|
right={
|
|
<Select
|
|
ariaLabel={i18n('Preferences--see-me')}
|
|
disabled
|
|
onChange={noop}
|
|
options={[
|
|
{
|
|
text: i18n('Preferences__who-can--everybody'),
|
|
value: PhoneNumberSharingMode.Everybody,
|
|
},
|
|
{
|
|
text: i18n('Preferences__who-can--contacts'),
|
|
value: PhoneNumberSharingMode.ContactsOnly,
|
|
},
|
|
{
|
|
text: i18n('Preferences__who-can--nobody'),
|
|
value: PhoneNumberSharingMode.Nobody,
|
|
},
|
|
]}
|
|
value={whoCanSeeMe}
|
|
/>
|
|
}
|
|
/>
|
|
<Control
|
|
left={i18n('Preferences--find-me')}
|
|
right={
|
|
<Select
|
|
ariaLabel={i18n('Preferences--find-me')}
|
|
disabled
|
|
onChange={noop}
|
|
options={[
|
|
{
|
|
text: i18n('Preferences__who-can--everybody'),
|
|
value: PhoneNumberDiscoverability.Discoverable,
|
|
},
|
|
{
|
|
text: i18n('Preferences__who-can--nobody'),
|
|
value: PhoneNumberDiscoverability.NotDiscoverable,
|
|
},
|
|
]}
|
|
value={whoCanFindMe}
|
|
/>
|
|
}
|
|
/>
|
|
<div className="Preferences__padding">
|
|
<div className="Preferences__description">
|
|
{i18n('Preferences__privacy--description')}
|
|
</div>
|
|
</div>
|
|
</SettingsRow>
|
|
) : null}
|
|
<SettingsRow title={i18n('Preferences--messaging')}>
|
|
<Checkbox
|
|
checked={hasReadReceipts}
|
|
disabled
|
|
label={i18n('Preferences--read-receipts')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="readReceipts"
|
|
onChange={noop}
|
|
/>
|
|
<Checkbox
|
|
checked={hasTypingIndicators}
|
|
disabled
|
|
label={i18n('Preferences--typing-indicators')}
|
|
moduleClassName="Preferences__checkbox"
|
|
name="typingIndicators"
|
|
onChange={noop}
|
|
/>
|
|
<div className="Preferences__padding">
|
|
<div className="Preferences__description">
|
|
{i18n('Preferences__privacy--description')}
|
|
</div>
|
|
</div>
|
|
</SettingsRow>
|
|
{showDisappearingTimerDialog && (
|
|
<DisappearingTimeDialog
|
|
i18n={i18n}
|
|
initialValue={universalExpireTimer}
|
|
onClose={() => setShowDisappearingTimerDialog(false)}
|
|
onSubmit={onUniversalExpireTimerChange}
|
|
/>
|
|
)}
|
|
<SettingsRow title={i18n('icu:disappearingMessages')}>
|
|
<Control
|
|
left={
|
|
<>
|
|
<div>
|
|
{i18n('settings__DisappearingMessages__timer__label')}
|
|
</div>
|
|
<div className="Preferences__description">
|
|
{i18n('settings__DisappearingMessages__footer')}
|
|
</div>
|
|
</>
|
|
}
|
|
right={
|
|
<Select
|
|
ariaLabel={i18n('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('selectedCustomDisappearingTimeOption'),
|
|
},
|
|
])}
|
|
value={universalExpireTimer}
|
|
/>
|
|
}
|
|
/>
|
|
</SettingsRow>
|
|
{shouldShowStoriesSettings && (
|
|
<SettingsRow title={i18n('Stories__title')}>
|
|
<Control
|
|
left={
|
|
<label htmlFor={storiesId}>
|
|
<div>{i18n('Stories__settings-toggle--title')}</div>
|
|
<div className="Preferences__description">
|
|
{i18n('Stories__settings-toggle--description')}
|
|
</div>
|
|
</label>
|
|
}
|
|
right={
|
|
hasStoriesDisabled ? (
|
|
<Button
|
|
onClick={() => onHasStoriesDisabledChanged(false)}
|
|
variant={ButtonVariant.Secondary}
|
|
>
|
|
{i18n('Preferences__turn-stories-on')}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
className="Preferences__stories-off"
|
|
onClick={() => setConfirmStoriesOff(true)}
|
|
variant={ButtonVariant.SecondaryDestructive}
|
|
>
|
|
{i18n('Preferences__turn-stories-off')}
|
|
</Button>
|
|
)
|
|
}
|
|
/>
|
|
</SettingsRow>
|
|
)}
|
|
<SettingsRow>
|
|
<Control
|
|
left={
|
|
<>
|
|
<div>{i18n('clearDataHeader')}</div>
|
|
<div className="Preferences__description">
|
|
{i18n('clearDataExplanation')}
|
|
</div>
|
|
</>
|
|
}
|
|
right={
|
|
<div className="Preferences__right-button">
|
|
<Button
|
|
onClick={() => setConfirmDelete(true)}
|
|
variant={ButtonVariant.SecondaryDestructive}
|
|
>
|
|
{i18n('clearDataButton')}
|
|
</Button>
|
|
</div>
|
|
}
|
|
/>
|
|
</SettingsRow>
|
|
{confirmDelete ? (
|
|
<ConfirmationDialog
|
|
dialogName="Preference.deleteAllData"
|
|
actions={[
|
|
{
|
|
action: doDeleteAllData,
|
|
style: 'negative',
|
|
text: i18n('clearDataButton'),
|
|
},
|
|
]}
|
|
i18n={i18n}
|
|
onClose={() => {
|
|
setConfirmDelete(false);
|
|
}}
|
|
title={i18n('deleteAllDataHeader')}
|
|
>
|
|
{i18n('deleteAllDataBody')}
|
|
</ConfirmationDialog>
|
|
) : null}
|
|
{confirmStoriesOff ? (
|
|
<ConfirmationDialog
|
|
dialogName="Preference.turnStoriesOff"
|
|
actions={[
|
|
{
|
|
action: () => onHasStoriesDisabledChanged(true),
|
|
style: 'negative',
|
|
text: i18n('Preferences__turn-stories-off--action'),
|
|
},
|
|
]}
|
|
i18n={i18n}
|
|
onClose={() => {
|
|
setConfirmStoriesOff(false);
|
|
}}
|
|
>
|
|
{i18n('Preferences__turn-stories-off--body')}
|
|
</ConfirmationDialog>
|
|
) : null}
|
|
</>
|
|
);
|
|
} else if (page === Page.ChatColor) {
|
|
settings = (
|
|
<>
|
|
<div className="Preferences__title">
|
|
<button
|
|
aria-label={i18n('goBack')}
|
|
className="Preferences__back-icon"
|
|
onClick={() => setPage(Page.Appearance)}
|
|
type="button"
|
|
/>
|
|
<div className="Preferences__title--header">
|
|
{i18n('ChatColorPicker__menu-title')}
|
|
</div>
|
|
</div>
|
|
<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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<TitleBarContainer
|
|
hasCustomTitleBar={hasCustomTitleBar}
|
|
theme={theme}
|
|
executeMenuRole={executeMenuRole}
|
|
>
|
|
<div className="Preferences">
|
|
<div className="Preferences__page-selector">
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
Preferences__button: true,
|
|
'Preferences__button--general': true,
|
|
'Preferences__button--selected': page === Page.General,
|
|
})}
|
|
onClick={() => setPage(Page.General)}
|
|
>
|
|
{i18n('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('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('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('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('Preferences__button--notifications')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={classNames({
|
|
Preferences__button: true,
|
|
'Preferences__button--privacy': true,
|
|
'Preferences__button--selected': page === Page.Privacy,
|
|
})}
|
|
onClick={() => setPage(Page.Privacy)}
|
|
>
|
|
{i18n('Preferences__button--privacy')}
|
|
</button>
|
|
</div>
|
|
<div className="Preferences__settings-pane">{settings}</div>
|
|
</div>
|
|
</TitleBarContainer>
|
|
);
|
|
}
|
|
|
|
function SettingsRow({
|
|
children,
|
|
title,
|
|
}: {
|
|
children: ReactNode;
|
|
title?: string;
|
|
}): JSX.Element {
|
|
return (
|
|
<div className="Preferences__settings-row">
|
|
{title && <h3 className="Preferences__padding">{title}</h3>}
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Control({
|
|
left,
|
|
onClick,
|
|
right,
|
|
}: {
|
|
left: ReactNode;
|
|
onClick?: () => unknown;
|
|
right: ReactNode;
|
|
}): JSX.Element {
|
|
const content = (
|
|
<>
|
|
<div className="Preferences__control--key">{left}</div>
|
|
<div className="Preferences__control--value">{right}</div>
|
|
</>
|
|
);
|
|
|
|
if (onClick) {
|
|
return (
|
|
<button
|
|
className="Preferences__control Preferences__control--clickable"
|
|
type="button"
|
|
onClick={onClick}
|
|
>
|
|
{content}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return <div className="Preferences__control">{content}</div>;
|
|
}
|
|
|
|
function localizeDefault(i18n: LocalizerType, deviceLabel: string): string {
|
|
return deviceLabel.toLowerCase().startsWith('default')
|
|
? deviceLabel.replace(
|
|
/default/i,
|
|
i18n('callingDeviceSelection__select--default')
|
|
)
|
|
: deviceLabel;
|
|
}
|