Introduce the new Settings tab

Co-authored-by: Jamie Kyle <jamie@signal.org>
Co-authored-by: Fedor Indutny <indutny@signal.org>
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
Scott Nonnenberg 2025-05-15 13:58:20 +10:00 committed by GitHub
commit fe9d042e40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 1468 additions and 2092 deletions

View file

@ -1682,6 +1682,10 @@
"messageformat": "Need help?",
"description": "Shown on the install screen. Link takes users to a support page"
},
"icu:Preferences--header": {
"messageformat": "Settings",
"description": "Shown at the top of the settings tab when open"
},
"icu:Preferences--phone-number": {
"messageformat": "Phone Number",
"description": "The label in settings panel shown for the phone number associated with user's account"

View file

@ -1173,9 +1173,6 @@ async function readyForUpdates() {
'SettingsChannel must be initialized'
);
await updater.start({
settingsChannel,
logger: getLogger(),
getMainWindow,
canRunSilently: () => {
return (
systemTrayService?.isVisible() === true &&
@ -1183,6 +1180,9 @@ async function readyForUpdates() {
mainWindow?.webContents?.getBackgroundThrottling() !== false
);
},
getMainWindow,
logger: getLogger(),
sql,
});
} catch (error) {
getLogger().error(
@ -1424,57 +1424,6 @@ async function showAbout() {
);
}
let settingsWindow: BrowserWindow | undefined;
async function showSettingsWindow() {
if (settingsWindow) {
settingsWindow.show();
return;
}
const options = {
width: 700,
height: 700,
frame: true,
resizable: false,
title: getResolvedMessagesLocale().i18n('icu:signalDesktopPreferences'),
titleBarStyle: mainTitleBarStyle,
autoHideMenuBar: true,
backgroundColor: await getBackgroundColor(),
show: false,
webPreferences: {
...defaultWebPrefs,
nodeIntegration: false,
nodeIntegrationInWorker: false,
sandbox: true,
contextIsolation: true,
preload: join(__dirname, '../bundles/settings/preload.js'),
nativeWindowOpen: true,
},
};
settingsWindow = new BrowserWindow(options);
await handleCommonWindowEvents(settingsWindow);
settingsWindow.on('closed', () => {
settingsWindow = undefined;
});
ipc.once('settings-done-rendering', () => {
if (!settingsWindow) {
getLogger().warn('settings-done-rendering: no settingsWindow available!');
return;
}
settingsWindow.show();
});
await safeLoadURL(
settingsWindow,
await prepareFileUrl([__dirname, '../settings.html'])
);
}
async function getIsLinked() {
try {
const number = await sql.sqlRead('getItemById', 'number_id');
@ -2310,6 +2259,8 @@ app.on('ready', async () => {
},
});
appStartInitialSpellcheckSetting = await getSpellCheckSetting();
// Run window preloading in parallel with database initialization.
await createWindow();
@ -2322,8 +2273,6 @@ app.on('ready', async () => {
return;
}
appStartInitialSpellcheckSetting = await getSpellCheckSetting();
try {
const IDB_KEY = 'indexeddb-delete-needed';
const item = await sql.sqlRead('getItemById', IDB_KEY);
@ -2382,7 +2331,15 @@ function setupMenu(options?: Partial<CreateTemplateOptionsType>) {
showDebugLog: showDebugLogWindow,
showCallingDevTools: showCallingDevToolsWindow,
showKeyboardShortcuts,
showSettings: showSettingsWindow,
showSettings: () => {
if (!settingsChannel) {
getLogger().warn(
'showSettings: No settings channel; cannot open settings tab.'
);
return;
}
settingsChannel.openSettingsTab();
},
showWindow,
zoomIn,
zoomOut,
@ -2750,17 +2707,6 @@ function removeDarkOverlay() {
}
}
ipc.on('show-settings', showSettingsWindow);
ipc.on('delete-all-data', () => {
if (settingsWindow) {
settingsWindow.close();
}
if (mainWindow && mainWindow.webContents) {
mainWindow.webContents.send('delete-all-data');
}
});
ipc.on('get-config', async event => {
const theme = await getResolvedThemeSetting();

View file

@ -0,0 +1 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.836 1.284c-.55 0-1.036.36-1.196.887l-.398 1.31a.417.417 0 0 1-.19.24l-1.016.586a.417.417 0 0 1-.302.045L4.4 4.042a1.25 1.25 0 0 0-1.366.592L1.87 6.65a1.25 1.25 0 0 0 .17 1.48l.936 1a.417.417 0 0 1 .112.284v1.172c0 .106-.04.208-.112.285l-.936 1a1.25 1.25 0 0 0-.17 1.48l1.164 2.015c.275.477.83.717 1.366.592l1.334-.31a.416.416 0 0 1 .302.045l1.016.586c.091.053.16.139.19.24l.398 1.31c.16.527.646.887 1.196.887h2.328c.55 0 1.036-.36 1.196-.887l.398-1.31a.417.417 0 0 1 .19-.24l1.016-.586a.417.417 0 0 1 .303-.045l1.333.31a1.25 1.25 0 0 0 1.366-.592l1.164-2.016a1.25 1.25 0 0 0-.17-1.479l-.936-1a.417.417 0 0 1-.112-.285V9.414c0-.106.04-.208.112-.285l.936-1a1.25 1.25 0 0 0 .17-1.48l-1.163-2.015a1.25 1.25 0 0 0-1.366-.592l-1.335.31a.417.417 0 0 1-.302-.045l-1.016-.586a.417.417 0 0 1-.19-.24l-.398-1.31a1.25 1.25 0 0 0-1.196-.887H8.836ZM6.666 10a3.333 3.333 0 1 1 6.667 0 3.333 3.333 0 0 1-6.666 0Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -134,7 +134,6 @@ async function sandboxedEnv() {
path.join(ROOT_DIR, 'ts', 'windows', 'loading', 'start.ts'),
path.join(ROOT_DIR, 'ts', 'windows', 'permissions', 'app.tsx'),
path.join(ROOT_DIR, 'ts', 'windows', 'screenShare', 'app.tsx'),
path.join(ROOT_DIR, 'ts', 'windows', 'settings', 'app.tsx'),
path.join(
ROOT_DIR,
'ts',
@ -154,7 +153,6 @@ async function sandboxedEnv() {
path.join(ROOT_DIR, 'ts', 'windows', 'permissions', 'preload.ts'),
path.join(ROOT_DIR, 'ts', 'windows', 'calling-tools', 'preload.ts'),
path.join(ROOT_DIR, 'ts', 'windows', 'screenShare', 'preload.ts'),
path.join(ROOT_DIR, 'ts', 'windows', 'settings', 'preload.ts'),
],
format: 'cjs',
outdir: 'bundles',

View file

@ -170,6 +170,10 @@ $NavTabs__ProfileAvatar__size: 28px;
.NavTabs__ItemIcon--Settings {
@include NavTabs__Icon('../images/icons/v3/settings/settings.svg');
.NavTabs__Item:active &,
.NavTabs__Item[aria-selected='true'] & {
@include NavTabs__Icon('../images/icons/v3/settings/settings-fill.svg');
}
}
.NavTabs__ItemIcon--Chats {
@ -201,13 +205,31 @@ $NavTabs__ProfileAvatar__size: 28px;
}
.NavTabs__TabList {
display: flex;
flex-direction: column;
display: grid;
grid-template-rows:
[Chats] auto
[Calls] auto
[Stories] auto
1fr
[Settings] auto;
align-items: center;
width: 100%;
flex: 1;
}
.NavTabs__Item--Chats {
grid-row: Chats;
}
.NavTabs__Item--Calls {
grid-row: Calls;
}
.NavTabs__Item--Stories {
grid-row: Stories;
}
.NavTabs__Item--Settings {
grid-row: Settings;
}
.NavTabs__Misc {
width: 100%;
display: flex;

View file

@ -24,6 +24,8 @@ $secondary-text-color: light-dark(
display: flex;
overflow: hidden;
user-select: none;
width: 100vw;
@include mixins.light-theme {
background: variables.$color-white;
}
@ -31,14 +33,43 @@ $secondary-text-color: light-dark(
background: variables.$color-gray-95;
}
&__header {
margin-inline-start: 24px;
@include mixins.font-title-medium;
line-height: 20px;
margin-top: 12px;
margin-bottom: 8px;
}
&__page-selector {
padding-top: calc(24px + var(--title-bar-drag-area-height));
min-width: min(34%, 240px);
padding-top: var(--title-bar-drag-area-height);
width: 279px;
flex-grow: 0;
flex-shrink: 0;
@include mixins.light-theme {
background: variables.$color-gray-02;
background: variables.$color-gray-04;
border-inline-end: 0.5px solid variables.$color-black-alpha-16;
}
@include mixins.dark-theme {
background: variables.$color-gray-80;
border-inline-end: 0.5px solid variables.$color-white-alpha-16;
}
}
&__scroll-area {
margin-top: 8px;
overflow-y: scroll;
max-height: calc(100% - var(--title-bar-drag-area-height) - 20px);
&::-webkit-scrollbar-thumb {
@include mixins.light-theme {
background: variables.$color-gray-25;
border-color: variables.$color-gray-04;
}
@include mixins.dark-theme {
background: variables.$color-gray-45;
border-color: variables.$color-gray-80;
}
}
}
@ -58,9 +89,12 @@ $secondary-text-color: light-dark(
align-items: center;
display: flex;
height: 48px;
width: 100%;
width: calc(100% - 20px);
padding-block: 14px;
padding-inline: 0;
margin-inline-start: 10px;
margin-inline-end: 10px;
border-radius: 10px;
}
&--selected {
@ -130,12 +164,19 @@ $secondary-text-color: light-dark(
height: 100vh;
overflow: overlay;
width: 100%;
flex-grow: 1;
max-width: 825px;
&::-webkit-scrollbar-corner {
background: transparent;
}
}
&__settings-pane-spacer {
flex-grow: 1;
min-width: 0;
}
&__title {
@include mixins.font-body-1-bold;
align-items: center;

View file

@ -215,6 +215,7 @@ import { waitForEvent } from './shims/events';
import { sendSyncRequests } from './textsecure/syncRequests';
import { handleServerAlerts } from './util/handleServerAlerts';
import { isLocalBackupsEnabled } from './util/isLocalBackupsEnabled';
import { NavTab } from './state/ducks/nav';
export function isOverHourIntoPast(timestamp: number): boolean {
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
@ -1356,6 +1357,10 @@ export async function startApp(): Promise<void> {
window.reduxActions.app.openStandalone();
});
window.Whisper.events.on('openSettingsTab', () => {
window.reduxActions.nav.changeNavTab(NavTab.Settings);
});
window.Whisper.events.on('powerMonitorSuspend', () => {
log.info('powerMonitor: suspend');
server?.cancelInflightRequests('powerMonitorSuspend');

View file

@ -24,7 +24,7 @@ export default {
addCustomColor: action('addCustomColor'),
colorSelected: action('colorSelected'),
editCustomColor: action('editCustomColor'),
getConversationsWithCustomColor: (_: string) => Promise.resolve([]),
getConversationsWithCustomColor: (_: string) => [],
i18n,
removeCustomColor: action('removeCustomColor'),
removeCustomColorOnConversations: action(

View file

@ -26,9 +26,7 @@ type CustomColorDataType = {
export type PropsDataType = {
conversationId?: string;
customColors?: Record<string, CustomColorType>;
getConversationsWithCustomColor: (
colorId: string
) => Promise<Array<ConversationType>>;
getConversationsWithCustomColor: (colorId: string) => Array<ConversationType>;
i18n: LocalizerType;
isGlobal?: boolean;
selectedColor?: ConversationColorType;
@ -270,9 +268,7 @@ export function ChatColorPicker({
type CustomColorBubblePropsType = {
color: CustomColorType;
colorId: string;
getConversationsWithCustomColor: (
colorId: string
) => Promise<Array<ConversationType>>;
getConversationsWithCustomColor: (colorId: string) => Array<ConversationType>;
i18n: LocalizerType;
isSelected: boolean;
onDelete: () => unknown;
@ -393,12 +389,11 @@ function CustomColorBubble({
attributes={{
className: 'ChatColorPicker__context--delete',
}}
onClick={async (event: MouseEvent) => {
onClick={(event: MouseEvent) => {
event.stopPropagation();
event.preventDefault();
const conversations =
await getConversationsWithCustomColor(colorId);
const conversations = getConversationsWithCustomColor(colorId);
if (!conversations.length) {
onDelete();
} else {

View file

@ -3,16 +3,19 @@
import type { ReactNode } from 'react';
import React, { useCallback } from 'react';
import { isBeta } from '../util/version';
import { DialogType } from '../types/Dialogs';
import type { LocalizerType } from '../types/Util';
import { PRODUCTION_DOWNLOAD_URL, BETA_DOWNLOAD_URL } from '../types/support';
import { I18n } from './I18n';
import { LeftPaneDialog } from './LeftPaneDialog';
import type { WidthBreakpoint } from './_util';
import { formatFileSize } from '../util/formatFileSize';
import { getLocalizedUrl } from '../util/getLocalizedUrl';
import type { LocalizerType } from '../types/Util';
import type { DismissOptions } from './LeftPaneDialog';
import type { WidthBreakpoint } from './_util';
function contactSupportLink(parts: ReactNode): JSX.Element {
const localizedSupportLink = getLocalizedUrl(
'https://support.signal.org/hc/LOCALE/requests/new?desktop'
@ -32,6 +35,7 @@ function contactSupportLink(parts: ReactNode): JSX.Element {
export type PropsType = {
containerWidthBreakpoint: WidthBreakpoint;
dialogType: DialogType;
disableDismiss?: boolean;
dismissDialog: () => void;
downloadSize?: number;
downloadedSize?: number;
@ -45,6 +49,7 @@ export type PropsType = {
export function DialogUpdate({
containerWidthBreakpoint,
dialogType,
disableDismiss,
dismissDialog,
downloadSize,
downloadedSize,
@ -215,6 +220,19 @@ export function DialogUpdate({
title = i18n('icu:DialogUpdate__downloaded');
}
let dismissOptions: DismissOptions = {
hasXButton: true,
onClose: snoozeUpdate,
closeLabel: i18n('icu:autoUpdateIgnoreButtonLabel'),
};
if (disableDismiss) {
dismissOptions = {
hasXButton: false,
onClose: undefined,
closeLabel: undefined,
};
}
return (
<LeftPaneDialog
containerWidthBreakpoint={containerWidthBreakpoint}
@ -225,9 +243,7 @@ export function DialogUpdate({
hasAction
onClick={startUpdate}
clickLabel={clickLabel}
hasXButton
onClose={snoozeUpdate}
closeLabel={i18n('icu:autoUpdateIgnoreButtonLabel')}
{...dismissOptions}
/>
);
}

View file

@ -23,6 +23,7 @@ export type PropsType = {
renderCustomizingPreferredReactionsModal: () => JSX.Element;
renderNavTabs: (props: SmartNavTabsProps) => JSX.Element;
renderStoriesTab: () => JSX.Element;
renderSettingsTab: () => JSX.Element;
};
const PART_COUNT = 16;
@ -41,6 +42,7 @@ export function Inbox({
renderCustomizingPreferredReactionsModal,
renderNavTabs,
renderStoriesTab,
renderSettingsTab,
}: PropsType): JSX.Element {
const [internalHasInitialLoadCompleted, setInternalHasInitialLoadCompleted] =
useState(hasInitialLoadCompleted);
@ -200,6 +202,7 @@ export function Inbox({
renderChatsTab,
renderCallsTab,
renderStoriesTab,
renderSettingsTab,
})}
</div>
{activeModal}

View file

@ -9,6 +9,17 @@ import { WidthBreakpoint } from './_util';
const BASE_CLASS_NAME = 'LeftPaneDialog';
const TOOLTIP_CLASS_NAME = `${BASE_CLASS_NAME}__tooltip`;
export type DismissOptions =
| {
onClose?: undefined;
closeLabel?: undefined;
hasXButton?: false;
}
| {
onClose: () => void;
closeLabel: string;
hasXButton: true;
};
export type PropsType = {
type?: 'warning' | 'error' | 'info';
@ -30,18 +41,7 @@ export type PropsType = {
hasAction: boolean;
}
) &
(
| {
onClose?: undefined;
closeLabel?: undefined;
hasXButton?: false;
}
| {
onClose: () => void;
closeLabel: string;
hasXButton: true;
}
);
DismissOptions;
export function LeftPaneDialog({
icon = 'warning',

View file

@ -13,7 +13,6 @@ import { NavTab } from '../state/ducks/nav';
import { Tooltip, TooltipPlacement } from './Tooltip';
import { Theme } from '../util/theme';
import type { UnreadStats } from '../util/countUnreadStats';
import { ContextMenu } from './ContextMenu';
type NavTabsItemBadgesProps = Readonly<{
i18n: LocalizerType;
@ -72,25 +71,33 @@ function NavTabsItemBadges({
}
type NavTabProps = Readonly<{
hasError?: boolean;
i18n: LocalizerType;
iconClassName: string;
id: NavTab;
hasError?: boolean;
label: string;
navTabClassName: string;
unreadStats: UnreadStats | null;
hasPendingUpdate?: boolean;
}>;
function NavTabsItem({
hasError,
i18n,
iconClassName,
id,
label,
navTabClassName,
unreadStats,
hasError,
hasPendingUpdate,
}: NavTabProps) {
const isRTL = i18n.getLocaleDirection() === 'rtl';
return (
<Tab id={id} data-testid={`NavTabsItem--${id}`} className="NavTabs__Item">
<Tab
id={id}
data-testid={`NavTabsItem--${id}`}
className={classNames('NavTabs__Item', navTabClassName)}
>
<span className="NavTabs__ItemLabel">{label}</span>
<Tooltip
content={label}
@ -108,6 +115,7 @@ function NavTabsItem({
i18n={i18n}
unreadStats={unreadStats}
hasError={hasError}
hasPendingUpdate={hasPendingUpdate}
/>
</span>
</span>
@ -187,14 +195,13 @@ export type NavTabsProps = Readonly<{
i18n: LocalizerType;
me: ConversationType;
navTabsCollapsed: boolean;
onShowSettings: () => void;
onStartUpdate: () => unknown;
onNavTabSelected: (tab: NavTab) => void;
onToggleNavTabsCollapse: (collapsed: boolean) => void;
onToggleProfileEditor: () => void;
renderCallsTab: () => ReactNode;
renderChatsTab: () => ReactNode;
renderStoriesTab: () => ReactNode;
renderSettingsTab: () => ReactNode;
selectedNavTab: NavTab;
storiesEnabled: boolean;
theme: ThemeType;
@ -210,14 +217,13 @@ export function NavTabs({
i18n,
me,
navTabsCollapsed,
onShowSettings,
onStartUpdate,
onNavTabSelected,
onToggleNavTabsCollapse,
onToggleProfileEditor,
renderCallsTab,
renderChatsTab,
renderStoriesTab,
renderSettingsTab,
selectedNavTab,
storiesEnabled,
theme,
@ -259,6 +265,7 @@ export function NavTabs({
id={NavTab.Chats}
label={i18n('icu:NavTabs__ItemLabel--Chats')}
iconClassName="NavTabs__ItemIcon--Chats"
navTabClassName="NavTabs__Item--Chats"
unreadStats={unreadConversationsStats}
/>
<NavTabsItem
@ -266,6 +273,7 @@ export function NavTabs({
id={NavTab.Calls}
label={i18n('icu:NavTabs__ItemLabel--Calls')}
iconClassName="NavTabs__ItemIcon--Calls"
navTabClassName="NavTabs__Item--Calls"
unreadStats={{
unreadCount: unreadCallsCount,
unreadMentionsCount: 0,
@ -279,6 +287,7 @@ export function NavTabs({
label={i18n('icu:NavTabs__ItemLabel--Stories')}
iconClassName="NavTabs__ItemIcon--Stories"
hasError={hasFailedStorySends}
navTabClassName="NavTabs__Item--Stories"
unreadStats={{
unreadCount: unreadStoriesCount,
unreadMentionsCount: 0,
@ -286,75 +295,21 @@ export function NavTabs({
}}
/>
)}
</TabList>
<div className="NavTabs__Misc">
<ContextMenu
<NavTabsItem
i18n={i18n}
menuOptions={[
{
icon: 'NavTabs__ContextMenuIcon--Settings',
label: i18n('icu:NavTabs__ItemLabel--Settings'),
onClick: onShowSettings,
},
{
icon: 'NavTabs__ContextMenuIcon--Update',
label: i18n('icu:NavTabs__ItemLabel--Update'),
onClick: onStartUpdate,
},
]}
popperOptions={{
placement: 'top-start',
strategy: 'absolute',
id={NavTab.Settings}
label={i18n('icu:NavTabs__ItemLabel--Settings')}
iconClassName="NavTabs__ItemIcon--Settings"
navTabClassName="NavTabs__Item--Settings"
unreadStats={{
unreadCount: unreadCallsCount,
unreadMentionsCount: 0,
markedUnread: false,
}}
portalToRoot
>
{({ onClick, onKeyDown, ref }) => {
return (
<button
type="button"
className="NavTabs__Item"
onKeyDown={event => {
if (hasPendingUpdate) {
onKeyDown(event);
}
}}
onClick={event => {
if (hasPendingUpdate) {
onClick(event);
} else {
onShowSettings();
}
}}
>
<Tooltip
content={i18n('icu:NavTabs__ItemLabel--Settings')}
theme={Theme.Dark}
direction={TooltipPlacement.Right}
delay={600}
>
<span className="NavTabs__ItemButton" ref={ref}>
<span className="NavTabs__ItemContent">
<span
role="presentation"
className="NavTabs__ItemIcon NavTabs__ItemIcon--Settings"
/>
<span className="NavTabs__ItemLabel">
{i18n('icu:NavTabs__ItemLabel--Settings')}
</span>
<NavTabsItemBadges
i18n={i18n}
unreadStats={null}
hasPendingUpdate={hasPendingUpdate}
/>
</span>
</span>
</Tooltip>
</button>
);
}}
</ContextMenu>
</TabList>
<div className="NavTabs__Misc">
<button
type="button"
className="NavTabs__Item NavTabs__Item--Profile"
@ -402,6 +357,9 @@ export function NavTabs({
<TabPanel id={NavTab.Stories} className="NavTabs__TabPanel">
{renderStoriesTab}
</TabPanel>
<TabPanel id={NavTab.Settings} className="NavTabs__TabPanel">
{renderSettingsTab}
</TabPanel>
</Tabs>
);
}

View file

@ -5,13 +5,17 @@ import type { Meta, StoryFn } from '@storybook/react';
import React from 'react';
import { action } from '@storybook/addon-actions';
import type { PropsType } from './Preferences';
import { Page, Preferences } from './Preferences';
import { DEFAULT_CONVERSATION_COLOR } from '../types/Colors';
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
import { EmojiSkinTone } from './fun/data/emojis';
import { DAY, DurationInSeconds, WEEK } from '../util/durations';
import { DialogUpdate } from './DialogUpdate';
import { DialogType } from '../types/Dialogs';
import type { PropsType } from './Preferences';
import type { WidthBreakpoint } from './_util';
const { i18n } = window.SignalContext;
@ -65,6 +69,26 @@ const exportLocalBackupResult = {
snapshotDir: '/home/signaluser/SignalBackups/signal-backup-1745618069169',
};
function renderUpdateDialog(
props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
): JSX.Element {
return (
<DialogUpdate
i18n={i18n}
containerWidthBreakpoint={props.containerWidthBreakpoint}
dialogType={DialogType.DownloadReady}
downloadSize={100000}
downloadedSize={50000}
version="8.99.0"
currentVersion="8.98.00"
disableDismiss
dismissDialog={action('dismissDialog')}
snoozeUpdate={action('snoozeUpdate')}
startUpdate={action('startUpdate')}
/>
);
}
export default {
title: 'Components/Preferences',
component: Preferences,
@ -123,6 +147,7 @@ export default {
hasMinimizeToSystemTray: true,
hasNotificationAttention: false,
hasNotifications: true,
hasPendingUpdate: false,
hasReadReceipts: true,
hasRelayCalls: false,
hasSpellCheck: true,
@ -140,6 +165,7 @@ export default {
isContentProtectionSupported: true,
isContentProtectionNeeded: true,
isMinimizeToAndStartInSystemTraySupported: true,
isUpdateDownloaded: false,
lastSyncTime: Date.now(),
localeOverride: null,
notificationContent: 'name',
@ -156,12 +182,11 @@ export default {
whoCanSeeMe: PhoneNumberSharingMode.Everybody,
zoomFactor: 1,
getConversationsWithCustomColor: () => Promise.resolve([]),
renderUpdateDialog,
getConversationsWithCustomColor: () => [],
addCustomColor: action('addCustomColor'),
closeSettings: action('closeSettings'),
doDeleteAllData: action('doDeleteAllData'),
doneRendering: action('doneRendering'),
editCustomColor: action('editCustomColor'),
exportLocalBackup: async () => {
return {
@ -211,6 +236,7 @@ export default {
onSelectedSpeakerChange: action('onSelectedSpeakerChange'),
onSentMediaQualityChange: action('onSentMediaQualityChange'),
onSpellCheckChange: action('onSpellCheckChange'),
onStartUpdate: action('onStartUpdate'),
onTextFormattingChange: action('onTextFormattingChange'),
onThemeChange: action('onThemeChange'),
onUniversalExpireTimerChange: action('onUniversalExpireTimerChange'),
@ -350,3 +376,13 @@ Internal.args = {
initialPage: Page.Internal,
isInternalUser: true,
};
export const UpdateAvailable = Template.bind({});
UpdateAvailable.args = {
hasPendingUpdate: true,
};
export const UpdateDownloaded = Template.bind({});
UpdateDownloaded.args = {
isUpdateDownloaded: true,
};

View file

@ -9,7 +9,7 @@ import React, {
useState,
useId,
} from 'react';
import { noop, partition } from 'lodash';
import { isNumber, noop, partition } from 'lodash';
import classNames from 'classnames';
import * as LocaleMatcher from '@formatjs/intl-localematcher';
import type { MediaDeviceSettings } from '../types/Calling';
@ -53,7 +53,6 @@ import {
format as formatExpirationTimer,
} from '../util/expirationTimer';
import { DurationInSeconds } from '../util/durations';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { focusableSelector } from '../util/focusableSelectors';
import { Modal } from './Modal';
import { SearchInput } from './SearchInput';
@ -93,24 +92,24 @@ export type PropsDataType = {
hasAudioNotifications?: boolean;
hasAutoConvertEmoji: boolean;
hasAutoDownloadUpdate: boolean;
hasAutoLaunch: boolean;
hasAutoLaunch: boolean | undefined;
hasCallNotifications: boolean;
hasCallRingtoneNotification: boolean;
hasContentProtection: boolean;
hasContentProtection: boolean | undefined;
hasCountMutedConversations: boolean;
hasHideMenuBar?: boolean;
hasIncomingCallNotifications: boolean;
hasLinkPreviews: boolean;
hasMediaCameraPermissions: boolean | undefined;
hasMediaPermissions: boolean;
hasMediaPermissions: boolean | undefined;
hasMessageAudio: boolean;
hasMinimizeToAndStartInSystemTray: boolean;
hasMinimizeToSystemTray: boolean;
hasMinimizeToAndStartInSystemTray: boolean | undefined;
hasMinimizeToSystemTray: boolean | undefined;
hasNotificationAttention: boolean;
hasNotifications: boolean;
hasReadReceipts: boolean;
hasRelayCalls?: boolean;
hasSpellCheck: boolean;
hasSpellCheck: boolean | undefined;
hasStoriesDisabled: boolean;
hasTextFormatting: boolean;
hasTypingIndicators: boolean;
@ -122,51 +121,56 @@ export type PropsDataType = {
selectedMicrophone?: AudioDevice;
selectedSpeaker?: AudioDevice;
sentMediaQualitySetting: SentMediaQualitySettingType;
themeSetting: ThemeSettingType;
themeSetting: ThemeSettingType | undefined;
universalExpireTimer: DurationInSeconds;
whoCanFindMe: PhoneNumberDiscoverability;
whoCanSeeMe: PhoneNumberSharingMode;
zoomFactor: ZoomFactorType;
zoomFactor: ZoomFactorType | undefined;
// Localization
availableLocales: ReadonlyArray<string>;
localeOverride: string | null;
localeOverride: string | null | undefined;
preferredSystemLocales: ReadonlyArray<string>;
resolvedLocale: string;
// Other props
hasPendingUpdate: boolean;
initialSpellCheckSetting: boolean;
isUpdateDownloaded: boolean;
// Limited support features
isAutoDownloadUpdatesSupported: boolean;
isAutoLaunchSupported: boolean;
isContentProtectionNeeded: boolean;
isContentProtectionSupported: boolean;
isHideMenuBarSupported: boolean;
isNotificationAttentionSupported: boolean;
isSyncSupported: boolean;
isSystemTraySupported: boolean;
isMinimizeToAndStartInSystemTraySupported: boolean;
isInternalUser: boolean;
isContentProtectionNeeded: boolean;
isContentProtectionSupported: boolean;
// Devices
availableCameras: Array<
Pick<MediaDeviceInfo, 'deviceId' | 'groupId' | 'kind' | 'label'>
>;
} & Omit<MediaDeviceSettings, 'availableCameras'>;
type PropsFunctionType = {
// Render props
renderUpdateDialog: (
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
) => JSX.Element;
// Other props
addCustomColor: (color: CustomColorType) => unknown;
closeSettings: () => unknown;
doDeleteAllData: () => unknown;
doneRendering: () => unknown;
editCustomColor: (colorId: string, color: CustomColorType) => unknown;
exportLocalBackup: () => Promise<BackupValidationResultType>;
getConversationsWithCustomColor: (
colorId: string
) => Promise<Array<ConversationType>>;
getConversationsWithCustomColor: (colorId: string) => Array<ConversationType>;
importLocalBackup: () => Promise<ValidateLocalBackupStructureResultType>;
makeSyncRequest: () => unknown;
onStartUpdate: () => unknown;
refreshCloudBackupStatus: () => void;
refreshBackupSubscriptionStatus: () => void;
removeCustomColor: (colorId: string) => unknown;
@ -199,7 +203,7 @@ type PropsFunctionType = {
onHideMenuBarChange: CheckboxChangeHandlerType;
onIncomingCallNotificationsChange: CheckboxChangeHandlerType;
onLastSyncTimeChange: (time: number) => unknown;
onLocaleChange: (locale: string | null) => void;
onLocaleChange: (locale: string | null | undefined) => void;
onMediaCameraPermissionsChange: CheckboxChangeHandlerType;
onMediaPermissionsChange: CheckboxChangeHandlerType;
onMessageAudioChange: CheckboxChangeHandlerType;
@ -284,13 +288,11 @@ export function Preferences({
backupFeatureEnabled,
backupSubscriptionStatus,
blockedCount,
closeSettings,
cloudBackupStatus,
customColors,
defaultConversationColor,
deviceName = '',
doDeleteAllData,
doneRendering,
editCustomColor,
emojiSkinToneDefault,
exportLocalBackup,
@ -313,6 +315,7 @@ export function Preferences({
hasMinimizeToSystemTray,
hasNotificationAttention,
hasNotifications,
hasPendingUpdate,
hasReadReceipts,
hasRelayCalls,
hasSpellCheck,
@ -325,14 +328,15 @@ export function Preferences({
initialSpellCheckSetting,
isAutoDownloadUpdatesSupported,
isAutoLaunchSupported,
isContentProtectionNeeded,
isContentProtectionSupported,
isHideMenuBarSupported,
isNotificationAttentionSupported,
isSyncSupported,
isSystemTraySupported,
isMinimizeToAndStartInSystemTraySupported,
isInternalUser,
isContentProtectionNeeded,
isContentProtectionSupported,
isUpdateDownloaded,
lastSyncTime,
makeSyncRequest,
notificationContent,
@ -377,6 +381,7 @@ export function Preferences({
refreshBackupSubscriptionStatus,
removeCustomColor,
removeCustomColorOnConversations,
renderUpdateDialog,
resetAllChatColors,
resetDefaultChatColor,
resolvedLocale,
@ -411,7 +416,7 @@ export function Preferences({
null
);
const [selectedLanguageLocale, setSelectedLanguageLocale] = useState<
string | null
string | null | undefined
>(localeOverride);
const [languageSearchInput, setLanguageSearchInput] = useState('');
const [toast, setToast] = useState<AnyToast | undefined>();
@ -432,6 +437,13 @@ export function Preferences({
setPage(Page.General);
}
let maybeUpdateDialog: JSX.Element | undefined;
if (hasPendingUpdate || isUpdateDownloaded) {
maybeUpdateDialog = renderUpdateDialog({
containerWidthBreakpoint: WidthBreakpoint.Wide,
});
}
useEffect(() => {
if (page === Page.Backups) {
refreshCloudBackupStatus();
@ -439,18 +451,6 @@ export function Preferences({
}
}, [page, refreshCloudBackupStatus, refreshBackupSubscriptionStatus]);
useEffect(() => {
doneRendering();
}, [doneRendering]);
useEscapeHandling(() => {
if (languageDialog != null) {
closeLanguageDialog();
} else {
closeSettings();
}
});
const onZoomSelectChange = useCallback(
(value: string) => {
const number = parseFloat(value);
@ -631,6 +631,7 @@ export function Preferences({
{isAutoLaunchSupported && (
<Checkbox
checked={hasAutoLaunch}
disabled={hasAutoLaunch === undefined}
label={i18n('icu:autoLaunchDescription')}
moduleClassName="Preferences__checkbox"
name="autoLaunch"
@ -650,6 +651,7 @@ export function Preferences({
<>
<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"
@ -658,7 +660,10 @@ export function Preferences({
{isMinimizeToAndStartInSystemTraySupported && (
<Checkbox
checked={hasMinimizeToAndStartInSystemTray}
disabled={!hasMinimizeToSystemTray}
disabled={
!hasMinimizeToSystemTray ||
hasMinimizeToAndStartInSystemTray === undefined
}
label={i18n(
'icu:SystemTraySetting__minimize-to-and-start-in-system-tray'
)}
@ -673,6 +678,7 @@ export function Preferences({
<SettingsRow title={i18n('icu:permissions')}>
<Checkbox
checked={hasMediaPermissions}
disabled={hasMediaPermissions === undefined}
label={i18n('icu:mediaPermissionsDescription')}
moduleClassName="Preferences__checkbox"
name="mediaPermissions"
@ -680,6 +686,7 @@ export function Preferences({
/>
<Checkbox
checked={hasMediaCameraPermissions ?? false}
disabled={hasMediaCameraPermissions === undefined}
label={i18n('icu:mediaCameraPermissionsDescription')}
moduleClassName="Preferences__checkbox"
name="mediaCameraPermissions"
@ -702,7 +709,10 @@ export function Preferences({
} else if (page === Page.Appearance) {
let zoomFactors = DEFAULT_ZOOM_FACTORS;
if (!zoomFactors.some(({ value }) => value === zoomFactor)) {
if (
isNumber(zoomFactor) &&
!zoomFactors.some(({ value }) => value === zoomFactor)
) {
zoomFactors = [
...zoomFactors,
{
@ -711,6 +721,13 @@ export function Preferences({
},
].sort((a, b) => a.value - b.value);
}
let localeText = '';
if (localeOverride !== undefined) {
localeText =
localeOverride != null
? getLocaleDisplayName(resolvedLocale, localeOverride)
: i18n('icu:Preferences__Language__SystemLanguage');
}
settings = (
<>
@ -728,12 +745,14 @@ export function Preferences({
className="Preferences__LanguageButton"
lang={localeOverride ?? resolvedLocale}
>
{localeOverride != null
? getLocaleDisplayName(resolvedLocale, localeOverride)
: i18n('icu:Preferences__Language__SystemLanguage')}
{localeText}
</span>
}
onClick={() => {
// We haven't loaded the user's setting yet
if (localeOverride === undefined) {
return;
}
setLanguageDialog(LanguageDialog.Selection);
}}
/>
@ -852,6 +871,7 @@ export function Preferences({
right={
<Select
id={themeSelectId}
disabled={themeSetting === undefined}
onChange={onThemeChange}
options={[
{
@ -898,8 +918,9 @@ export function Preferences({
right={
<Select
id={zoomSelectId}
disabled={zoomFactor === undefined}
onChange={onZoomSelectChange}
options={zoomFactors}
options={zoomFactor === undefined ? [] : zoomFactors}
value={zoomFactor}
/>
}
@ -909,7 +930,10 @@ export function Preferences({
);
} else if (page === Page.Chats) {
let spellCheckDirtyText: string | undefined;
if (initialSpellCheckSetting !== hasSpellCheck) {
if (
hasSpellCheck !== undefined &&
initialSpellCheckSetting !== hasSpellCheck
) {
spellCheckDirtyText = hasSpellCheck
? i18n('icu:spellCheckWillBeEnabled')
: i18n('icu:spellCheckWillBeDisabled');
@ -927,6 +951,7 @@ export function Preferences({
<SettingsRow title={i18n('icu:Preferences__button--chats')}>
<Checkbox
checked={hasSpellCheck}
disabled={hasSpellCheck === undefined}
description={spellCheckDirtyText}
label={i18n('icu:spellCheckDescription')}
moduleClassName="Preferences__checkbox"
@ -1390,6 +1415,7 @@ export function Preferences({
<SettingsRow title={i18n('icu:Preferences__Privacy__Application')}>
<Checkbox
checked={hasContentProtection}
disabled={hasContentProtection === undefined}
description={i18n(
'icu:Preferences__content-protection--description'
)}
@ -1811,6 +1837,13 @@ export function Preferences({
<div className="module-title-bar-drag-area" />
<div className="Preferences">
<div className="Preferences__page-selector">
<h1 className="Preferences__header">
{i18n('icu:Preferences--header')}
</h1>
{maybeUpdateDialog ? (
<div className="module-left-pane__dialogs">{maybeUpdateDialog}</div>
) : null}
<div className="Preferences__scroll-area">
<button
type="button"
className={classNames({
@ -1867,7 +1900,6 @@ export function Preferences({
>
{i18n('icu:Preferences__button--notifications')}
</button>
<button
type="button"
className={classNames({
@ -1880,7 +1912,6 @@ export function Preferences({
>
{i18n('icu:Preferences__button--privacy')}
</button>
<button
type="button"
className={classNames({
@ -1919,9 +1950,12 @@ export function Preferences({
</button>
) : null}
</div>
</div>
<div className="Preferences__settings-pane-spacer" />
<div className="Preferences__settings-pane" ref={settingsPaneRef}>
{settings}
</div>
<div className="Preferences__settings-pane-spacer" />
</div>
<ToastManager
OS="unused"

View file

@ -5,16 +5,13 @@ import type { BrowserWindow } from 'electron';
import { ipcMain as ipc, session } from 'electron';
import { EventEmitter } from 'events';
import * as log from '../logging/log';
import { userConfig } from '../../app/user_config';
import { ephemeralConfig } from '../../app/ephemeral_config';
import { installPermissionsHandler } from '../../app/permissions';
import { strictAssert } from '../util/assert';
import { explodePromise } from '../util/explodePromise';
import type {
IPCEventsValuesType,
IPCEventsCallbacksType,
} from '../util/createIPCEvents';
import type { EphemeralSettings, SettingsValuesType } from '../util/preload';
import type { EphemeralSettings } from '../util/preload';
const EPHEMERAL_NAME_MAP = new Map([
['spellCheck', 'spell-check'],
@ -24,18 +21,8 @@ const EPHEMERAL_NAME_MAP = new Map([
['contentProtection', 'contentProtection'],
]);
type ResponseQueueEntry = Readonly<{
resolve(value: unknown): void;
reject(error: Error): void;
}>;
type SettingChangeEventType<Key extends keyof SettingsValuesType> =
`change:${Key}`;
export class SettingsChannel extends EventEmitter {
#mainWindow?: BrowserWindow;
readonly #responseQueue = new Map<number, ResponseQueueEntry>();
#responseSeq = 0;
public setMainWindow(mainWindow: BrowserWindow | undefined): void {
this.#mainWindow = mainWindow;
@ -45,81 +32,16 @@ export class SettingsChannel extends EventEmitter {
return this.#mainWindow;
}
public openSettingsTab(): void {
if (!this.#mainWindow) {
log.warn('openSettingsTab: No mainWindow, cannot open settings tab');
return;
}
this.#mainWindow.webContents.send('open-settings-tab');
this.#mainWindow.show();
}
public install(): void {
this.#installSetting('deviceName', { setter: false });
this.#installSetting('phoneNumber', { setter: false });
// ChatColorPicker redux hookups
this.#installCallback('getCustomColors');
this.#installCallback('getConversationsWithCustomColor');
this.#installCallback('resetAllChatColors');
this.#installCallback('resetDefaultChatColor');
this.#installCallback('addCustomColor');
this.#installCallback('editCustomColor');
this.#installCallback('removeCustomColor');
this.#installCallback('removeCustomColorOnConversations');
this.#installCallback('setGlobalDefaultConversationColor');
this.#installCallback('getDefaultConversationColor');
// Various callbacks
this.#installCallback('deleteAllMyStories');
this.#installCallback('getAvailableIODevices');
this.#installCallback('isPrimary');
this.#installCallback('isInternalUser');
this.#installCallback('syncRequest');
this.#installCallback('setEmojiSkinToneDefault');
this.#installCallback('getEmojiSkinToneDefault');
this.#installCallback('exportLocalBackup');
this.#installCallback('importLocalBackup');
this.#installCallback('validateBackup');
// Backups
this.#installSetting('backupFeatureEnabled', { setter: false });
this.#installSetting('cloudBackupStatus', { setter: false });
this.#installSetting('backupSubscriptionStatus', { setter: false });
this.#installCallback('refreshCloudBackupStatus');
this.#installCallback('refreshBackupSubscriptionStatus');
// Getters only. These are set by the primary device
this.#installSetting('blockedCount', { setter: false });
this.#installSetting('linkPreviewSetting', { setter: false });
this.#installSetting('readReceiptSetting', { setter: false });
this.#installSetting('typingIndicatorSetting', { setter: false });
this.#installSetting('hideMenuBar');
this.#installSetting('notificationSetting');
this.#installSetting('notificationDrawAttention');
this.#installSetting('audioMessage');
this.#installSetting('audioNotification');
this.#installSetting('countMutedConversations');
this.#installSetting('sentMediaQualitySetting');
this.#installSetting('textFormatting');
this.#installSetting('autoConvertEmoji');
this.#installSetting('autoDownloadUpdate');
this.#installSetting('autoDownloadAttachment');
this.#installSetting('autoLaunch');
this.#installSetting('alwaysRelayCalls');
this.#installSetting('callRingtoneNotification');
this.#installSetting('callSystemNotification');
this.#installSetting('incomingCallNotification');
// Media settings
this.#installSetting('preferredAudioInputDevice');
this.#installSetting('preferredAudioOutputDevice');
this.#installSetting('preferredVideoInputDevice');
this.#installSetting('lastSyncTime');
this.#installSetting('universalExpireTimer');
this.#installSetting('hasStoriesDisabled');
this.#installSetting('zoomFactor');
this.#installSetting('phoneNumberDiscoverabilitySetting');
this.#installSetting('phoneNumberSharingSetting');
this.#installEphemeralSetting('themeSetting');
this.#installEphemeralSetting('systemTraySetting');
this.#installEphemeralSetting('localeOverride');
@ -156,113 +78,6 @@ export class SettingsChannel extends EventEmitter {
userConfig,
});
});
ipc.on('settings:response', (_event, seq, error, value) => {
const entry = this.#responseQueue.get(seq);
this.#responseQueue.delete(seq);
if (!entry) {
return;
}
const { resolve, reject } = entry;
if (error) {
reject(error);
} else {
resolve(value);
}
});
}
#waitForResponse<Value>(): { promise: Promise<Value>; seq: number } {
const seq = this.#responseSeq;
// eslint-disable-next-line no-bitwise
this.#responseSeq = (this.#responseSeq + 1) & 0x7fffffff;
const { promise, resolve, reject } = explodePromise<Value>();
this.#responseQueue.set(seq, { resolve, reject });
return { seq, promise };
}
public getSettingFromMainWindow<Name extends keyof IPCEventsValuesType>(
name: Name
): Promise<IPCEventsValuesType[Name]> {
const mainWindow = this.#mainWindow;
if (!mainWindow || !mainWindow.webContents) {
throw new Error('No main window');
}
const { seq, promise } = this.#waitForResponse<IPCEventsValuesType[Name]>();
mainWindow.webContents.send(`settings:get:${name}`, { seq });
return promise;
}
public setSettingInMainWindow<Name extends keyof IPCEventsValuesType>(
name: Name,
value: IPCEventsValuesType[Name]
): Promise<void> {
const mainWindow = this.#mainWindow;
if (!mainWindow || !mainWindow.webContents) {
throw new Error('No main window');
}
const { seq, promise } = this.#waitForResponse<void>();
mainWindow.webContents.send(`settings:set:${name}`, { seq, value });
return promise;
}
public invokeCallbackInMainWindow<Name extends keyof IPCEventsCallbacksType>(
name: Name,
args: ReadonlyArray<unknown>
): Promise<unknown> {
const mainWindow = this.#mainWindow;
if (!mainWindow || !mainWindow.webContents) {
throw new Error('Main window not found');
}
const { seq, promise } = this.#waitForResponse<unknown>();
mainWindow.webContents.send(`settings:call:${name}`, { seq, args });
return promise;
}
#installCallback<Name extends keyof IPCEventsCallbacksType>(
name: Name
): void {
ipc.handle(`settings:call:${name}`, async (_event, args) => {
return this.invokeCallbackInMainWindow(name, args);
});
}
#installSetting<Name extends keyof IPCEventsValuesType>(
name: Name,
{
getter = true,
setter = true,
}: { getter?: boolean; setter?: boolean } = {}
): void {
if (getter) {
ipc.handle(`settings:get:${name}`, async () => {
return this.getSettingFromMainWindow(name);
});
}
if (!setter) {
return;
}
ipc.handle(`settings:set:${name}`, async (_event, value) => {
await this.setSettingInMainWindow(name, value);
this.emit(`change:${name}`, value);
});
}
#installEphemeralSetting<Name extends keyof EphemeralSettings>(
@ -285,8 +100,6 @@ export class SettingsChannel extends EventEmitter {
);
ephemeralConfig.set(ephemeralName, value);
this.emit(`change:${name}`, value);
// Notify main to notify windows of preferences change. As for DB-backed
// settings, those are set by the renderer, and afterwards the renderer IPC sends
// to main the event 'preferences-changed'.
@ -313,12 +126,6 @@ export class SettingsChannel extends EventEmitter {
callback: (name: string) => void
): this;
public override on(
type: SettingChangeEventType<keyof SettingsValuesType>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (...args: Array<any>) => void
): this;
public override on(
type: string | symbol,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -337,12 +144,6 @@ export class SettingsChannel extends EventEmitter {
name: string
): boolean;
public override emit(
type: SettingChangeEventType<keyof SettingsValuesType>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: Array<any>
): boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public override emit(type: string | symbol, ...args: Array<any>): boolean {
return super.emit(type, ...args);

View file

@ -34,6 +34,7 @@ import {
import { drop } from '../util/drop';
import { getMessageById } from '../messages/getMessageById';
import { MessageModel } from '../models/messages';
import { areStoryViewReceiptsEnabled } from '../types/Stories';
const { deleteSentProtoRecipient, removeSyncTasks, removeSyncTaskById } =
DataWriter;
@ -398,7 +399,7 @@ const shouldDropReceipt = (
return !window.storage.get('read-receipt-setting');
case messageReceiptTypeSchema.Enum.View:
if (isStory(message)) {
return !window.Events.getStoryViewReceiptsEnabled();
return !areStoryViewReceiptsEnabled();
}
return !window.storage.get('read-receipt-setting');
default:

View file

@ -193,6 +193,7 @@ import { cleanupMessages } from '../util/cleanup';
import { MessageModel } from './messages';
import { applyNewAvatar } from '../groups';
import { safeSetTimeout } from '../util/timeout';
import { getTypingIndicatorSetting } from '../types/Util';
import { INITIAL_EXPIRE_TIMER_VERSION } from '../util/expirationTimer';
/* eslint-disable more/no-then */
@ -1125,7 +1126,7 @@ export class ConversationModel extends window.Backbone
bumpTyping(): void {
// We don't send typing messages if the setting is disabled
if (!window.Events.getTypingIndicatorSetting()) {
if (!getTypingIndicatorSetting()) {
return;
}

View file

@ -63,7 +63,7 @@ function _maybeGrabLinkPreview(
): void {
// Don't generate link previews if user has turned them off. When posting a
// story we should return minimal (url-only) link previews.
if (!window.Events.getLinkPreviewSetting() && mode === 'conversation') {
if (!LinkPreview.getLinkPreviewSetting() && mode === 'conversation') {
return;
}
@ -102,7 +102,7 @@ function _maybeGrabLinkPreview(
drop(
addLinkPreview(link, source, {
conversationId,
disableFetch: !window.Events.getLinkPreviewSetting(),
disableFetch: !LinkPreview.getLinkPreviewSetting(),
})
);
}

View file

@ -153,6 +153,8 @@ import { generateBackupsSubscriberData } from '../../util/backupSubscriptionData
import { getEnvironment, isTestEnvironment } from '../../environment';
import { calculateLightness } from '../../util/getHSL';
import { toDayOfWeekArray } from '../../types/NotificationProfile';
import { getLinkPreviewSetting } from '../../types/LinkPreview';
import { getTypingIndicatorSetting } from '../../types/Util';
const MAX_CONCURRENCY = 10;
@ -864,8 +866,8 @@ export class BackupExportStream extends Readable {
accountSettings: {
readReceipts: storage.get('read-receipt-setting'),
sealedSenderIndicators: storage.get('sealedSenderIndicators'),
typingIndicators: window.Events.getTypingIndicatorSetting(),
linkPreviews: window.Events.getLinkPreviewSetting(),
typingIndicators: getTypingIndicatorSetting(),
linkPreviews: getLinkPreviewSetting(),
notDiscoverableByPhoneNumber:
parsePhoneNumberDiscoverability(
storage.get('phoneNumberDiscoverability')

View file

@ -235,6 +235,38 @@ export type SetPresentingOptionsType = Readonly<{
callLinkRootKey?: string;
}>;
function getIncomingCallNotification(): boolean {
return window.storage.get('incoming-call-notification', true);
}
function getAlwaysRelayCalls(): boolean {
return window.storage.get('always-relay-calls', false);
}
function getPreferredAudioInputDevice(): AudioDevice | undefined {
return window.storage.get('preferred-audio-input-device');
}
async function setPreferredAudioInputDevice(
device: AudioDevice
): Promise<void> {
await window.storage.put('preferred-audio-input-device', device);
}
function getPreferredAudioOutputDevice(): AudioDevice | undefined {
return window.storage.get('preferred-audio-output-device');
}
async function setPreferredAudioOutputDevice(
device: AudioDevice
): Promise<void> {
await window.storage.put('preferred-audio-output-device', device);
}
function getPreferredVideoInputDevice(): string | undefined {
return window.storage.get('preferred-video-input-device');
}
async function setPreferredVideoInputDevice(device: string): Promise<void> {
await window.storage.put('preferred-video-input-device', device);
}
function truncateForLogging(name: string | undefined): string | undefined {
if (!name || name.length <= 4) {
return name;
@ -2653,7 +2685,7 @@ export class CallingClass {
const { availableCameras, availableMicrophones, availableSpeakers } =
await this.getAvailableIODevices();
const preferredMicrophone = window.Events.getPreferredAudioInputDevice();
const preferredMicrophone = getPreferredAudioInputDevice();
const selectedMicIndex = findBestMatchingAudioDeviceIndex(
{
available: availableMicrophones,
@ -2666,7 +2698,7 @@ export class CallingClass {
? availableMicrophones[selectedMicIndex]
: undefined;
const preferredSpeaker = window.Events.getPreferredAudioOutputDevice();
const preferredSpeaker = getPreferredAudioOutputDevice();
const selectedSpeakerIndex = findBestMatchingAudioDeviceIndex(
{
available: availableSpeakers,
@ -2679,7 +2711,7 @@ export class CallingClass {
? availableSpeakers[selectedSpeakerIndex]
: undefined;
const preferredCamera = window.Events.getPreferredVideoInputDevice();
const preferredCamera = getPreferredVideoInputDevice();
const selectedCamera = findBestMatchingCameraId(
availableCameras,
preferredCamera
@ -2701,7 +2733,7 @@ export class CallingClass {
device.index,
truncateForLogging(device.name)
);
void window.Events.setPreferredAudioInputDevice(device);
drop(setPreferredAudioInputDevice(device));
RingRTC.setAudioInput(device.index);
}
@ -2711,7 +2743,7 @@ export class CallingClass {
device.index,
truncateForLogging(device.name)
);
void window.Events.setPreferredAudioOutputDevice(device);
drop(setPreferredAudioOutputDevice(device));
RingRTC.setAudioOutput(device.index);
}
@ -2749,7 +2781,7 @@ export class CallingClass {
async setPreferredCamera(device: string): Promise<void> {
log.info('MediaDevice: setPreferredCamera', device);
void window.Events.setPreferredVideoInputDevice(device);
drop(setPreferredVideoInputDevice(device));
await this.#videoCapturer.setPreferredDevice(device);
}
@ -2760,7 +2792,7 @@ export class CallingClass {
const logId = `CallingClass.handleCallingMessage(${envelope.timestamp})`;
log.info(logId);
const enableIncomingCalls = window.Events.getIncomingCallNotification();
const enableIncomingCalls = getIncomingCallNotification();
if (callingMessage.offer && !enableIncomingCalls) {
// Drop offers silently if incoming call notifications are disabled.
log.info(`${logId}: Incoming calls are disabled, ignoring call offer.`);
@ -3078,7 +3110,7 @@ export class CallingClass {
RingRTC.cancelGroupRing(groupIdBytes, ringId, null);
} else if (this.#areAnyCallsActiveOrRinging()) {
RingRTC.cancelGroupRing(groupIdBytes, ringId, RingCancelReason.Busy);
} else if (window.Events.getIncomingCallNotification()) {
} else if (getIncomingCallNotification()) {
shouldRing = true;
} else {
log.info(
@ -3633,7 +3665,7 @@ export class CallingClass {
return false;
}
const shouldRelayCalls = window.Events.getAlwaysRelayCalls();
const shouldRelayCalls = getAlwaysRelayCalls();
// If the peer is not a Signal Connection, force IP hiding.
const isContactUntrusted = !isSignalConnection(conversation.attributes);

View file

@ -84,6 +84,12 @@ import {
generateBackupsSubscriberData,
saveBackupsSubscriberData,
} from '../util/backupSubscriptionData';
import { getLinkPreviewSetting } from '../types/LinkPreview';
import {
getReadReceiptSetting,
getSealedSenderIndicatorSetting,
getTypingIndicatorSetting,
} from '../types/Util';
const MY_STORY_BYTES = uuidToBytes(MY_STORY_ID);
@ -338,14 +344,10 @@ export function toAccountRecord(
accountRecord.noteToSelfMarkedUnread = Boolean(
conversation.get('markedUnread')
);
accountRecord.readReceipts = Boolean(window.Events.getReadReceiptSetting());
accountRecord.sealedSenderIndicators = Boolean(
window.storage.get('sealedSenderIndicators')
);
accountRecord.typingIndicators = Boolean(
window.Events.getTypingIndicatorSetting()
);
accountRecord.linkPreviews = Boolean(window.Events.getLinkPreviewSetting());
accountRecord.readReceipts = getReadReceiptSetting();
accountRecord.sealedSenderIndicators = getSealedSenderIndicatorSetting();
accountRecord.typingIndicators = getTypingIndicatorSetting();
accountRecord.linkPreviews = getLinkPreviewSetting();
const preferContactAvatars = window.storage.get('preferContactAvatars');
if (preferContactAvatars !== undefined) {

View file

@ -1,6 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export function showSettings(): void {
window.IPC.showSettings();
}

View file

@ -21,6 +21,7 @@ import { actions as items } from './ducks/items';
import { actions as lightbox } from './ducks/lightbox';
import { actions as linkPreviews } from './ducks/linkPreviews';
import { actions as mediaGallery } from './ducks/mediaGallery';
import { actions as nav } from './ducks/nav';
import { actions as network } from './ducks/network';
import { actions as notificationProfiles } from './ducks/notificationProfiles';
import { actions as safetyNumber } from './ducks/safetyNumber';
@ -55,6 +56,7 @@ export const actionCreators: ReduxActions = {
lightbox,
linkPreviews,
mediaGallery,
nav,
network,
notificationProfiles,
safetyNumber,

View file

@ -11,6 +11,7 @@ export enum NavTab {
Chats = 'Chats',
Calls = 'Calls',
Stories = 'Stories',
Settings = 'Settings',
}
// State

View file

@ -29,7 +29,11 @@ import { DataReader, DataWriter } from '../../sql/Client';
import { ReadStatus } from '../../messages/MessageReadStatus';
import { SendStatus } from '../../messages/MessageSendState';
import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
import {
areStoryViewReceiptsEnabled,
StoryViewDirectionType,
StoryViewModeType,
} from '../../types/Stories';
import { assertDev, strictAssert } from '../../util/assert';
import { drop } from '../../util/drop';
import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUntilConversationsAreVerified';
@ -51,6 +55,7 @@ import {
getStories,
getStoryDownloadableAttachment,
} from '../selectors/stories';
import { setStoriesDisabled as utilSetStoriesDisabled } from '../../util/stories';
import { getStoryDataFromMessageAttributes } from '../../services/storyLoader';
import { isGroup } from '../../util/whatTypeOfConversation';
import { isNotNil } from '../../util/isNotNil';
@ -443,10 +448,7 @@ function markStoryRead(
drop(viewSyncJobQueue.add({ viewSyncs }));
}
if (
!isSignalOnboardingStory &&
window.Events.getStoryViewReceiptsEnabled()
) {
if (!isSignalOnboardingStory && areStoryViewReceiptsEnabled()) {
drop(
conversationJobQueue.add({
type: conversationQueueJobEnum.enum.Receipts,
@ -1380,7 +1382,7 @@ function setStoriesDisabled(
value: boolean
): ThunkAction<void, RootStateType, unknown, never> {
return async () => {
await window.Events.setHasStoriesDisabled(value);
await utilSetStoriesDisabled(value);
};
}

View file

@ -82,6 +82,7 @@ export function initializeRedux(data: ReduxInitData): void {
actionCreators.mediaGallery,
store.dispatch
),
nav: bindActionCreators(actionCreators.nav, store.dispatch),
network: bindActionCreators(actionCreators.network, store.dispatch),
notificationProfiles: bindActionCreators(
actionCreators.notificationProfiles,

View file

@ -63,6 +63,9 @@ import { getActiveProfile } from '../selectors/notificationProfiles';
function renderDeviceSelection(): JSX.Element {
return <SmartCallingDeviceSelection />;
}
function getCallSystemNotification() {
return window.storage.get('call-system-notification', true);
}
const getGroupCallVideoFrameSource =
callingService.getGroupCallVideoFrameSource.bind(callingService);
@ -74,7 +77,7 @@ async function notifyForCall(
): Promise<void> {
const shouldNotify =
!window.SignalContext.activeWindowService.isActive() &&
window.Events.getCallSystemNotification();
getCallSystemNotification();
if (!shouldNotify) {
return;
}

View file

@ -63,7 +63,7 @@ export const SmartChatColorPicker = memo(function SmartChatColorPicker({
};
const getConversationsWithCustomColor = useCallback(
async (colorId: string): Promise<Array<ConversationType>> => {
(colorId: string): Array<ConversationType> => {
return conversationWithCustomColorSelector(colorId);
},
[conversationWithCustomColorSelector]

View file

@ -20,6 +20,7 @@ import {
getInboxEnvelopeTimestamp,
getInboxFirstEnvelopeTimestamp,
} from '../selectors/inbox';
import { SmartPreferences } from './Preferences';
function renderChatsTab() {
return <SmartChatsTab />;
@ -41,6 +42,10 @@ function renderStoriesTab() {
return <SmartStoriesTab />;
}
function renderSettingsTab() {
return <SmartPreferences />;
}
export const SmartInbox = memo(function SmartInbox(): JSX.Element {
const i18n = useSelector(getIntl);
const isCustomizingPreferredReactions = useSelector(
@ -70,6 +75,7 @@ export const SmartInbox = memo(function SmartInbox(): JSX.Element {
}
renderNavTabs={renderNavTabs}
renderStoriesTab={renderStoriesTab}
renderSettingsTab={renderSettingsTab}
/>
);
});

View file

@ -15,9 +15,7 @@ import {
getHasAnyFailedStorySends,
getStoriesNotificationCount,
} from '../selectors/stories';
import { showSettings } from '../../shims/Whisper';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useUpdatesActions } from '../ducks/updates';
import { getStoriesEnabled } from '../selectors/items';
import { getSelectedNavTab } from '../selectors/nav';
import type { NavTab } from '../ducks/nav';
@ -31,6 +29,7 @@ export type SmartNavTabsProps = Readonly<{
renderCallsTab: () => ReactNode;
renderChatsTab: () => ReactNode;
renderStoriesTab: () => ReactNode;
renderSettingsTab: () => ReactNode;
}>;
export const SmartNavTabs = memo(function SmartNavTabs({
@ -39,6 +38,7 @@ export const SmartNavTabs = memo(function SmartNavTabs({
renderCallsTab,
renderChatsTab,
renderStoriesTab,
renderSettingsTab,
}: SmartNavTabsProps): JSX.Element {
const i18n = useSelector(getIntl);
const selectedNavTab = useSelector(getSelectedNavTab);
@ -54,7 +54,6 @@ export const SmartNavTabs = memo(function SmartNavTabs({
const hasPendingUpdate = useSelector(getHasPendingUpdate);
const { toggleProfileEditor } = useGlobalModalActions();
const { startUpdate } = useUpdatesActions();
const onNavTabSelected = useCallback(
(tab: NavTab) => {
@ -75,14 +74,13 @@ export const SmartNavTabs = memo(function SmartNavTabs({
i18n={i18n}
me={me}
navTabsCollapsed={navTabsCollapsed}
onShowSettings={showSettings}
onStartUpdate={startUpdate}
onNavTabSelected={onNavTabSelected}
onToggleNavTabsCollapse={onToggleNavTabsCollapse}
onToggleProfileEditor={toggleProfileEditor}
renderCallsTab={renderCallsTab}
renderChatsTab={renderChatsTab}
renderStoriesTab={renderStoriesTab}
renderSettingsTab={renderSettingsTab}
selectedNavTab={selectedNavTab}
storiesEnabled={storiesEnabled}
theme={theme}

View file

@ -0,0 +1,705 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { StrictMode, useEffect } from 'react';
import { useSelector } from 'react-redux';
import type { AudioDevice } from '@signalapp/ringrtc';
import { useItemsActions } from '../ducks/items';
import { useConversationsActions } from '../ducks/conversations';
import { getConversationsWithCustomColorSelector } from '../selectors/conversations';
import { getCustomColors, getItems } from '../selectors/items';
import { DEFAULT_AUTO_DOWNLOAD_ATTACHMENT } from '../../textsecure/Storage';
import { DEFAULT_CONVERSATION_COLOR } from '../../types/Colors';
import { isBackupFeatureEnabledForRedux } from '../../util/isBackupEnabled';
import { format } from '../../types/PhoneNumber';
import { getIntl, getUserDeviceId, getUserNumber } from '../selectors/user';
import { EmojiSkinTone } from '../../components/fun/data/emojis';
import { renderClearingDataView } from '../../shims/renderClearingDataView';
import OS from '../../util/os/osPreload';
import { themeChanged } from '../../shims/themeChanged';
import * as Settings from '../../types/Settings';
import * as universalExpireTimerUtil from '../../util/universalExpireTimer';
import {
parseSystemTraySetting,
shouldMinimizeToSystemTray,
SystemTraySetting,
} from '../../types/SystemTraySetting';
import { calling } from '../../services/calling';
import { drop } from '../../util/drop';
import { assertDev, strictAssert } from '../../util/assert';
import { backupsService } from '../../services/backups';
import { DurationInSeconds } from '../../util/durations/duration-in-seconds';
import { PhoneNumberDiscoverability } from '../../util/phoneNumberDiscoverability';
import { PhoneNumberSharingMode } from '../../util/phoneNumberSharingMode';
import { writeProfile } from '../../services/writeProfile';
import { getConversation } from '../../util/getConversation';
import { waitForEvent } from '../../shims/events';
import { MINUTE } from '../../util/durations';
import { sendSyncRequests } from '../../textsecure/syncRequests';
import { SmartUpdateDialog } from './UpdateDialog';
import { Preferences } from '../../components/Preferences';
import type { StorageAccessType, ZoomFactorType } from '../../types/Storage';
import type { ThemeType } from '../../util/preload';
import type { WidthBreakpoint } from '../../components/_util';
import { useUpdatesActions } from '../ducks/updates';
import {
getHasPendingUpdate,
isUpdateDownloaded as getIsUpdateDownloaded,
} from '../selectors/updates';
const DEFAULT_NOTIFICATION_SETTING = 'message';
function renderUpdateDialog(
props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
): JSX.Element {
return <SmartUpdateDialog {...props} disableDismiss />;
}
function getSystemTraySettingValues(
systemTraySetting: SystemTraySetting | undefined
): {
hasMinimizeToAndStartInSystemTray: boolean | undefined;
hasMinimizeToSystemTray: boolean | undefined;
} {
if (systemTraySetting === undefined) {
return {
hasMinimizeToAndStartInSystemTray: undefined,
hasMinimizeToSystemTray: undefined,
};
}
const parsedSystemTraySetting = parseSystemTraySetting(systemTraySetting);
const hasMinimizeToAndStartInSystemTray =
parsedSystemTraySetting ===
SystemTraySetting.MinimizeToAndStartInSystemTray;
const hasMinimizeToSystemTray = shouldMinimizeToSystemTray(
parsedSystemTraySetting
);
return {
hasMinimizeToAndStartInSystemTray,
hasMinimizeToSystemTray,
};
}
export function SmartPreferences(): JSX.Element {
const {
addCustomColor,
editCustomColor,
putItem,
removeCustomColor,
resetDefaultChatColor,
setEmojiSkinToneDefault: onEmojiSkinToneDefaultChange,
setGlobalDefaultConversationColor,
} = useItemsActions();
const { removeCustomColorOnConversations, resetAllChatColors } =
useConversationsActions();
const { startUpdate } = useUpdatesActions();
// Selectors
const customColors = useSelector(getCustomColors) ?? {};
const getConversationsWithCustomColor = useSelector(
getConversationsWithCustomColorSelector
);
const items = useSelector(getItems);
const i18n = useSelector(getIntl);
const hasPendingUpdate = useSelector(getHasPendingUpdate);
const isUpdateDownloaded = useSelector(getIsUpdateDownloaded);
// The weird ones
const makeSyncRequest = async () => {
const contactSyncComplete = waitForEvent(
'contactSync:complete',
5 * MINUTE
);
return Promise.all([sendSyncRequests(), contactSyncComplete]);
};
const universalExpireTimer = universalExpireTimerUtil.getForRedux(items);
const onUniversalExpireTimerChange = async (newValue: number) => {
await universalExpireTimerUtil.set(DurationInSeconds.fromMillis(newValue));
// Update account in Storage Service
const account = window.ConversationController.getOurConversationOrThrow();
account.captureChange('universalExpireTimer');
// Add a notification to the currently open conversation
const state = window.reduxStore.getState();
const selectedId = state.conversations.selectedConversationId;
if (selectedId) {
const conversation = window.ConversationController.get(selectedId);
assertDev(conversation, "Conversation wasn't found");
await conversation.updateLastMessage();
}
};
const validateBackup = () => backupsService._internalValidate();
const exportLocalBackup = () => backupsService._internalExportLocalBackup();
const importLocalBackup = () =>
backupsService._internalStageLocalBackupForImport();
const doDeleteAllData = () => renderClearingDataView();
const refreshCloudBackupStatus =
window.Signal.Services.backups.throttledFetchCloudBackupStatus;
const refreshBackupSubscriptionStatus =
window.Signal.Services.backups.throttledFetchSubscriptionStatus;
// Context - these don't change per startup
const version = window.SignalContext.getVersion();
const availableLocales = window.SignalContext.getI18nAvailableLocales();
const resolvedLocale = window.SignalContext.getI18nLocale();
const preferredSystemLocales =
window.SignalContext.getPreferredSystemLocales();
const initialSpellCheckSetting =
window.SignalContext.config.appStartInitialSpellcheckSetting;
// Settings - these capabilities are unchanging
const isAutoDownloadUpdatesSupported =
Settings.isAutoDownloadUpdatesSupported(OS, version);
const isAutoLaunchSupported = Settings.isAutoLaunchSupported(OS);
const isHideMenuBarSupported = Settings.isHideMenuBarSupported(OS);
const isMinimizeToAndStartInSystemTraySupported =
Settings.isMinimizeToAndStartInSystemTraySupported(OS);
const isNotificationAttentionSupported =
Settings.isDrawAttentionSupported(OS);
const isSystemTraySupported = Settings.isSystemTraySupported(OS);
// Textsecure - user can change number and change this device's name
const phoneNumber = format(useSelector(getUserNumber) ?? '', {});
const isPrimary = useSelector(getUserDeviceId) === 1;
const isSyncSupported = !isPrimary;
const [deviceName, setDeviceName] = React.useState(
window.textsecure.storage.user.getDeviceName()
);
useEffect(() => {
let canceled = false;
const onDeviceNameChanged = () => {
const value = window.textsecure.storage.user.getDeviceName();
if (canceled) {
return;
}
setDeviceName(value);
};
window.Whisper.events.on('deviceNameChanged', onDeviceNameChanged);
return () => {
canceled = true;
window.Whisper.events.off('deviceNameChanged', onDeviceNameChanged);
};
}, []);
// RingRTC - the list of devices is unchanging while settings window is open
// The select boxes for devices are disabled while these arrays have zero length
const [availableCameras, setAvailableCameras] = React.useState<
Array<MediaDeviceInfo>
>([]);
const [availableMicrophones, setAvailableMicrophones] = React.useState<
Array<AudioDevice>
>([]);
const [availableSpeakers, setAvailableSpeakers] = React.useState<
Array<AudioDevice>
>([]);
useEffect(() => {
let canceled = false;
const loadDevices = async () => {
const {
availableCameras: cameras,
availableMicrophones: microphones,
availableSpeakers: speakers,
} = await calling.getAvailableIODevices();
if (canceled) {
return;
}
setAvailableCameras(cameras);
setAvailableMicrophones(microphones);
setAvailableSpeakers(speakers);
};
drop(loadDevices());
return () => {
canceled = true;
};
}, []);
// Ephemeral settings, via async IPC, all can be modiified
const [localeOverride, setLocaleOverride] = React.useState<string | null>();
const [systemTraySettings, setSystemTraySettings] =
React.useState<SystemTraySetting>();
const [hasContentProtection, setContentProtection] =
React.useState<boolean>();
const [hasSpellCheck, setSpellCheck] = React.useState<boolean>();
const [themeSetting, setThemeSetting] = React.useState<ThemeType>();
useEffect(() => {
let canceled = false;
const loadOverride = async () => {
const value = await window.Events.getLocaleOverride();
if (canceled) {
return;
}
setLocaleOverride(value);
};
drop(loadOverride());
const loadSystemTraySettings = async () => {
const value = await window.Events.getSystemTraySetting();
if (canceled) {
return;
}
setSystemTraySettings(value);
};
drop(loadSystemTraySettings());
const loadSpellCheck = async () => {
const value = await window.Events.getSpellCheck();
if (canceled) {
return;
}
setSpellCheck(value);
};
drop(loadSpellCheck());
const loadContentProtection = async () => {
const value = await window.Events.getContentProtection();
setContentProtection(value);
};
drop(loadContentProtection());
const loadThemeSetting = async () => {
const value = await window.Events.getThemeSetting();
if (canceled) {
return;
}
setThemeSetting(value);
};
drop(loadThemeSetting());
return () => {
canceled = true;
};
}, []);
const onLocaleChange = async (locale: string | null | undefined) => {
setLocaleOverride(locale);
await window.Events.setLocaleOverride(locale ?? null);
};
const { hasMinimizeToAndStartInSystemTray, hasMinimizeToSystemTray } =
getSystemTraySettingValues(systemTraySettings);
const onMinimizeToSystemTrayChange = async (value: boolean) => {
const newSetting = value
? SystemTraySetting.MinimizeToSystemTray
: SystemTraySetting.DoNotUseSystemTray;
setSystemTraySettings(newSetting);
await window.Events.setSystemTraySetting(newSetting);
};
const onMinimizeToAndStartInSystemTrayChange = async (value: boolean) => {
const newSetting = value
? SystemTraySetting.MinimizeToAndStartInSystemTray
: SystemTraySetting.MinimizeToSystemTray;
setSystemTraySettings(newSetting);
await window.Events.setSystemTraySetting(newSetting);
};
const onSpellCheckChange = async (value: boolean) => {
setSpellCheck(value);
await window.Events.setSpellCheck(value);
};
const onContentProtectionChange = async (value: boolean) => {
setContentProtection(value);
await window.Events.setContentProtection(value);
};
const onThemeChange = (value: ThemeType) => {
setThemeSetting(value);
drop(window.Events.setThemeSetting(value));
drop(themeChanged());
};
// Async IPC for electron configuration, all can be modified
const [hasAutoLaunch, setAutoLaunch] = React.useState<boolean>();
const [hasMediaCameraPermissions, setMediaCameraPermissions] =
React.useState<boolean>();
const [hasMediaPermissions, setMediaPermissions] = React.useState<boolean>();
const [zoomFactor, setZoomFactor] = React.useState<ZoomFactorType>();
useEffect(() => {
let canceled = false;
const loadAutoLaunch = async () => {
const value = await window.Events.getAutoLaunch();
if (canceled) {
return;
}
setAutoLaunch(value);
};
drop(loadAutoLaunch());
const loadMediaCameraPermissions = async () => {
const value = await window.Events.getMediaCameraPermissions();
if (canceled) {
return;
}
setMediaCameraPermissions(value);
};
drop(loadMediaCameraPermissions());
const loadMediaPermissions = async () => {
const value = await window.Events.getMediaPermissions();
if (canceled) {
return;
}
setMediaPermissions(value);
};
drop(loadMediaPermissions());
const loadZoomFactor = async () => {
const value = await window.Events.getZoomFactor();
if (canceled) {
return;
}
setZoomFactor(value);
};
drop(loadZoomFactor());
// We need to be ready for zoom changes from the keyboard
const updateZoomFactorFromIpc = (value: ZoomFactorType) => {
if (canceled) {
return;
}
setZoomFactor(value);
};
window.Events.onZoomFactorChange(updateZoomFactorFromIpc);
return () => {
canceled = true;
window.Events.offZoomFactorChange(updateZoomFactorFromIpc);
};
}, []);
const onAutoLaunchChange = async (value: boolean) => {
setAutoLaunch(value);
await window.Events.setAutoLaunch(value);
};
const onZoomFactorChange = async (value: ZoomFactorType) => {
setZoomFactor(value);
await window.Events.setZoomFactor(value);
};
const onMediaCameraPermissionsChange = async (value: boolean) => {
setMediaCameraPermissions(value);
await window.IPC.setMediaCameraPermissions(value);
};
const onMediaPermissionsChange = async (value: boolean) => {
setMediaPermissions(value);
await window.IPC.setMediaPermissions(value);
};
// Simple, one-way items
const { backupSubscriptionStatus, cloudBackupStatus } = items;
const defaultConversationColor =
items.defaultConversationColor || DEFAULT_CONVERSATION_COLOR;
const hasLinkPreviews = items.linkPreviews ?? false;
const hasReadReceipts = items['read-receipt-setting'] ?? false;
const hasTypingIndicators = items.typingIndicators ?? false;
const blockedCount =
(items['blocked-groups']?.length ?? 0) +
(items['blocked-uuids']?.length ?? 0);
const emojiSkinToneDefault = items.emojiSkinToneDefault ?? EmojiSkinTone.None;
const isInternalUser =
items.remoteConfig?.['desktop.internalUser']?.enabled ?? false;
const isContentProtectionSupported =
Settings.isContentProtectionSupported(OS);
const isContentProtectionNeeded = Settings.isContentProtectionNeeded(OS);
const backupFeatureEnabled = isBackupFeatureEnabledForRedux(
items.remoteConfig
);
// Two-way items
function createItemsAccess<K extends keyof StorageAccessType>(
key: K,
defaultValue: StorageAccessType[K],
callback?: (value: StorageAccessType[K]) => void
): [StorageAccessType[K], (value: StorageAccessType[K]) => void] {
const value = items[key] ?? defaultValue;
const setter = (newValue: StorageAccessType[K]) => {
putItem(key, newValue);
callback?.(newValue);
};
return [value, setter];
}
const [autoDownloadAttachment, onAutoDownloadAttachmentChange] =
createItemsAccess(
'auto-download-attachment',
DEFAULT_AUTO_DOWNLOAD_ATTACHMENT
);
const [hasAudioNotifications, onAudioNotificationsChange] = createItemsAccess(
'audio-notification',
false
);
const [hasAutoConvertEmoji, onAutoConvertEmojiChange] = createItemsAccess(
'autoConvertEmoji',
true
);
const [hasAutoDownloadUpdate, onAutoDownloadUpdateChange] = createItemsAccess(
'auto-download-update',
true
);
const [hasCallNotifications, onCallNotificationsChange] = createItemsAccess(
'call-system-notification',
true
);
const [hasIncomingCallNotifications, onIncomingCallNotificationsChange] =
createItemsAccess('incoming-call-notification', true);
const [hasCallRingtoneNotification, onCallRingtoneNotificationChange] =
createItemsAccess('call-ringtone-notification', true);
const [hasCountMutedConversations, onCountMutedConversationsChange] =
createItemsAccess('badge-count-muted-conversations', false, () => {
window.Whisper.events.trigger('updateUnreadCount');
});
const [hasHideMenuBar, onHideMenuBarChange] = createItemsAccess(
'hide-menu-bar',
false,
value => {
window.IPC.setAutoHideMenuBar(value);
window.IPC.setMenuBarVisibility(!value);
}
);
const [hasMessageAudio, onMessageAudioChange] = createItemsAccess(
'audioMessage',
false
);
const [hasNotificationAttention, onNotificationAttentionChange] =
createItemsAccess('notification-draw-attention', false);
const [notificationContent, onNotificationContentChange] = createItemsAccess(
'notification-setting',
'message'
);
const hasNotifications = notificationContent !== 'off';
const onNotificationsChange = (value: boolean) => {
putItem(
'notification-setting',
value ? DEFAULT_NOTIFICATION_SETTING : 'off'
);
};
const [hasRelayCalls, onRelayCallsChange] = createItemsAccess(
'always-relay-calls',
false
);
const [hasStoriesDisabled, onHasStoriesDisabledChanged] = createItemsAccess(
'hasStoriesDisabled',
false,
value => {
const account = window.ConversationController.getOurConversationOrThrow();
account.captureChange('hasStoriesDisabled');
window.textsecure.server?.onHasStoriesDisabledChange(value);
}
);
const [hasTextFormatting, onTextFormattingChange] = createItemsAccess(
'textFormatting',
true
);
const [lastSyncTime, onLastSyncTimeChange] = createItemsAccess(
'synced_at',
undefined
);
const [selectedCamera, onSelectedCameraChange] = createItemsAccess(
'preferred-video-input-device',
undefined
);
const [selectedMicrophone, onSelectedMicrophoneChange] = createItemsAccess(
'preferred-audio-input-device',
undefined
);
const [selectedSpeaker, onSelectedSpeakerChange] = createItemsAccess(
'preferred-audio-output-device',
undefined
);
const [sentMediaQualitySetting, onSentMediaQualityChange] = createItemsAccess(
'sent-media-quality',
'standard'
);
const [whoCanFindMe, onWhoCanFindMeChange] = createItemsAccess(
'phoneNumberDiscoverability',
PhoneNumberDiscoverability.NotDiscoverable,
async (newValue: PhoneNumberDiscoverability) => {
strictAssert(window.textsecure.server, 'WebAPI must be available');
await window.textsecure.server.setPhoneNumberDiscoverability(
newValue === PhoneNumberDiscoverability.Discoverable
);
const account = window.ConversationController.getOurConversationOrThrow();
account.captureChange('phoneNumberDiscoverability');
}
);
const [whoCanSeeMe, onWhoCanSeeMeChange] = createItemsAccess(
'phoneNumberSharingMode',
PhoneNumberSharingMode.Nobody,
async (newValue: PhoneNumberSharingMode) => {
const account = window.ConversationController.getOurConversationOrThrow();
if (newValue === PhoneNumberSharingMode.Everybody) {
onWhoCanFindMeChange(PhoneNumberDiscoverability.Discoverable);
}
account.captureChange('phoneNumberSharingMode');
// Write profile after updating storage so that the write has up-to-date
// information.
await writeProfile(getConversation(account), {
keepAvatar: true,
});
}
);
return (
<StrictMode>
<Preferences
addCustomColor={addCustomColor}
autoDownloadAttachment={autoDownloadAttachment}
availableCameras={availableCameras}
availableLocales={availableLocales}
availableMicrophones={availableMicrophones}
availableSpeakers={availableSpeakers}
backupFeatureEnabled={backupFeatureEnabled}
backupSubscriptionStatus={backupSubscriptionStatus}
blockedCount={blockedCount}
cloudBackupStatus={cloudBackupStatus}
customColors={customColors}
defaultConversationColor={defaultConversationColor}
deviceName={deviceName}
emojiSkinToneDefault={emojiSkinToneDefault}
exportLocalBackup={exportLocalBackup}
phoneNumber={phoneNumber}
doDeleteAllData={doDeleteAllData}
editCustomColor={editCustomColor}
getConversationsWithCustomColor={getConversationsWithCustomColor}
hasAudioNotifications={hasAudioNotifications}
hasAutoConvertEmoji={hasAutoConvertEmoji}
hasAutoDownloadUpdate={hasAutoDownloadUpdate}
hasAutoLaunch={hasAutoLaunch}
hasCallNotifications={hasCallNotifications}
hasCallRingtoneNotification={hasCallRingtoneNotification}
hasContentProtection={hasContentProtection}
hasCountMutedConversations={hasCountMutedConversations}
hasHideMenuBar={hasHideMenuBar}
hasIncomingCallNotifications={hasIncomingCallNotifications}
hasLinkPreviews={hasLinkPreviews}
hasMediaCameraPermissions={hasMediaCameraPermissions}
hasMediaPermissions={hasMediaPermissions}
hasMessageAudio={hasMessageAudio}
hasMinimizeToAndStartInSystemTray={hasMinimizeToAndStartInSystemTray}
hasMinimizeToSystemTray={hasMinimizeToSystemTray}
hasNotificationAttention={hasNotificationAttention}
hasNotifications={hasNotifications}
hasPendingUpdate={hasPendingUpdate}
hasReadReceipts={hasReadReceipts}
hasRelayCalls={hasRelayCalls}
hasSpellCheck={hasSpellCheck}
hasStoriesDisabled={hasStoriesDisabled}
hasTextFormatting={hasTextFormatting}
hasTypingIndicators={hasTypingIndicators}
i18n={i18n}
importLocalBackup={importLocalBackup}
initialSpellCheckSetting={initialSpellCheckSetting}
isAutoDownloadUpdatesSupported={isAutoDownloadUpdatesSupported}
isAutoLaunchSupported={isAutoLaunchSupported}
isContentProtectionNeeded={isContentProtectionNeeded}
isContentProtectionSupported={isContentProtectionSupported}
isHideMenuBarSupported={isHideMenuBarSupported}
isMinimizeToAndStartInSystemTraySupported={
isMinimizeToAndStartInSystemTraySupported
}
isNotificationAttentionSupported={isNotificationAttentionSupported}
isSyncSupported={isSyncSupported}
isSystemTraySupported={isSystemTraySupported}
isInternalUser={isInternalUser}
isUpdateDownloaded={isUpdateDownloaded}
lastSyncTime={lastSyncTime}
localeOverride={localeOverride}
makeSyncRequest={makeSyncRequest}
notificationContent={notificationContent}
onAudioNotificationsChange={onAudioNotificationsChange}
onAutoConvertEmojiChange={onAutoConvertEmojiChange}
onAutoDownloadAttachmentChange={onAutoDownloadAttachmentChange}
onAutoDownloadUpdateChange={onAutoDownloadUpdateChange}
onAutoLaunchChange={onAutoLaunchChange}
onCallNotificationsChange={onCallNotificationsChange}
onCallRingtoneNotificationChange={onCallRingtoneNotificationChange}
onContentProtectionChange={onContentProtectionChange}
onCountMutedConversationsChange={onCountMutedConversationsChange}
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
onHasStoriesDisabledChanged={onHasStoriesDisabledChanged}
onHideMenuBarChange={onHideMenuBarChange}
onIncomingCallNotificationsChange={onIncomingCallNotificationsChange}
onLastSyncTimeChange={onLastSyncTimeChange}
onLocaleChange={onLocaleChange}
onMediaCameraPermissionsChange={onMediaCameraPermissionsChange}
onMediaPermissionsChange={onMediaPermissionsChange}
onMessageAudioChange={onMessageAudioChange}
onMinimizeToAndStartInSystemTrayChange={
onMinimizeToAndStartInSystemTrayChange
}
onMinimizeToSystemTrayChange={onMinimizeToSystemTrayChange}
onNotificationAttentionChange={onNotificationAttentionChange}
onNotificationContentChange={onNotificationContentChange}
onNotificationsChange={onNotificationsChange}
onStartUpdate={startUpdate}
onRelayCallsChange={onRelayCallsChange}
onSelectedCameraChange={onSelectedCameraChange}
onSelectedMicrophoneChange={onSelectedMicrophoneChange}
onSelectedSpeakerChange={onSelectedSpeakerChange}
onSentMediaQualityChange={onSentMediaQualityChange}
onSpellCheckChange={onSpellCheckChange}
onTextFormattingChange={onTextFormattingChange}
onThemeChange={onThemeChange}
onUniversalExpireTimerChange={onUniversalExpireTimerChange}
onWhoCanFindMeChange={onWhoCanFindMeChange}
onWhoCanSeeMeChange={onWhoCanSeeMeChange}
onZoomFactorChange={onZoomFactorChange}
preferredSystemLocales={preferredSystemLocales}
refreshCloudBackupStatus={refreshCloudBackupStatus}
refreshBackupSubscriptionStatus={refreshBackupSubscriptionStatus}
removeCustomColorOnConversations={removeCustomColorOnConversations}
removeCustomColor={removeCustomColor}
renderUpdateDialog={renderUpdateDialog}
resetAllChatColors={resetAllChatColors}
resetDefaultChatColor={resetDefaultChatColor}
resolvedLocale={resolvedLocale}
selectedCamera={selectedCamera}
selectedMicrophone={selectedMicrophone}
selectedSpeaker={selectedSpeaker}
sentMediaQualitySetting={sentMediaQualitySetting}
setGlobalDefaultConversationColor={setGlobalDefaultConversationColor}
themeSetting={themeSetting}
universalExpireTimer={universalExpireTimer}
validateBackup={validateBackup}
whoCanFindMe={whoCanFindMe}
whoCanSeeMe={whoCanSeeMe}
zoomFactor={zoomFactor}
/>
</StrictMode>
);
}

View file

@ -16,10 +16,12 @@ import {
type SmartUpdateDialogProps = Readonly<{
containerWidthBreakpoint: WidthBreakpoint;
disableDismiss?: boolean;
}>;
export const SmartUpdateDialog = memo(function SmartUpdateDialog({
containerWidthBreakpoint,
disableDismiss,
}: SmartUpdateDialogProps) {
const i18n = useSelector(getIntl);
const { dismissDialog, snoozeUpdate, startUpdate } = useUpdatesActions();
@ -37,6 +39,7 @@ export const SmartUpdateDialog = memo(function SmartUpdateDialog({
version={version}
currentVersion={window.getVersion()}
dismissDialog={dismissDialog}
disableDismiss={disableDismiss}
snoozeUpdate={snoozeUpdate}
startUpdate={startUpdate}
/>

View file

@ -21,6 +21,7 @@ import type { actions as items } from './ducks/items';
import type { actions as lightbox } from './ducks/lightbox';
import type { actions as linkPreviews } from './ducks/linkPreviews';
import type { actions as mediaGallery } from './ducks/mediaGallery';
import type { actions as nav } from './ducks/nav';
import type { actions as network } from './ducks/network';
import type { actions as notificationProfiles } from './ducks/notificationProfiles';
import type { actions as safetyNumber } from './ducks/safetyNumber';
@ -54,6 +55,7 @@ export type ReduxActions = {
lightbox: typeof lightbox;
linkPreviews: typeof linkPreviews;
mediaGallery: typeof mediaGallery;
nav: typeof nav;
network: typeof network;
notificationProfiles: typeof notificationProfiles;
safetyNumber: typeof safetyNumber;

View file

@ -278,11 +278,4 @@ export async function setupBasics(): Promise<void> {
systemGivenName: 'ME',
profileKey: Bytes.toBase64(PROFILE_KEY),
});
window.Events = {
...window.Events,
getTypingIndicatorSetting: () =>
window.storage.get('typingIndicators', false),
getLinkPreviewSetting: () => window.storage.get('linkPreviews', false),
};
}

View file

@ -230,24 +230,12 @@ describe('calling duck', () => {
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let oldEvents: any;
beforeEach(function (this: Mocha.Context) {
this.sandbox = sinon.createSandbox();
oldEvents = window.Events;
window.Events = {
...(oldEvents || {}),
getCallRingtoneNotification: sinon.spy(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
});
afterEach(function (this: Mocha.Context) {
this.sandbox.restore();
window.Events = oldEvents;
});
describe('actions', () => {

View file

@ -31,30 +31,31 @@ describe('settings', function (this: Mocha.Suite) {
await bootstrap.teardown();
});
it('settings window and all panes load when opened', async () => {
it('settings tab and all panes load when opened', async () => {
const window = await app.getWindow();
const newPagePromise = window.context().waitForEvent('page');
await window.locator('.NavTabs__ItemIcon--Settings').click();
const settingsWindow = await newPagePromise;
await settingsWindow.getByText('Device Name').waitFor();
await window.getByRole('heading', { name: 'Settings' }).waitFor();
await settingsWindow.getByText('Appearance').click();
await settingsWindow.getByText('Language').first().waitFor();
await window.getByRole('button', { name: 'General' }).click();
await window.getByText('Device Name').waitFor();
await settingsWindow.getByText('Chats').click();
await settingsWindow.getByText('Spell check text').waitFor();
await window.getByRole('button', { name: 'Appearance' }).click();
await window.getByText('Language').first().waitFor();
await settingsWindow.getByText('Calls').click();
await settingsWindow.getByText('Enable incoming calls').waitFor();
await window.getByRole('button', { name: 'Chats' }).click();
await window.getByText('Spell check text').waitFor();
await settingsWindow.getByText('Notifications').click();
await settingsWindow.getByText('Notification content').waitFor();
await window.getByRole('button', { name: 'Calls' }).click();
await window.getByText('Enable incoming calls').waitFor();
await settingsWindow.getByText('Privacy').click();
await settingsWindow.getByText('Read receipts').waitFor();
await window.getByRole('button', { name: 'Notifications' }).click();
await window.getByText('Notification content').waitFor();
await settingsWindow.getByText('Data usage').click();
await settingsWindow.getByText('Sent media quality').waitFor();
await window.getByRole('button', { name: 'Privacy' }).click();
await window.getByText('Read receipts').waitFor();
await window.getByRole('button', { name: 'Data usage' }).click();
await window.getByText('Sent media quality').waitFor();
});
});

View file

@ -50,6 +50,10 @@ export type AddLinkPreviewOptionsType = Readonly<{
const linkify = new LinkifyIt();
export function getLinkPreviewSetting(): boolean {
return window.storage.get('linkPreviews', false);
}
export function isValidLink(maybeUrl: string | undefined): boolean {
if (maybeUrl == null) {
return false;

View file

@ -122,7 +122,7 @@ export type StorageAccessType = {
signedKeyUpdateTime: number;
signedKeyUpdateTimePNI: number;
storageKey: string;
synced_at: number;
synced_at: number | undefined;
userAgent: string;
uuid_id: string;
useRingrtcAdm: boolean;
@ -148,9 +148,9 @@ export type StorageAccessType = {
'storage-service-error-records': ReadonlyArray<UnknownRecord>;
'storage-service-unknown-records': ReadonlyArray<UnknownRecord>;
'storage-service-pending-deletes': ReadonlyArray<ExtendedStorageID>;
'preferred-video-input-device': string;
'preferred-audio-input-device': AudioDevice;
'preferred-audio-output-device': AudioDevice;
'preferred-video-input-device': string | undefined;
'preferred-audio-input-device': AudioDevice | undefined;
'preferred-audio-output-device': AudioDevice | undefined;
remoteConfig: RemoteConfigType;
serverTimeSkew: number;
unidentifiedDeliveryIndicators: boolean;

View file

@ -179,3 +179,19 @@ export type StoryMessageRecipientsType = Array<{
distributionListIds: Array<StoryDistributionIdString>;
isAllowedToReply: boolean;
}>;
export function areStoryViewReceiptsEnabled(): boolean {
return (
window.storage.get('storyViewReceiptsEnabled') ??
window.storage.get('read-receipt-setting') ??
false
);
}
export async function setStoryViewReceiptsEnabled(
value: boolean
): Promise<void> {
await window.storage.put('storyViewReceiptsEnabled', value);
const account = window.ConversationController.getOurConversationOrThrow();
account.captureChange('storyViewReceiptsEnabled');
}

View file

@ -112,3 +112,13 @@ export type JSONWithUnknownFields<Value> =
export type WithRequiredProperties<T, P extends keyof T> = Omit<T, P> &
Required<Pick<T, P>>;
export function getTypingIndicatorSetting(): boolean {
return window.storage.get('typingIndicators', false);
}
export function getReadReceiptSetting(): boolean {
return window.storage.get('read-receipt-setting', false);
}
export function getSealedSenderIndicatorSetting(): boolean {
return window.storage.get('sealedSenderIndicators', false);
}

View file

@ -34,18 +34,15 @@ import {
isNotUpdatable,
isStaging,
} from '../util/version';
import { isPathInside } from '../util/isPathInside';
import * as packageJson from '../../package.json';
import type { SettingsChannel } from '../main/settingsChannel';
import { isPathInside } from '../util/isPathInside';
import {
getSignatureFileName,
hexToBinary,
verifySignature,
} from './signature';
import type { LoggerType } from '../types/Logging';
import type { PrepareDownloadResultType as DifferentialDownloadDataType } from './differential';
import {
download as downloadDifferentialData,
getBlockMapFileName,
@ -60,6 +57,10 @@ import {
isTimeToUpdate,
} from './util';
import type { LoggerType } from '../types/Logging';
import type { PrepareDownloadResultType as DifferentialDownloadDataType } from './differential';
import type { MainSQL } from '../sql/main';
const POLL_INTERVAL = 30 * durations.MINUTE;
type JSONVendorSchema = {
@ -109,10 +110,10 @@ type DownloadUpdateResultType = Readonly<{
}>;
export type UpdaterOptionsType = Readonly<{
settingsChannel: SettingsChannel;
logger: LoggerType;
getMainWindow: () => BrowserWindow | undefined;
canRunSilently: () => boolean;
getMainWindow: () => BrowserWindow | undefined;
logger: LoggerType;
sql: MainSQL;
}>;
enum CheckType {
@ -134,7 +135,7 @@ export abstract class Updater {
protected readonly logger: LoggerType;
readonly #settingsChannel: SettingsChannel;
readonly #sql: MainSQL;
protected readonly getMainWindow: () => BrowserWindow | undefined;
@ -157,15 +158,15 @@ export abstract class Updater {
#pollId = getGuid();
constructor({
settingsChannel,
logger,
getMainWindow,
canRunSilently,
getMainWindow,
logger,
sql,
}: UpdaterOptionsType) {
this.#settingsChannel = settingsChannel;
this.logger = logger;
this.getMainWindow = getMainWindow;
this.#canRunSilently = canRunSilently;
this.getMainWindow = getMainWindow;
this.logger = logger;
this.#sql = sql;
this.#throttledSendDownloadingUpdate = throttle(
(downloadedSize: number, downloadSize: number) => {
@ -921,9 +922,11 @@ export abstract class Updater {
async #getAutoDownloadUpdateSetting(): Promise<boolean> {
try {
return await this.#settingsChannel.getSettingFromMainWindow(
'autoDownloadUpdate'
const result = await this.#sql.sqlRead(
'getItemById',
'auto-download-update'
);
return result?.value ?? true;
} catch (error) {
this.logger.warn(
'getAutoDownloadUpdateSetting: Failed to fetch, returning false',

View file

@ -11,11 +11,15 @@ const ringtoneEventQueue = new PQueue({
throwOnTimeout: true,
});
function getCallRingtoneNotificationSetting(): boolean {
return window.storage.get('call-ringtone-notification', true);
}
class CallingTones {
#ringtone?: Sound;
async handRaised() {
const canPlayTone = window.Events.getCallRingtoneNotification();
const canPlayTone = getCallRingtoneNotificationSetting();
if (!canPlayTone) {
return;
}
@ -28,7 +32,7 @@ class CallingTones {
}
async playEndCall(): Promise<void> {
const canPlayTone = window.Events.getCallRingtoneNotification();
const canPlayTone = getCallRingtoneNotificationSetting();
if (!canPlayTone) {
return;
}
@ -46,7 +50,7 @@ class CallingTones {
this.#ringtone = undefined;
}
const canPlayTone = window.Events.getCallRingtoneNotification();
const canPlayTone = getCallRingtoneNotificationSetting();
if (!canPlayTone) {
return;
}
@ -70,7 +74,7 @@ class CallingTones {
}
async someonePresenting() {
const canPlayTone = window.Events.getCallRingtoneNotification();
const canPlayTone = getCallRingtoneNotificationSetting();
if (!canPlayTone) {
return;
}

View file

@ -3,46 +3,20 @@
import { ipcRenderer } from 'electron';
import type { SystemPreferences } from 'electron';
import type { AudioDevice } from '@signalapp/ringrtc';
import { noop } from 'lodash';
import type {
AutoDownloadAttachmentType,
ZoomFactorType,
} from '../types/Storage.d';
import type {
ConversationColorType,
CustomColorType,
DefaultConversationColorType,
} from '../types/Colors';
import { DEFAULT_CONVERSATION_COLOR } from '../types/Colors';
import type { ZoomFactorType } from '../types/Storage.d';
import * as Errors from '../types/errors';
import * as Stickers from '../types/Stickers';
import * as Settings from '../types/Settings';
import type { ConversationType } from '../state/ducks/conversations';
import { calling } from '../services/calling';
import { resolveUsernameByLinkBase64 } from '../services/username';
import { writeProfile } from '../services/writeProfile';
import {
backupsService,
type ValidationResultType as BackupValidationResultType,
} from '../services/backups';
import { isInCall } from '../state/selectors/calling';
import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations';
import { getCustomColors } from '../state/selectors/items';
import { themeChanged } from '../shims/themeChanged';
import { renderClearingDataView } from '../shims/renderClearingDataView';
import * as universalExpireTimer from './universalExpireTimer';
import { PhoneNumberDiscoverability } from './phoneNumberDiscoverability';
import { PhoneNumberSharingMode } from './phoneNumberSharingMode';
import { strictAssert, assertDev } from './assert';
import * as durations from './durations';
import type { DurationInSeconds } from './durations';
import { strictAssert } from './assert';
import * as Registration from './registration';
import { lookupConversationWithoutServiceId } from './lookupConversationWithoutServiceId';
import * as log from '../logging/log';
import { deleteAllMyStories } from './deleteAllMyStories';
import {
type NotificationClickData,
notificationService,
@ -50,103 +24,34 @@ import {
import { StoryViewModeType, StoryViewTargetType } from '../types/Stories';
import { isValidE164 } from './isValidE164';
import { fromWebSafeBase64 } from './webSafeBase64';
import { getConversation } from './getConversation';
import { instance, PhoneNumberFormat } from './libphonenumberInstance';
import { showConfirmationDialog } from './showConfirmationDialog';
import type {
EphemeralSettings,
SettingsValuesType,
ThemeType,
} from './preload';
import type { SystemTraySetting } from '../types/SystemTraySetting';
import { drop } from './drop';
import { sendSyncRequests } from '../textsecure/syncRequests';
import { waitForEvent } from '../shims/events';
import { DEFAULT_AUTO_DOWNLOAD_ATTACHMENT } from '../textsecure/Storage';
import { EmojiSkinTone } from '../components/fun/data/emojis';
import type {
BackupsSubscriptionType,
BackupStatusType,
} from '../types/backups';
import { isBackupFeatureEnabled } from './isBackupEnabled';
import { isSettingsInternalEnabled } from './isSettingsInternalEnabled';
import type { ValidateLocalBackupStructureResultType } from '../services/backups/util/localBackup';
type SentMediaQualityType = 'standard' | 'high';
type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
import { SystemTraySetting } from '../types/SystemTraySetting';
import OS from './os/osPreload';
export type IPCEventsValuesType = {
alwaysRelayCalls: boolean | undefined;
audioNotification: boolean | undefined;
audioMessage: boolean;
autoConvertEmoji: boolean;
autoDownloadAttachment: AutoDownloadAttachmentType;
autoDownloadUpdate: boolean;
// IPC-mediated
autoLaunch: boolean;
callRingtoneNotification: boolean;
callSystemNotification: boolean;
countMutedConversations: boolean;
hasStoriesDisabled: boolean;
hideMenuBar: boolean | undefined;
incomingCallNotification: boolean;
lastSyncTime: number | undefined;
notificationDrawAttention: boolean;
notificationSetting: NotificationSettingType;
preferredAudioInputDevice: AudioDevice | undefined;
preferredAudioOutputDevice: AudioDevice | undefined;
preferredVideoInputDevice: string | undefined;
sentMediaQualitySetting: SentMediaQualityType;
textFormatting: boolean;
universalExpireTimer: DurationInSeconds;
zoomFactor: ZoomFactorType;
storyViewReceiptsEnabled: boolean;
// Optional
mediaPermissions: boolean;
mediaCameraPermissions: boolean | undefined;
// Only getters
backupFeatureEnabled: boolean;
cloudBackupStatus: BackupStatusType | undefined;
backupSubscriptionStatus: BackupsSubscriptionType | undefined;
blockedCount: number;
linkPreviewSetting: boolean;
phoneNumberDiscoverabilitySetting: PhoneNumberDiscoverability;
phoneNumberSharingSetting: PhoneNumberSharingMode;
readReceiptSetting: boolean;
typingIndicatorSetting: boolean;
deviceName: string | undefined;
phoneNumber: string | undefined;
zoomFactor: ZoomFactorType;
};
export type IPCEventsCallbacksType = {
getAvailableIODevices(): Promise<{
availableCameras: Array<
Pick<MediaDeviceInfo, 'deviceId' | 'groupId' | 'kind' | 'label'>
>;
availableMicrophones: Array<AudioDevice>;
availableSpeakers: Array<AudioDevice>;
}>;
refreshCloudBackupStatus(): void;
refreshBackupSubscriptionStatus(): void;
addCustomColor: (customColor: CustomColorType) => void;
addDarkOverlay: () => void;
removeDarkOverlay: () => void;
cleanupDownloads: () => Promise<void>;
deleteAllData: () => Promise<void>;
deleteAllMyStories: () => Promise<void>;
editCustomColor: (colorId: string, customColor: CustomColorType) => void;
getConversationsWithCustomColor: (x: string) => Array<ConversationType>;
getIsInCall: () => boolean;
getMediaAccessStatus: (
mediaType: 'screen' | 'microphone' | 'camera'
) => Promise<ReturnType<SystemPreferences['getMediaAccessStatus']>>;
installStickerPack: (packId: string, key: string) => Promise<void>;
isPrimary: () => boolean;
isInternalUser: () => boolean;
removeCustomColor: (x: string) => void;
removeCustomColorOnConversations: (x: string) => void;
removeDarkOverlay: () => void;
resetAllChatColors: () => void;
resetDefaultChatColor: () => void;
requestCloseConfirmation: () => Promise<boolean>;
setMediaPlaybackDisabled: (playbackDisabled: boolean) => void;
showConversationViaNotification: (data: NotificationClickData) => void;
showConversationViaToken: (token: string) => void;
@ -158,23 +63,9 @@ export type IPCEventsCallbacksType = {
showGroupViaLink: (value: string) => Promise<void>;
showReleaseNotes: () => void;
showStickerPack: (packId: string, key: string) => void;
startCallingLobbyViaToken: (token: string) => void;
requestCloseConfirmation: () => Promise<boolean>;
getIsInCall: () => boolean;
shutdown: () => Promise<void>;
startCallingLobbyViaToken: (token: string) => void;
unknownSignalLink: () => void;
getCustomColors: () => Record<string, CustomColorType>;
syncRequest: () => Promise<void>;
exportLocalBackup: () => Promise<BackupValidationResultType>;
importLocalBackup: () => Promise<ValidateLocalBackupStructureResultType>;
validateBackup: () => Promise<BackupValidationResultType>;
setGlobalDefaultConversationColor: (
color: ConversationColorType,
customColor?: { id: string; value: CustomColorType }
) => void;
setEmojiSkinToneDefault: (emojiSkinTone: EmojiSkinTone) => void;
getDefaultConversationColor: () => DefaultConversationColorType;
getEmojiSkinToneDefault: () => EmojiSkinTone;
uploadStickerPack: (
manifest: Uint8Array,
stickers: ReadonlyArray<Uint8Array>
@ -183,42 +74,20 @@ export type IPCEventsCallbacksType = {
type ValuesWithGetters = Omit<
SettingsValuesType,
// Async
| 'zoomFactor'
// Async - we'll redefine these in IPCEventsGettersType
| 'autoLaunch'
| 'localeOverride'
| 'spellCheck'
| 'themeSetting'
// Optional
| 'mediaPermissions'
| 'mediaCameraPermissions'
| 'autoLaunch'
| 'spellCheck'
| 'contentProtection'
| 'systemTraySetting'
| 'themeSetting'
| 'zoomFactor'
>;
type ValuesWithSetters = Omit<
SettingsValuesType,
| 'blockedCount'
| 'defaultConversationColor'
| 'linkPreviewSetting'
| 'readReceiptSetting'
| 'typingIndicatorSetting'
| 'deviceName'
| 'phoneNumber'
| 'backupFeatureEnabled'
| 'cloudBackupStatus'
| 'backupSubscriptionStatus'
// Optional
| 'mediaPermissions'
| 'mediaCameraPermissions'
// Only set in the Settings window
| 'localeOverride'
| 'spellCheck'
| 'systemTraySetting'
>;
// Right now everything is symmetrical
type ValuesWithSetters = SettingsValuesType;
export type IPCEventsUpdatersType = {
[Key in keyof EphemeralSettings as IPCEventUpdaterType<Key>]?: (
@ -235,22 +104,23 @@ export type IPCEventSetterType<Key extends keyof SettingsValuesType> =
export type IPCEventUpdaterType<Key extends keyof SettingsValuesType> =
`update${Capitalize<Key>}`;
export type ZoomFactorChangeCallback = (zoomFactor: ZoomFactorType) => void;
export type IPCEventsGettersType = {
[Key in keyof ValuesWithGetters as IPCEventGetterType<Key>]: () => ValuesWithGetters[Key];
} & {
// Async
getZoomFactor: () => Promise<ZoomFactorType>;
getAutoLaunch: () => Promise<boolean>;
getLocaleOverride: () => Promise<string | null>;
getMediaPermissions: () => Promise<boolean>;
getMediaCameraPermissions: () => Promise<boolean>;
getSpellCheck: () => Promise<boolean>;
getContentProtection: () => Promise<boolean>;
getSystemTraySetting: () => Promise<SystemTraySetting>;
getThemeSetting: () => Promise<ThemeType>;
getZoomFactor: () => Promise<ZoomFactorType>;
// Events
onZoomFactorChange: (callback: (zoomFactor: ZoomFactorType) => void) => void;
// Optional
getMediaPermissions?: () => Promise<boolean>;
getMediaCameraPermissions?: () => Promise<boolean>;
getAutoLaunch?: () => Promise<boolean>;
onZoomFactorChange: (callback: ZoomFactorChangeCallback) => void;
offZoomFactorChange: (callback: ZoomFactorChangeCallback) => void;
};
export type IPCEventsSettersType = {
@ -258,6 +128,7 @@ export type IPCEventsSettersType = {
value: NonNullable<ValuesWithSetters[Key]>
) => Promise<void>;
} & {
setLocaleOverride: (value: string | null) => Promise<void>;
setMediaPermissions?: (value: boolean) => Promise<void>;
setMediaCameraPermissions?: (value: boolean) => Promise<void>;
};
@ -270,325 +141,92 @@ export type IPCEventsType = IPCEventsGettersType &
export function createIPCEvents(
overrideEvents: Partial<IPCEventsType> = {}
): IPCEventsType {
const setPhoneNumberDiscoverabilitySetting = async (
newValue: PhoneNumberDiscoverability
): Promise<void> => {
strictAssert(window.textsecure.server, 'WebAPI must be available');
await window.storage.put('phoneNumberDiscoverability', newValue);
await window.textsecure.server.setPhoneNumberDiscoverability(
newValue === PhoneNumberDiscoverability.Discoverable
);
const account = window.ConversationController.getOurConversationOrThrow();
account.captureChange('phoneNumberDiscoverability');
};
let zoomFactorChangeCallbacks: Array<ZoomFactorChangeCallback> = [];
ipcRenderer.on('zoomFactorChanged', (_event, zoomFactor) => {
zoomFactorChangeCallbacks.forEach(callback => callback(zoomFactor));
});
return {
getDeviceName: () => window.textsecure.storage.user.getDeviceName(),
getPhoneNumber: () => {
try {
const e164 = window.textsecure.storage.user.getNumber();
const parsedNumber = instance.parse(e164);
return instance.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL);
} catch (error) {
log.warn(
'IPC.getPhoneNumber: failed to parse our E164',
Errors.toLogFormat(error)
);
return '';
}
// From IPCEventsValuesType
getAutoLaunch: async () => {
return (await window.IPC.getAutoLaunch()) ?? false;
},
setAutoLaunch: async (value: boolean) => {
await window.IPC.setAutoLaunch(value);
},
getMediaCameraPermissions: async () => {
return (await window.IPC.getMediaCameraPermissions()) ?? false;
},
setMediaCameraPermissions: async () => {
const forCamera = true;
await window.IPC.showPermissionsPopup(false, forCamera);
},
getMediaPermissions: async () => {
return (await window.IPC.getMediaPermissions()) ?? false;
},
setMediaPermissions: async () => {
const forCalling = true;
await window.IPC.showPermissionsPopup(forCalling, false);
},
getZoomFactor: () => {
return ipcRenderer.invoke('getZoomFactor');
},
setZoomFactor: async zoomFactor => {
ipcRenderer.send('setZoomFactor', zoomFactor);
},
// From IPCEventsGettersType
onZoomFactorChange: callback => {
ipcRenderer.on('zoomFactorChanged', (_event, zoomFactor) => {
callback(zoomFactor);
});
zoomFactorChangeCallbacks.push(callback);
},
offZoomFactorChange: toRemove => {
zoomFactorChangeCallbacks = zoomFactorChangeCallbacks.filter(
callback => toRemove !== callback
);
},
setPhoneNumberDiscoverabilitySetting,
setPhoneNumberSharingSetting: async (newValue: PhoneNumberSharingMode) => {
const account = window.ConversationController.getOurConversationOrThrow();
const promises = new Array<Promise<void>>();
promises.push(window.storage.put('phoneNumberSharingMode', newValue));
if (newValue === PhoneNumberSharingMode.Everybody) {
promises.push(
setPhoneNumberDiscoverabilitySetting(
PhoneNumberDiscoverability.Discoverable
// From EphemeralSettings
getLocaleOverride: async () => {
return (await getEphemeralSetting('localeOverride')) ?? null;
},
setLocaleOverride: async (value: string | null) => {
await setEphemeralSetting('localeOverride', value);
},
getContentProtection: async () => {
return (
(await getEphemeralSetting('contentProtection')) ??
Settings.isContentProtectionEnabledByDefault(
OS,
window.SignalContext.config.osRelease
)
);
}
account.captureChange('phoneNumberSharingMode');
await Promise.all(promises);
// Write profile after updating storage so that the write has up-to-date
// information.
await writeProfile(getConversation(account), {
keepAvatar: true,
});
},
getHasStoriesDisabled: () =>
window.storage.get('hasStoriesDisabled', false),
setHasStoriesDisabled: async (value: boolean) => {
await window.storage.put('hasStoriesDisabled', value);
const account = window.ConversationController.getOurConversationOrThrow();
account.captureChange('hasStoriesDisabled');
window.textsecure.server?.onHasStoriesDisabledChange(value);
},
getContentProtection: () => {
return getEphemeralSetting('contentProtection');
},
setContentProtection: async (value: boolean) => {
await setEphemeralSetting('contentProtection', value);
},
getStoryViewReceiptsEnabled: () => {
getSpellCheck: async () => {
return (await getEphemeralSetting('spellCheck')) ?? false;
},
setSpellCheck: async (value: boolean) => {
await setEphemeralSetting('spellCheck', value);
},
getSystemTraySetting: async () => {
return (
window.storage.get('storyViewReceiptsEnabled') ??
window.storage.get('read-receipt-setting') ??
false
(await getEphemeralSetting('systemTraySetting')) ??
SystemTraySetting.Uninitialized
);
},
setStoryViewReceiptsEnabled: async (value: boolean) => {
await window.storage.put('storyViewReceiptsEnabled', value);
const account = window.ConversationController.getOurConversationOrThrow();
account.captureChange('storyViewReceiptsEnabled');
setSystemTraySetting: async (value: SystemTraySetting) => {
await setEphemeralSetting('systemTraySetting', value);
},
getPreferredAudioInputDevice: () =>
window.storage.get('preferred-audio-input-device'),
setPreferredAudioInputDevice: device =>
window.storage.put('preferred-audio-input-device', device),
getPreferredAudioOutputDevice: () =>
window.storage.get('preferred-audio-output-device'),
setPreferredAudioOutputDevice: device =>
window.storage.put('preferred-audio-output-device', device),
getPreferredVideoInputDevice: () =>
window.storage.get('preferred-video-input-device'),
setPreferredVideoInputDevice: device =>
window.storage.put('preferred-video-input-device', device),
deleteAllMyStories: async () => {
await deleteAllMyStories();
},
setGlobalDefaultConversationColor: (...args) =>
window.reduxActions.items.setGlobalDefaultConversationColor(...args),
setEmojiSkinToneDefault: (emojiSkinTone: EmojiSkinTone) =>
window.reduxActions.items.setEmojiSkinToneDefault(emojiSkinTone),
// Chat Color redux hookups
getCustomColors: () => {
return getCustomColors(window.reduxStore.getState()) || {};
},
getConversationsWithCustomColor: colorId => {
return getConversationsWithCustomColorSelector(
window.reduxStore.getState()
)(colorId);
},
addCustomColor: (...args) =>
window.reduxActions.items.addCustomColor(...args),
editCustomColor: (...args) =>
window.reduxActions.items.editCustomColor(...args),
removeCustomColor: colorId =>
window.reduxActions.items.removeCustomColor(colorId),
removeCustomColorOnConversations: colorId =>
window.reduxActions.conversations.removeCustomColorOnConversations(
colorId
),
resetAllChatColors: () =>
window.reduxActions.conversations.resetAllChatColors(),
resetDefaultChatColor: () =>
window.reduxActions.items.resetDefaultChatColor(),
// Getters only
getAvailableIODevices: async () => {
const { availableCameras, availableMicrophones, availableSpeakers } =
await calling.getAvailableIODevices();
return {
// mapping it to a pojo so that it is IPC friendly
availableCameras: availableCameras.map(
(inputDeviceInfo: MediaDeviceInfo) => ({
deviceId: inputDeviceInfo.deviceId,
groupId: inputDeviceInfo.groupId,
kind: inputDeviceInfo.kind,
label: inputDeviceInfo.label,
})
),
availableMicrophones,
availableSpeakers,
};
},
getBackupFeatureEnabled: () => {
return isBackupFeatureEnabled();
},
getCloudBackupStatus: () => {
return window.storage.get('cloudBackupStatus');
},
getBackupSubscriptionStatus: () => {
return window.storage.get('backupSubscriptionStatus');
},
refreshCloudBackupStatus:
window.Signal.Services.backups.throttledFetchCloudBackupStatus,
refreshBackupSubscriptionStatus:
window.Signal.Services.backups.throttledFetchSubscriptionStatus,
getBlockedCount: () =>
window.storage.blocked.getBlockedServiceIds().length +
window.storage.blocked.getBlockedGroups().length,
getDefaultConversationColor: () =>
window.storage.get(
'defaultConversationColor',
DEFAULT_CONVERSATION_COLOR
),
getEmojiSkinToneDefault: () =>
window.storage.get('emojiSkinToneDefault', EmojiSkinTone.None),
getLinkPreviewSetting: () => window.storage.get('linkPreviews', false),
getPhoneNumberDiscoverabilitySetting: () =>
window.storage.get(
'phoneNumberDiscoverability',
PhoneNumberDiscoverability.NotDiscoverable
),
getPhoneNumberSharingSetting: () =>
window.storage.get(
'phoneNumberSharingMode',
PhoneNumberSharingMode.Nobody
),
getReadReceiptSetting: () =>
window.storage.get('read-receipt-setting', false),
getTypingIndicatorSetting: () =>
window.storage.get('typingIndicators', false),
// Configurable settings
getAutoDownloadAttachment: () =>
window.storage.get(
'auto-download-attachment',
DEFAULT_AUTO_DOWNLOAD_ATTACHMENT
),
setAutoDownloadAttachment: (setting: AutoDownloadAttachmentType) =>
window.storage.put('auto-download-attachment', setting),
getAutoDownloadUpdate: () =>
window.storage.get('auto-download-update', true),
setAutoDownloadUpdate: value =>
window.storage.put('auto-download-update', value),
getAutoConvertEmoji: () => window.storage.get('autoConvertEmoji', true),
setAutoConvertEmoji: value => window.storage.put('autoConvertEmoji', value),
getSentMediaQualitySetting: () =>
window.storage.get('sent-media-quality', 'standard'),
setSentMediaQualitySetting: value =>
window.storage.put('sent-media-quality', value),
getThemeSetting: async () => {
return getEphemeralSetting('themeSetting') ?? null;
return (await getEphemeralSetting('themeSetting')) ?? 'system';
},
setThemeSetting: async value => {
drop(setEphemeralSetting('themeSetting', value));
},
updateThemeSetting: _theme => {
drop(themeChanged());
},
getHideMenuBar: () => window.storage.get('hide-menu-bar'),
setHideMenuBar: value => {
const promise = window.storage.put('hide-menu-bar', value);
window.IPC.setAutoHideMenuBar(value);
window.IPC.setMenuBarVisibility(!value);
return promise;
},
getSystemTraySetting: () => getEphemeralSetting('systemTraySetting'),
getLocaleOverride: async () => {
return getEphemeralSetting('localeOverride') ?? null;
},
getNotificationSetting: () =>
window.storage.get('notification-setting', 'message'),
setNotificationSetting: (value: 'message' | 'name' | 'count' | 'off') =>
window.storage.put('notification-setting', value),
getNotificationDrawAttention: () =>
window.storage.get('notification-draw-attention', false),
setNotificationDrawAttention: value =>
window.storage.put('notification-draw-attention', value),
getAudioMessage: () => window.storage.get('audioMessage', false),
setAudioMessage: value => window.storage.put('audioMessage', value),
getAudioNotification: () => window.storage.get('audio-notification'),
setAudioNotification: value =>
window.storage.put('audio-notification', value),
getCountMutedConversations: () =>
window.storage.get('badge-count-muted-conversations', false),
setCountMutedConversations: value => {
const promise = window.storage.put(
'badge-count-muted-conversations',
value
);
window.Whisper.events.trigger('updateUnreadCount');
return promise;
},
getCallRingtoneNotification: () =>
window.storage.get('call-ringtone-notification', true),
setCallRingtoneNotification: value =>
window.storage.put('call-ringtone-notification', value),
getCallSystemNotification: () =>
window.storage.get('call-system-notification', true),
setCallSystemNotification: value =>
window.storage.put('call-system-notification', value),
getIncomingCallNotification: () =>
window.storage.get('incoming-call-notification', true),
setIncomingCallNotification: value =>
window.storage.put('incoming-call-notification', value),
getSpellCheck: () => {
return getEphemeralSetting('spellCheck');
},
getTextFormatting: () => window.storage.get('textFormatting', true),
setTextFormatting: value => window.storage.put('textFormatting', value),
getAlwaysRelayCalls: () => window.storage.get('always-relay-calls'),
setAlwaysRelayCalls: value =>
window.storage.put('always-relay-calls', value),
getAutoLaunch: () => window.IPC.getAutoLaunch(),
setAutoLaunch: async (value: boolean) => {
return window.IPC.setAutoLaunch(value);
},
isPrimary: () => window.textsecure.storage.user.getDeviceId() === 1,
isInternalUser: () => isSettingsInternalEnabled(),
syncRequest: async () => {
const contactSyncComplete = waitForEvent(
'contactSync:complete',
5 * durations.MINUTE
);
await sendSyncRequests();
return contactSyncComplete;
},
// Only for internal use
exportLocalBackup: () => backupsService._internalExportLocalBackup(),
importLocalBackup: () =>
backupsService._internalStageLocalBackupForImport(),
validateBackup: () => backupsService._internalValidate(),
getLastSyncTime: () => window.storage.get('synced_at'),
setLastSyncTime: value => window.storage.put('synced_at', value),
getUniversalExpireTimer: () => universalExpireTimer.get(),
setUniversalExpireTimer: async newValue => {
await universalExpireTimer.set(newValue);
// Update account in Storage Service
const account = window.ConversationController.getOurConversationOrThrow();
account.captureChange('universalExpireTimer');
// Add a notification to the currently open conversation
const state = window.reduxStore.getState();
const selectedId = state.conversations.selectedConversationId;
if (selectedId) {
const conversation = window.ConversationController.get(selectedId);
assertDev(conversation, "Conversation wasn't found");
await conversation.updateLastMessage();
}
setThemeSetting: async (value: ThemeType) => {
await setEphemeralSetting('themeSetting', value);
},
// From IPCEventsCallbacksType
addDarkOverlay: () => {
const elems = document.querySelectorAll('.dark-overlay');
if (elems.length) {
@ -608,45 +246,57 @@ export function createIPCEvents(
elem.remove();
}
},
showKeyboardShortcuts: () =>
window.reduxActions.globalModals.showShortcutGuideModal(),
cleanupDownloads: async () => {
await ipcRenderer.invoke('cleanup-downloads');
},
deleteAllData: async () => {
renderClearingDataView();
getIsInCall: (): boolean => {
return isInCall(window.reduxStore.getState());
},
showStickerPack: (packId, key) => {
// We can get these events even if the user has never linked this instance.
if (!Registration.everDone()) {
log.warn('showStickerPack: Not registered, returning early');
return;
}
window.reduxActions.globalModals.showStickerPackPreview(packId, key);
getMediaAccessStatus: async (
mediaType: 'screen' | 'microphone' | 'camera'
) => {
return window.IPC.getMediaAccessStatus(mediaType);
},
showGroupViaLink: async value => {
// We can get these events even if the user has never linked this instance.
if (!Registration.everDone()) {
log.warn('showGroupViaLink: Not registered, returning early');
return;
}
try {
await window.Signal.Groups.joinViaLink(value);
} catch (error) {
log.error(
'showGroupViaLink: Ran into an error!',
Errors.toLogFormat(error)
);
window.reduxActions.globalModals.showErrorModal({
title: window.i18n('icu:GroupV2--join--general-join-failure--title'),
description: window.i18n('icu:GroupV2--join--general-join-failure'),
installStickerPack: async (packId, key) => {
void Stickers.downloadStickerPack(packId, key, {
finalStatus: 'installed',
actionSource: 'ui',
});
},
requestCloseConfirmation: async (): Promise<boolean> => {
try {
await new Promise<void>((resolve, reject) => {
showConfirmationDialog({
dialogName: 'closeConfirmation',
onTopOfEverything: true,
cancelText: window.i18n(
'icu:ConfirmationDialog__Title--close-requested-not-now'
),
confirmStyle: 'negative',
title: window.i18n(
'icu:ConfirmationDialog__Title--in-call-close-requested'
),
okText: window.i18n('icu:close'),
reject: () => reject(),
resolve: () => resolve(),
});
});
log.info('requestCloseConfirmation: Close confirmed by user.');
window.reduxActions.calling.hangUpActiveCall(
'User confirmed in-call close.'
);
return true;
} catch {
log.info('requestCloseConfirmation: Close cancelled by user.');
return false;
}
},
setMediaPlaybackDisabled: (playbackDisabled: boolean) => {
window.reduxActions?.lightbox.setPlaybackDisabled(playbackDisabled);
if (playbackDisabled) {
window.reduxActions?.audioPlayer.pauseVoiceNotePlayer();
}
},
showConversationViaNotification({
conversationId,
messageId,
@ -667,7 +317,6 @@ export function createIPCEvents(
});
}
},
showConversationViaToken(token: string) {
const data = notificationService.resolveToken(token);
if (!data) {
@ -676,7 +325,6 @@ export function createIPCEvents(
window.Events.showConversationViaNotification(data);
}
},
async showConversationViaSignalDotMe(kind: string, value: string) {
if (!Registration.everDone()) {
log.info(
@ -722,7 +370,40 @@ export function createIPCEvents(
log.info('showConversationViaSignalDotMe: invalid E164');
showUnknownSgnlLinkModal();
},
showKeyboardShortcuts: () =>
window.reduxActions.globalModals.showShortcutGuideModal(),
showGroupViaLink: async value => {
// We can get these events even if the user has never linked this instance.
if (!Registration.everDone()) {
log.warn('showGroupViaLink: Not registered, returning early');
return;
}
try {
await window.Signal.Groups.joinViaLink(value);
} catch (error) {
log.error(
'showGroupViaLink: Ran into an error!',
Errors.toLogFormat(error)
);
window.reduxActions.globalModals.showErrorModal({
title: window.i18n('icu:GroupV2--join--general-join-failure--title'),
description: window.i18n('icu:GroupV2--join--general-join-failure'),
});
}
},
showReleaseNotes: () => {
const { showWhatsNewModal } = window.reduxActions.globalModals;
showWhatsNewModal();
},
showStickerPack: (packId, key) => {
// We can get these events even if the user has never linked this instance.
if (!Registration.everDone()) {
log.warn('showStickerPack: Not registered, returning early');
return;
}
window.reduxActions.globalModals.showStickerPackPreview(packId, key);
},
shutdown: () => Promise.resolve(),
startCallingLobbyViaToken(token: string) {
const data = notificationService.resolveToken(token);
if (!data) {
@ -733,76 +414,10 @@ export function createIPCEvents(
isVideoCall: true,
});
},
requestCloseConfirmation: async (): Promise<boolean> => {
try {
await new Promise<void>((resolve, reject) => {
showConfirmationDialog({
dialogName: 'closeConfirmation',
onTopOfEverything: true,
cancelText: window.i18n(
'icu:ConfirmationDialog__Title--close-requested-not-now'
),
confirmStyle: 'negative',
title: window.i18n(
'icu:ConfirmationDialog__Title--in-call-close-requested'
),
okText: window.i18n('icu:close'),
reject: () => reject(),
resolve: () => resolve(),
});
});
log.info('requestCloseConfirmation: Close confirmed by user.');
window.reduxActions.calling.hangUpActiveCall(
'User confirmed in-call close.'
);
return true;
} catch {
log.info('requestCloseConfirmation: Close cancelled by user.');
return false;
}
},
getIsInCall: (): boolean => {
return isInCall(window.reduxStore.getState());
},
unknownSignalLink: () => {
log.warn('unknownSignalLink: Showing error dialog');
showUnknownSgnlLinkModal();
},
installStickerPack: async (packId, key) => {
void Stickers.downloadStickerPack(packId, key, {
finalStatus: 'installed',
actionSource: 'ui',
});
},
shutdown: () => Promise.resolve(),
showReleaseNotes: () => {
const { showWhatsNewModal } = window.reduxActions.globalModals;
showWhatsNewModal();
},
getMediaAccessStatus: async (
mediaType: 'screen' | 'microphone' | 'camera'
) => {
return window.IPC.getMediaAccessStatus(mediaType);
},
getMediaPermissions: window.IPC.getMediaPermissions,
getMediaCameraPermissions: async () => {
return (await window.IPC.getMediaCameraPermissions()) || false;
},
setMediaPlaybackDisabled: (playbackDisabled: boolean) => {
window.reduxActions?.lightbox.setPlaybackDisabled(playbackDisabled);
if (playbackDisabled) {
window.reduxActions?.audioPlayer.pauseVoiceNotePlayer();
}
},
uploadStickerPack: (
manifest: Uint8Array,
stickers: ReadonlyArray<Uint8Array>
@ -812,7 +427,6 @@ export function createIPCEvents(
ipcRenderer.send('art-creator:onUploadProgress')
);
},
...overrideEvents,
};
}

View file

@ -11,3 +11,12 @@ export function isBackupFeatureEnabled(): boolean {
}
return Boolean(RemoteConfig.isEnabled('desktop.backup.credentialFetch'));
}
export function isBackupFeatureEnabledForRedux(
config: RemoteConfig.ConfigMapType | undefined
): boolean {
if (isStagingServer() || isTestOrMockEnvironment()) {
return true;
}
return Boolean(config?.['desktop.backup.credentialFetch']?.enabled);
}

View file

@ -82,6 +82,7 @@ async function fetchAndUpdateDeviceName() {
}
await window.storage.user.setDeviceName(newName);
window.Whisper.events.trigger('deviceNameChanged');
log.info(
'fetchAndUpdateDeviceName: successfully updated new device name locally'
);

View file

@ -7,8 +7,8 @@ import { strictAssert } from './assert';
import * as Errors from '../types/errors';
import type { UnwrapPromise } from '../types/Util';
import type {
IPCEventsValuesType,
IPCEventsCallbacksType,
IPCEventsValuesType,
} from './createIPCEvents';
import type { SystemTraySetting } from '../types/SystemTraySetting';
@ -25,11 +25,11 @@ export type SettingType<Value> = Readonly<{
export type ThemeType = 'light' | 'dark' | 'system';
export type EphemeralSettings = {
localeOverride: string | null;
spellCheck: boolean;
contentProtection: boolean;
systemTraySetting: SystemTraySetting;
themeSetting: ThemeType;
localeOverride: string | null;
};
export type SettingsValuesType = IPCEventsValuesType & EphemeralSettings;

View file

@ -9,7 +9,7 @@ export async function requestMicrophonePermissions(
await window.IPC.showPermissionsPopup(forCalling, false);
// Check the setting again (from the source of truth).
return window.IPC.getMediaPermissions();
return window.Events.getMediaPermissions();
}
return true;

View file

@ -2,6 +2,13 @@
// SPDX-License-Identifier: AGPL-3.0-only
export const getStoriesDisabled = (): boolean =>
window.Events.getHasStoriesDisabled();
window.storage.get('hasStoriesDisabled', false);
export const setStoriesDisabled = async (value: boolean): Promise<void> => {
await window.storage.put('hasStoriesDisabled', value);
const account = window.ConversationController.getOurConversationOrThrow();
account.captureChange('hasStoriesDisabled');
window.textsecure.server?.onHasStoriesDisabledChange(value);
};
export const getStoriesBlocked = (): boolean => getStoriesDisabled();

View file

@ -2,12 +2,16 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { DurationInSeconds } from './durations';
import type { ItemsStateType } from '../state/ducks/items';
export const ITEM_NAME = 'universalExpireTimer';
export function get(): DurationInSeconds {
return DurationInSeconds.fromSeconds(window.storage.get(ITEM_NAME) || 0);
}
export function getForRedux(items: ItemsStateType): DurationInSeconds {
return DurationInSeconds.fromSeconds(items[ITEM_NAME] || 0);
}
export function set(newValue: DurationInSeconds | undefined): Promise<void> {
return window.storage.put(ITEM_NAME, newValue || DurationInSeconds.ZERO);

6
ts/window.d.ts vendored
View file

@ -66,7 +66,7 @@ export type IPCType = {
erase: () => Promise<void>;
};
drawAttention: () => void;
getAutoLaunch: () => Promise<boolean>;
getAutoLaunch: () => Promise<boolean | undefined>;
getMediaAccessStatus: (
mediaType: 'screen' | 'microphone' | 'camera'
) => Promise<ReturnType<SystemPreferences['getMediaAccessStatus']>>;
@ -74,7 +74,7 @@ export type IPCType = {
openSystemMediaPermissions: (
mediaType: 'microphone' | 'camera' | 'screenCapture'
) => Promise<void>;
getMediaPermissions: () => Promise<boolean>;
getMediaPermissions: () => Promise<boolean | undefined>;
whenWindowVisible: () => Promise<void>;
logAppLoadedEvent?: (options: { processedCount?: number }) => void;
readyForUpdates: () => void;
@ -82,6 +82,8 @@ export type IPCType = {
setAutoHideMenuBar: (value: boolean) => void;
setAutoLaunch: (value: boolean) => Promise<void>;
setBadge: (badge: number | 'marked-unread') => void;
setMediaPermissions: (value: boolean) => Promise<void>;
setMediaCameraPermissions: (value: boolean) => Promise<void>;
setMenuBarVisibility: (value: boolean) => void;
showDebugLog: () => void;
showPermissionsPopup: (

View file

@ -123,6 +123,10 @@ const IPC: IPCType = {
},
showPermissionsPopup: (forCalling, forCamera) =>
ipc.invoke('show-permissions-popup', forCalling, forCamera),
setMediaPermissions: (value: boolean) =>
ipc.invoke('settings:set:mediaPermissions', value),
setMediaCameraPermissions: (value: boolean) =>
ipc.invoke('settings:set:mediaCameraPermissions', value),
showSettings: () => ipc.send('show-settings'),
showWindow: () => {
log.info('show window');
@ -263,6 +267,10 @@ ipc.on('additional-log-data-request', async event => {
});
});
ipc.on('open-settings-tab', () => {
window.Whisper.events.trigger('openSettingsTab');
});
ipc.on('set-up-as-new-device', () => {
window.Whisper.events.trigger('setupAsNewDevice');
});
@ -329,19 +337,6 @@ ipc.on('remove-dark-overlay', () => {
window.Events.removeDarkOverlay();
});
ipc.on('delete-all-data', async () => {
const { deleteAllData } = window.Events;
if (!deleteAllData) {
return;
}
try {
await deleteAllData();
} catch (error) {
log.error('delete-all-data: error', Errors.toLogFormat(error));
}
});
ipc.on('show-sticker-pack', (_event, info) => {
window.Events.showStickerPack?.(info.packId, info.packKey);
});

View file

@ -1,92 +1,10 @@
// Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import {
installCallback,
installSetting,
installEphemeralSetting,
} from '../util/preload';
import { installEphemeralSetting } from '../util/preload';
// ChatColorPicker redux hookups
installCallback('getCustomColors');
installCallback('getConversationsWithCustomColor');
installCallback('addCustomColor');
installCallback('editCustomColor');
installCallback('removeCustomColor');
installCallback('removeCustomColorOnConversations');
installCallback('resetAllChatColors');
installCallback('resetDefaultChatColor');
installCallback('setGlobalDefaultConversationColor');
installCallback('getDefaultConversationColor');
installSetting('backupFeatureEnabled', {
setter: false,
});
installSetting('backupSubscriptionStatus', {
setter: false,
});
installSetting('cloudBackupStatus', {
setter: false,
});
// Getters only. These are set by the primary device
installSetting('blockedCount', {
setter: false,
});
installSetting('linkPreviewSetting', {
setter: false,
});
installSetting('readReceiptSetting', {
setter: false,
});
installSetting('typingIndicatorSetting', {
setter: false,
});
installCallback('refreshCloudBackupStatus');
installCallback('refreshBackupSubscriptionStatus');
installCallback('deleteAllMyStories');
installCallback('isPrimary');
installCallback('isInternalUser');
installCallback('syncRequest');
installCallback('getEmojiSkinToneDefault');
installCallback('setEmojiSkinToneDefault');
installCallback('exportLocalBackup');
installCallback('importLocalBackup');
installCallback('validateBackup');
installSetting('alwaysRelayCalls');
installSetting('audioMessage');
installSetting('audioNotification');
installSetting('autoConvertEmoji');
installSetting('autoDownloadUpdate');
installSetting('autoDownloadAttachment');
installSetting('autoLaunch');
installSetting('callRingtoneNotification');
installSetting('callSystemNotification');
installSetting('countMutedConversations');
installSetting('deviceName');
installSetting('phoneNumber');
installSetting('hasStoriesDisabled');
installSetting('hideMenuBar');
installSetting('incomingCallNotification');
installSetting('lastSyncTime');
installSetting('notificationDrawAttention');
installSetting('notificationSetting');
installSetting('sentMediaQualitySetting');
installSetting('textFormatting');
installSetting('universalExpireTimer');
installSetting('zoomFactor');
installSetting('phoneNumberDiscoverabilitySetting');
installSetting('phoneNumberSharingSetting');
// Media Settings
installCallback('getAvailableIODevices');
installSetting('preferredAudioInputDevice');
installSetting('preferredAudioOutputDevice');
installSetting('preferredVideoInputDevice');
installEphemeralSetting('themeSetting');
installEphemeralSetting('systemTraySetting');
installEphemeralSetting('contentProtection');
installEphemeralSetting('localeOverride');
installEphemeralSetting('spellCheck');
installEphemeralSetting('systemTraySetting');
installEphemeralSetting('themeSetting');

View file

@ -1,271 +0,0 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom';
import type { PropsPreloadType } from '../../components/Preferences';
import { i18n } from '../sandboxedInit';
import { Preferences } from '../../components/Preferences';
import { startInteractionMode } from '../../services/InteractionMode';
import { strictAssert } from '../../util/assert';
import { parseEnvironment, setEnvironment } from '../../environment';
const { SettingsWindowProps } = window.Signal;
strictAssert(SettingsWindowProps, 'window values not provided');
startInteractionMode();
setEnvironment(
parseEnvironment(window.SignalContext.getEnvironment()),
window.SignalContext.isTestOrMockEnvironment()
);
SettingsWindowProps.onRender(
({
addCustomColor,
autoDownloadAttachment,
availableCameras,
availableLocales,
availableMicrophones,
availableSpeakers,
backupFeatureEnabled,
backupSubscriptionStatus,
blockedCount,
closeSettings,
cloudBackupStatus,
customColors,
defaultConversationColor,
deviceName,
emojiSkinToneDefault,
phoneNumber,
doDeleteAllData,
doneRendering,
editCustomColor,
exportLocalBackup,
getConversationsWithCustomColor,
hasAudioNotifications,
hasAutoConvertEmoji,
hasAutoDownloadUpdate,
hasAutoLaunch,
hasCallNotifications,
hasCallRingtoneNotification,
hasContentProtection,
hasCountMutedConversations,
hasHideMenuBar,
hasIncomingCallNotifications,
hasLinkPreviews,
hasMediaCameraPermissions,
hasMediaPermissions,
hasMessageAudio,
hasMinimizeToAndStartInSystemTray,
hasMinimizeToSystemTray,
hasNotificationAttention,
hasNotifications,
hasReadReceipts,
hasRelayCalls,
hasSpellCheck,
hasStoriesDisabled,
hasTextFormatting,
hasTypingIndicators,
importLocalBackup,
initialSpellCheckSetting,
isAutoDownloadUpdatesSupported,
isAutoLaunchSupported,
isHideMenuBarSupported,
isMinimizeToAndStartInSystemTraySupported,
isNotificationAttentionSupported,
isSyncSupported,
isSystemTraySupported,
isContentProtectionSupported,
isContentProtectionNeeded,
isInternalUser,
lastSyncTime,
makeSyncRequest,
notificationContent,
onAudioNotificationsChange,
onAutoConvertEmojiChange,
onAutoDownloadAttachmentChange,
onAutoDownloadUpdateChange,
onAutoLaunchChange,
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,
onUniversalExpireTimerChange,
onWhoCanFindMeChange,
onWhoCanSeeMeChange,
onZoomFactorChange,
preferredSystemLocales,
refreshCloudBackupStatus,
refreshBackupSubscriptionStatus,
removeCustomColor,
removeCustomColorOnConversations,
resetAllChatColors,
resetDefaultChatColor,
resolvedLocale,
selectedCamera,
selectedMicrophone,
selectedSpeaker,
sentMediaQualitySetting,
setGlobalDefaultConversationColor,
localeOverride,
themeSetting,
universalExpireTimer,
validateBackup,
whoCanFindMe,
whoCanSeeMe,
zoomFactor,
}: PropsPreloadType) => {
ReactDOM.render(
<StrictMode>
<Preferences
addCustomColor={addCustomColor}
autoDownloadAttachment={autoDownloadAttachment}
availableCameras={availableCameras}
availableLocales={availableLocales}
availableMicrophones={availableMicrophones}
availableSpeakers={availableSpeakers}
backupFeatureEnabled={backupFeatureEnabled}
backupSubscriptionStatus={backupSubscriptionStatus}
blockedCount={blockedCount}
closeSettings={closeSettings}
cloudBackupStatus={cloudBackupStatus}
customColors={customColors}
defaultConversationColor={defaultConversationColor}
deviceName={deviceName}
emojiSkinToneDefault={emojiSkinToneDefault}
exportLocalBackup={exportLocalBackup}
phoneNumber={phoneNumber}
doDeleteAllData={doDeleteAllData}
doneRendering={doneRendering}
editCustomColor={editCustomColor}
getConversationsWithCustomColor={getConversationsWithCustomColor}
hasAudioNotifications={hasAudioNotifications}
hasAutoConvertEmoji={hasAutoConvertEmoji}
hasAutoDownloadUpdate={hasAutoDownloadUpdate}
hasAutoLaunch={hasAutoLaunch}
hasCallNotifications={hasCallNotifications}
hasCallRingtoneNotification={hasCallRingtoneNotification}
hasContentProtection={hasContentProtection}
hasCountMutedConversations={hasCountMutedConversations}
hasHideMenuBar={hasHideMenuBar}
hasIncomingCallNotifications={hasIncomingCallNotifications}
hasLinkPreviews={hasLinkPreviews}
hasMediaCameraPermissions={hasMediaCameraPermissions}
hasMediaPermissions={hasMediaPermissions}
hasMessageAudio={hasMessageAudio}
hasMinimizeToAndStartInSystemTray={hasMinimizeToAndStartInSystemTray}
hasMinimizeToSystemTray={hasMinimizeToSystemTray}
hasNotificationAttention={hasNotificationAttention}
hasNotifications={hasNotifications}
hasReadReceipts={hasReadReceipts}
hasRelayCalls={hasRelayCalls}
hasSpellCheck={hasSpellCheck}
hasStoriesDisabled={hasStoriesDisabled}
hasTextFormatting={hasTextFormatting}
hasTypingIndicators={hasTypingIndicators}
i18n={i18n}
importLocalBackup={importLocalBackup}
initialSpellCheckSetting={initialSpellCheckSetting}
isAutoDownloadUpdatesSupported={isAutoDownloadUpdatesSupported}
isAutoLaunchSupported={isAutoLaunchSupported}
isHideMenuBarSupported={isHideMenuBarSupported}
isMinimizeToAndStartInSystemTraySupported={
isMinimizeToAndStartInSystemTraySupported
}
isNotificationAttentionSupported={isNotificationAttentionSupported}
isSyncSupported={isSyncSupported}
isSystemTraySupported={isSystemTraySupported}
isContentProtectionSupported={isContentProtectionSupported}
isContentProtectionNeeded={isContentProtectionNeeded}
isInternalUser={isInternalUser}
lastSyncTime={lastSyncTime}
localeOverride={localeOverride}
makeSyncRequest={makeSyncRequest}
notificationContent={notificationContent}
onAudioNotificationsChange={onAudioNotificationsChange}
onAutoConvertEmojiChange={onAutoConvertEmojiChange}
onAutoDownloadAttachmentChange={onAutoDownloadAttachmentChange}
onAutoDownloadUpdateChange={onAutoDownloadUpdateChange}
onAutoLaunchChange={onAutoLaunchChange}
onCallNotificationsChange={onCallNotificationsChange}
onCallRingtoneNotificationChange={onCallRingtoneNotificationChange}
onContentProtectionChange={onContentProtectionChange}
onCountMutedConversationsChange={onCountMutedConversationsChange}
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
onHasStoriesDisabledChanged={onHasStoriesDisabledChanged}
onHideMenuBarChange={onHideMenuBarChange}
onIncomingCallNotificationsChange={onIncomingCallNotificationsChange}
onLastSyncTimeChange={onLastSyncTimeChange}
onLocaleChange={onLocaleChange}
onMediaCameraPermissionsChange={onMediaCameraPermissionsChange}
onMediaPermissionsChange={onMediaPermissionsChange}
onMessageAudioChange={onMessageAudioChange}
onMinimizeToAndStartInSystemTrayChange={
onMinimizeToAndStartInSystemTrayChange
}
onMinimizeToSystemTrayChange={onMinimizeToSystemTrayChange}
onNotificationAttentionChange={onNotificationAttentionChange}
onNotificationContentChange={onNotificationContentChange}
onNotificationsChange={onNotificationsChange}
onRelayCallsChange={onRelayCallsChange}
onSelectedCameraChange={onSelectedCameraChange}
onSelectedMicrophoneChange={onSelectedMicrophoneChange}
onSelectedSpeakerChange={onSelectedSpeakerChange}
onSentMediaQualityChange={onSentMediaQualityChange}
onSpellCheckChange={onSpellCheckChange}
onTextFormattingChange={onTextFormattingChange}
onThemeChange={onThemeChange}
onUniversalExpireTimerChange={onUniversalExpireTimerChange}
onWhoCanFindMeChange={onWhoCanFindMeChange}
onWhoCanSeeMeChange={onWhoCanSeeMeChange}
onZoomFactorChange={onZoomFactorChange}
preferredSystemLocales={preferredSystemLocales}
refreshCloudBackupStatus={refreshCloudBackupStatus}
refreshBackupSubscriptionStatus={refreshBackupSubscriptionStatus}
removeCustomColorOnConversations={removeCustomColorOnConversations}
removeCustomColor={removeCustomColor}
resetAllChatColors={resetAllChatColors}
resetDefaultChatColor={resetDefaultChatColor}
resolvedLocale={resolvedLocale}
selectedCamera={selectedCamera}
selectedMicrophone={selectedMicrophone}
selectedSpeaker={selectedSpeaker}
sentMediaQualitySetting={sentMediaQualitySetting}
setGlobalDefaultConversationColor={setGlobalDefaultConversationColor}
themeSetting={themeSetting}
universalExpireTimer={universalExpireTimer}
validateBackup={validateBackup}
whoCanFindMe={whoCanFindMe}
whoCanSeeMe={whoCanSeeMe}
zoomFactor={zoomFactor}
/>
</StrictMode>,
document.getElementById('app')
);
}
);

View file

@ -1,537 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { contextBridge, ipcRenderer } from 'electron';
import { MinimalSignalContext } from '../minimalContext';
import type { PropsPreloadType } from '../../components/Preferences';
import OS from '../../util/os/osPreload';
import * as Settings from '../../types/Settings';
import {
SystemTraySetting,
parseSystemTraySetting,
shouldMinimizeToSystemTray,
} from '../../types/SystemTraySetting';
import { awaitObject } from '../../util/awaitObject';
import { DurationInSeconds } from '../../util/durations';
import { createSetting, createCallback } from '../../util/preload';
import { findBestMatchingAudioDeviceIndex } from '../../calling/findBestMatchingDevice';
import type { EmojiSkinTone } from '../../components/fun/data/emojis';
function doneRendering() {
ipcRenderer.send('settings-done-rendering');
}
const settingMessageAudio = createSetting('audioMessage');
const settingAudioNotification = createSetting('audioNotification');
const settingAutoConvertEmoji = createSetting('autoConvertEmoji');
const settingAutoDownloadUpdate = createSetting('autoDownloadUpdate');
const settingAutoDownloadAttachment = createSetting('autoDownloadAttachment');
const settingAutoLaunch = createSetting('autoLaunch');
const settingCallRingtoneNotification = createSetting(
'callRingtoneNotification'
);
const settingCallSystemNotification = createSetting('callSystemNotification');
const settingCountMutedConversations = createSetting('countMutedConversations');
const settingDeviceName = createSetting('deviceName', { setter: false });
const settingPhoneNumber = createSetting('phoneNumber', { setter: false });
const settingHideMenuBar = createSetting('hideMenuBar');
const settingIncomingCallNotification = createSetting(
'incomingCallNotification'
);
const settingMediaCameraPermissions = createSetting('mediaCameraPermissions');
const settingMediaPermissions = createSetting('mediaPermissions');
const settingNotificationDrawAttention = createSetting(
'notificationDrawAttention'
);
const settingNotificationSetting = createSetting('notificationSetting');
const settingRelayCalls = createSetting('alwaysRelayCalls');
const settingSentMediaQuality = createSetting('sentMediaQualitySetting');
const settingSpellCheck = createSetting('spellCheck');
const settingContentProtection = createSetting('contentProtection');
const settingTextFormatting = createSetting('textFormatting');
const settingTheme = createSetting('themeSetting');
const settingSystemTraySetting = createSetting('systemTraySetting');
const settingLocaleOverride = createSetting('localeOverride');
const settingLastSyncTime = createSetting('lastSyncTime');
const settingHasStoriesDisabled = createSetting('hasStoriesDisabled');
const settingZoomFactor = createSetting('zoomFactor');
// Getters only.
const settingBlockedCount = createSetting('blockedCount');
const settingBackupFeatureEnabled = createSetting('backupFeatureEnabled', {
setter: false,
});
const settingCloudBackupStatus = createSetting('cloudBackupStatus', {
setter: false,
});
const settingBackupSubscriptionStatus = createSetting(
'backupSubscriptionStatus',
{
setter: false,
}
);
const settingLinkPreview = createSetting('linkPreviewSetting', {
setter: false,
});
const settingPhoneNumberDiscoverability = createSetting(
'phoneNumberDiscoverabilitySetting'
);
const settingPhoneNumberSharing = createSetting('phoneNumberSharingSetting');
const settingReadReceipts = createSetting('readReceiptSetting', {
setter: false,
});
const settingTypingIndicators = createSetting('typingIndicatorSetting', {
setter: false,
});
// Media settings
const settingAudioInput = createSetting('preferredAudioInputDevice');
const settingAudioOutput = createSetting('preferredAudioOutputDevice');
const settingVideoInput = createSetting('preferredVideoInputDevice');
const settingUniversalExpireTimer = createSetting('universalExpireTimer');
// Callbacks
const ipcGetAvailableIODevices = createCallback('getAvailableIODevices');
const ipcGetCustomColors = createCallback('getCustomColors');
const ipcGetEmojiSkinToneDefault = createCallback('getEmojiSkinToneDefault');
const ipcIsSyncNotSupported = createCallback('isPrimary');
const ipcIsInternalUser = createCallback('isInternalUser');
const ipcMakeSyncRequest = createCallback('syncRequest');
const ipcExportLocalBackup = createCallback('exportLocalBackup');
const ipcImportLocalBackup = createCallback('importLocalBackup');
const ipcValidateBackup = createCallback('validateBackup');
const ipcDeleteAllMyStories = createCallback('deleteAllMyStories');
const ipcRefreshCloudBackupStatus = createCallback('refreshCloudBackupStatus');
const ipcRefreshBackupSubscriptionStatus = createCallback(
'refreshBackupSubscriptionStatus'
);
// ChatColorPicker redux hookups
// The redux actions update over IPC through a preferences re-render
const ipcGetDefaultConversationColor = createCallback(
'getDefaultConversationColor'
);
const ipcGetConversationsWithCustomColor = createCallback(
'getConversationsWithCustomColor'
);
const ipcAddCustomColor = createCallback('addCustomColor');
const ipcEditCustomColor = createCallback('editCustomColor');
const ipcRemoveCustomColor = createCallback('removeCustomColor');
const ipcRemoveCustomColorOnConversations = createCallback(
'removeCustomColorOnConversations'
);
const ipcResetAllChatColors = createCallback('resetAllChatColors');
const ipcResetDefaultChatColor = createCallback('resetDefaultChatColor');
const ipcSetGlobalDefaultConversationColor = createCallback(
'setGlobalDefaultConversationColor'
);
const ipcSetEmojiSkinToneDefault = createCallback('setEmojiSkinToneDefault');
const DEFAULT_NOTIFICATION_SETTING = 'message';
function getSystemTraySettingValues(systemTraySetting: SystemTraySetting): {
hasMinimizeToAndStartInSystemTray: boolean;
hasMinimizeToSystemTray: boolean;
} {
const parsedSystemTraySetting = parseSystemTraySetting(systemTraySetting);
const hasMinimizeToAndStartInSystemTray =
parsedSystemTraySetting ===
SystemTraySetting.MinimizeToAndStartInSystemTray;
const hasMinimizeToSystemTray = shouldMinimizeToSystemTray(
parsedSystemTraySetting
);
return {
hasMinimizeToAndStartInSystemTray,
hasMinimizeToSystemTray,
};
}
let renderInBrowser = (_props: PropsPreloadType): void => {
throw new Error('render is not defined');
};
function attachRenderCallback<Value>(f: (value: Value) => Promise<Value>) {
return async (value: Value) => {
await f(value);
void renderPreferences();
};
}
async function renderPreferences() {
const {
autoDownloadAttachment,
backupFeatureEnabled,
backupSubscriptionStatus,
blockedCount,
cloudBackupStatus,
deviceName,
emojiSkinToneDefault,
hasAudioNotifications,
hasAutoConvertEmoji,
hasAutoDownloadUpdate,
hasAutoLaunch,
hasCallNotifications,
hasCallRingtoneNotification,
hasContentProtection,
hasCountMutedConversations,
hasHideMenuBar,
hasIncomingCallNotifications,
hasLinkPreviews,
hasMediaCameraPermissions,
hasMediaPermissions,
hasMessageAudio,
hasNotificationAttention,
hasReadReceipts,
hasRelayCalls,
hasSpellCheck,
hasStoriesDisabled,
hasTextFormatting,
hasTypingIndicators,
lastSyncTime,
notificationContent,
phoneNumber,
selectedCamera,
selectedMicrophone,
selectedSpeaker,
sentMediaQualitySetting,
localeOverride,
systemTraySetting,
themeSetting,
universalExpireTimer,
whoCanFindMe,
whoCanSeeMe,
zoomFactor,
availableIODevices,
customColors,
defaultConversationColor,
isSyncNotSupported,
isInternalUser,
} = await awaitObject({
autoDownloadAttachment: settingAutoDownloadAttachment.getValue(),
backupFeatureEnabled: settingBackupFeatureEnabled.getValue(),
backupSubscriptionStatus: settingBackupSubscriptionStatus.getValue(),
blockedCount: settingBlockedCount.getValue(),
cloudBackupStatus: settingCloudBackupStatus.getValue(),
deviceName: settingDeviceName.getValue(),
hasAudioNotifications: settingAudioNotification.getValue(),
hasAutoConvertEmoji: settingAutoConvertEmoji.getValue(),
hasAutoDownloadUpdate: settingAutoDownloadUpdate.getValue(),
hasAutoLaunch: settingAutoLaunch.getValue(),
hasCallNotifications: settingCallSystemNotification.getValue(),
hasCallRingtoneNotification: settingCallRingtoneNotification.getValue(),
hasContentProtection: settingContentProtection.getValue(),
hasCountMutedConversations: settingCountMutedConversations.getValue(),
hasHideMenuBar: settingHideMenuBar.getValue(),
hasIncomingCallNotifications: settingIncomingCallNotification.getValue(),
hasLinkPreviews: settingLinkPreview.getValue(),
hasMediaCameraPermissions: settingMediaCameraPermissions.getValue(),
hasMediaPermissions: settingMediaPermissions.getValue(),
hasMessageAudio: settingMessageAudio.getValue(),
hasNotificationAttention: settingNotificationDrawAttention.getValue(),
hasReadReceipts: settingReadReceipts.getValue(),
hasRelayCalls: settingRelayCalls.getValue(),
hasSpellCheck: settingSpellCheck.getValue(),
hasStoriesDisabled: settingHasStoriesDisabled.getValue(),
hasTextFormatting: settingTextFormatting.getValue(),
hasTypingIndicators: settingTypingIndicators.getValue(),
lastSyncTime: settingLastSyncTime.getValue(),
notificationContent: settingNotificationSetting.getValue(),
phoneNumber: settingPhoneNumber.getValue(),
selectedCamera: settingVideoInput.getValue(),
selectedMicrophone: settingAudioInput.getValue(),
selectedSpeaker: settingAudioOutput.getValue(),
sentMediaQualitySetting: settingSentMediaQuality.getValue(),
localeOverride: settingLocaleOverride.getValue(),
systemTraySetting: settingSystemTraySetting.getValue(),
themeSetting: settingTheme.getValue(),
universalExpireTimer: settingUniversalExpireTimer.getValue(),
whoCanFindMe: settingPhoneNumberDiscoverability.getValue(),
whoCanSeeMe: settingPhoneNumberSharing.getValue(),
zoomFactor: settingZoomFactor.getValue(),
// Callbacks
availableIODevices: ipcGetAvailableIODevices(),
customColors: ipcGetCustomColors(),
defaultConversationColor: ipcGetDefaultConversationColor(),
emojiSkinToneDefault: ipcGetEmojiSkinToneDefault(),
isSyncNotSupported: ipcIsSyncNotSupported(),
isInternalUser: ipcIsInternalUser(),
});
const { availableCameras, availableMicrophones, availableSpeakers } =
availableIODevices;
const { hasMinimizeToAndStartInSystemTray, hasMinimizeToSystemTray } =
getSystemTraySettingValues(systemTraySetting);
const onUniversalExpireTimerChange = attachRenderCallback(
settingUniversalExpireTimer.setValue
);
const availableLocales = MinimalSignalContext.getI18nAvailableLocales();
const resolvedLocale = MinimalSignalContext.getI18nLocale();
const preferredSystemLocales =
MinimalSignalContext.getPreferredSystemLocales();
const selectedMicIndex = findBestMatchingAudioDeviceIndex(
{
available: availableMicrophones,
preferred: selectedMicrophone,
},
OS.isWindows()
);
const recomputedSelectedMicrophone =
selectedMicIndex !== undefined
? availableMicrophones[selectedMicIndex]
: undefined;
const selectedSpeakerIndex = findBestMatchingAudioDeviceIndex(
{
available: availableSpeakers,
preferred: selectedSpeaker,
},
OS.isWindows()
);
const recomputedSelectedSpeaker =
selectedSpeakerIndex !== undefined
? availableSpeakers[selectedSpeakerIndex]
: undefined;
const props: PropsPreloadType = {
// Settings
autoDownloadAttachment,
availableCameras,
availableLocales,
availableMicrophones,
availableSpeakers,
backupFeatureEnabled,
backupSubscriptionStatus,
blockedCount,
cloudBackupStatus,
customColors,
defaultConversationColor,
deviceName,
emojiSkinToneDefault,
hasAudioNotifications,
hasAutoConvertEmoji,
hasAutoDownloadUpdate,
hasAutoLaunch,
hasCallNotifications,
hasCallRingtoneNotification,
hasContentProtection:
hasContentProtection ??
Settings.isContentProtectionEnabledByDefault(
OS,
MinimalSignalContext.config.osRelease
),
hasCountMutedConversations,
hasHideMenuBar,
hasIncomingCallNotifications,
hasLinkPreviews,
hasMediaCameraPermissions,
hasMediaPermissions,
hasMessageAudio,
hasMinimizeToAndStartInSystemTray,
hasMinimizeToSystemTray,
hasNotificationAttention,
hasNotifications: notificationContent !== 'off',
hasReadReceipts,
hasRelayCalls,
hasSpellCheck,
hasStoriesDisabled,
hasTextFormatting,
hasTypingIndicators,
lastSyncTime,
localeOverride,
notificationContent,
phoneNumber,
preferredSystemLocales,
resolvedLocale,
selectedCamera,
selectedMicrophone: recomputedSelectedMicrophone,
selectedSpeaker: recomputedSelectedSpeaker,
sentMediaQualitySetting,
themeSetting,
universalExpireTimer: DurationInSeconds.fromSeconds(universalExpireTimer),
whoCanFindMe,
whoCanSeeMe,
zoomFactor,
// Actions and other props
addCustomColor: ipcAddCustomColor,
closeSettings: () => MinimalSignalContext.executeMenuRole('close'),
doDeleteAllData: () => ipcRenderer.send('delete-all-data'),
doneRendering,
editCustomColor: ipcEditCustomColor,
getConversationsWithCustomColor: ipcGetConversationsWithCustomColor,
initialSpellCheckSetting:
MinimalSignalContext.config.appStartInitialSpellcheckSetting,
makeSyncRequest: ipcMakeSyncRequest,
refreshCloudBackupStatus: ipcRefreshCloudBackupStatus,
refreshBackupSubscriptionStatus: ipcRefreshBackupSubscriptionStatus,
removeCustomColor: ipcRemoveCustomColor,
removeCustomColorOnConversations: ipcRemoveCustomColorOnConversations,
resetAllChatColors: ipcResetAllChatColors,
resetDefaultChatColor: ipcResetDefaultChatColor,
setGlobalDefaultConversationColor: ipcSetGlobalDefaultConversationColor,
exportLocalBackup: ipcExportLocalBackup,
importLocalBackup: ipcImportLocalBackup,
validateBackup: ipcValidateBackup,
// Limited support features
isAutoDownloadUpdatesSupported: Settings.isAutoDownloadUpdatesSupported(
OS,
MinimalSignalContext.getVersion()
),
isAutoLaunchSupported: Settings.isAutoLaunchSupported(OS),
isHideMenuBarSupported: Settings.isHideMenuBarSupported(OS),
isNotificationAttentionSupported: Settings.isDrawAttentionSupported(OS),
isSyncSupported: !isSyncNotSupported,
isInternalUser,
isSystemTraySupported: Settings.isSystemTraySupported(OS),
isContentProtectionSupported: Settings.isContentProtectionSupported(OS),
isContentProtectionNeeded: Settings.isContentProtectionNeeded(OS),
isMinimizeToAndStartInSystemTraySupported:
Settings.isMinimizeToAndStartInSystemTraySupported(OS),
// Change handlers
onAudioNotificationsChange: attachRenderCallback(
settingAudioNotification.setValue
),
onAutoConvertEmojiChange: attachRenderCallback(
settingAutoConvertEmoji.setValue
),
onAutoDownloadUpdateChange: attachRenderCallback(
settingAutoDownloadUpdate.setValue
),
onAutoDownloadAttachmentChange: attachRenderCallback(
settingAutoDownloadAttachment.setValue
),
onAutoLaunchChange: attachRenderCallback(settingAutoLaunch.setValue),
onCallNotificationsChange: attachRenderCallback(
settingCallSystemNotification.setValue
),
onCallRingtoneNotificationChange: attachRenderCallback(
settingCallRingtoneNotification.setValue
),
onContentProtectionChange: attachRenderCallback(
settingContentProtection.setValue
),
onCountMutedConversationsChange: attachRenderCallback(
settingCountMutedConversations.setValue
),
onEmojiSkinToneDefaultChange: attachRenderCallback(
async (emojiSkinTone: EmojiSkinTone) => {
await ipcSetEmojiSkinToneDefault(emojiSkinTone);
return emojiSkinTone;
}
),
onHasStoriesDisabledChanged: attachRenderCallback(
async (value: boolean) => {
await settingHasStoriesDisabled.setValue(value);
if (!value) {
void ipcDeleteAllMyStories();
}
return value;
}
),
onHideMenuBarChange: attachRenderCallback(settingHideMenuBar.setValue),
onIncomingCallNotificationsChange: attachRenderCallback(
settingIncomingCallNotification.setValue
),
onLastSyncTimeChange: attachRenderCallback(settingLastSyncTime.setValue),
onLocaleChange: async (locale: string | null) => {
await settingLocaleOverride.setValue(locale);
MinimalSignalContext.restartApp();
},
onMediaCameraPermissionsChange: attachRenderCallback(
settingMediaCameraPermissions.setValue
),
onMessageAudioChange: attachRenderCallback(settingMessageAudio.setValue),
onMinimizeToAndStartInSystemTrayChange: attachRenderCallback(
async (value: boolean) => {
await settingSystemTraySetting.setValue(
value
? SystemTraySetting.MinimizeToAndStartInSystemTray
: SystemTraySetting.MinimizeToSystemTray
);
return value;
}
),
onMinimizeToSystemTrayChange: attachRenderCallback(
async (value: boolean) => {
await settingSystemTraySetting.setValue(
value
? SystemTraySetting.MinimizeToSystemTray
: SystemTraySetting.DoNotUseSystemTray
);
return value;
}
),
onMediaPermissionsChange: attachRenderCallback(
settingMediaPermissions.setValue
),
onNotificationAttentionChange: attachRenderCallback(
settingNotificationDrawAttention.setValue
),
onNotificationContentChange: attachRenderCallback(
settingNotificationSetting.setValue
),
onNotificationsChange: attachRenderCallback(async (value: boolean) => {
await settingNotificationSetting.setValue(
value ? DEFAULT_NOTIFICATION_SETTING : 'off'
);
return value;
}),
onRelayCallsChange: attachRenderCallback(settingRelayCalls.setValue),
onSelectedCameraChange: attachRenderCallback(settingVideoInput.setValue),
onSelectedMicrophoneChange: attachRenderCallback(
settingAudioInput.setValue
),
onSelectedSpeakerChange: attachRenderCallback(settingAudioOutput.setValue),
onSentMediaQualityChange: attachRenderCallback(
settingSentMediaQuality.setValue
),
onSpellCheckChange: attachRenderCallback(settingSpellCheck.setValue),
onTextFormattingChange: attachRenderCallback(
settingTextFormatting.setValue
),
onThemeChange: attachRenderCallback(settingTheme.setValue),
onUniversalExpireTimerChange: (newValue: number): Promise<void> => {
return onUniversalExpireTimerChange(
DurationInSeconds.fromSeconds(newValue)
);
},
onWhoCanFindMeChange: attachRenderCallback(
settingPhoneNumberDiscoverability.setValue
),
onWhoCanSeeMeChange: attachRenderCallback(
settingPhoneNumberSharing.setValue
),
onZoomFactorChange: (zoomFactorValue: number) => {
ipcRenderer.send('setZoomFactor', zoomFactorValue);
},
};
renderInBrowser(props);
}
ipcRenderer.on('preferences-changed', renderPreferences);
ipcRenderer.on('zoomFactorChanged', renderPreferences);
const Signal = {
SettingsWindowProps: {
onRender: (renderer: (_props: PropsPreloadType) => void) => {
renderInBrowser = renderer;
void renderPreferences();
},
},
};
contextBridge.exposeInMainWorld('Signal', Signal);
contextBridge.exposeInMainWorld('SignalContext', MinimalSignalContext);