PNP Settings

This commit is contained in:
Fedor Indutny 2023-02-23 13:32:19 -08:00 committed by GitHub
parent 5bcf71ef2c
commit 5d110964b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 562 additions and 149 deletions

View file

@ -5429,23 +5429,75 @@
}, },
"Preferences__who-can--title": { "Preferences__who-can--title": {
"message": "Who can...", "message": "Who can...",
"description": "Title for the 'who can do X' setting" "description": "(deleted 2022/02/14) Title for the 'who can do X' setting"
}, },
"Preferences__privacy--description": { "Preferences__privacy--description": {
"message": "To change these settings, open the Signal app on your mobile device and navigate to Settings > Privacy", "message": "To change these settings, open the Signal app on your mobile device and navigate to Settings > Privacy",
"description": "Description for the 'who can do X' setting" "description": "(deleted 2022/02/14) Description for the 'who can do X' setting"
}, },
"Preferences__who-can--everybody": { "Preferences__who-can--everybody": {
"message": "Everybody", "message": "Everybody",
"description": "Option for who can see my X select" "description": "(deleted 2022/02/14) Option for who can see my X select"
}, },
"Preferences__who-can--contacts": { "Preferences__who-can--contacts": {
"message": "My Contacts", "message": "My Contacts",
"description": "Option for who can see my X select" "description": "(deleted 2022/02/14) Option for who can see my X select"
}, },
"Preferences__who-can--nobody": { "Preferences__who-can--nobody": {
"message": "Nobody", "message": "Nobody",
"description": "Option for who can see my X select" "description": "(deleted 2022/02/14) Option for who can see my X select"
},
"icu:Preferences__pnp__row--title": {
"messageformat": "Phone Number",
"description": "Title of Phone Number row in Privacy section of Preferences window"
},
"icu:Preferences__pnp__row--body": {
"messageformat": "Choose who can see your phone number and who can contact you on Signal with it.",
"description": "Body of Phone Number row in Privacy section of Preferences window"
},
"icu:Preferences__pnp__sharing--title": {
"messageformat": "Who can see my number",
"description": "Title for the phone number sharing setting row"
},
"icu:Preferences__pnp__sharing--description--everyone": {
"messageformat": "Your phone number will be visible to people and groups you message. People who have your number in their phone contacts will also see it on Signal.",
"description": "Description for the phone number sharing setting row when the value is Everyone"
},
"icu:Preferences__pnp__sharing--description--nobody": {
"messageformat": "Nobody will see your phone number on Signal.",
"description": "Description for the phone number sharing setting row when the value is Nobody"
},
"icu:Preferences__pnp--page-title": {
"messageformat": "Phone Number",
"description": "Title of the page in Phone Number Privacy settings"
},
"icu:Preferences__pnp__sharing__everyone": {
"messageformat": "Everyone",
"description": "Option for sharing phone number with everyone"
},
"icu:Preferences__pnp__sharing__nobody": {
"messageformat": "Nobody",
"description": "Option for sharing phone number with nobody"
},
"icu:Preferences__pnp__discoverability--title": {
"messageformat": "Who can find me by number",
"description": "Title for the phone number discoverability setting row"
},
"icu:Preferences__pnp__discoverability--description--everyone": {
"messageformat": "Anyone who has your phone number in their contacts will see you as a contact on Signal. Others will be able to reach you with your phone number when they start a new chat or group.",
"description": "Description for the phone number discoverability setting row wth the value is everyone"
},
"icu:Preferences__pnp__discoverability--description--nobody": {
"messageformat": "Nobody on Signal will be able to reach you with your phone number.",
"description": "Description for the phone number discoverability setting row wth the value is nobody"
},
"icu:Preferences__pnp__discoverability__everyone": {
"messageformat": "Everyone",
"description": "Option for letting everyone discover you by phone number"
},
"icu:Preferences__pnp__discoverability__nobody": {
"messageformat": "Nobody",
"description": "Option for letting nobody discover you by phone number"
}, },
"Preferences--messaging": { "Preferences--messaging": {
"message": "Messaging", "message": "Messaging",

View file

@ -29,7 +29,6 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="text/javascript" src="ts/set_os_class.js"></script>
<script type="application/javascript" src="ts/windows/init.js"></script> <script type="application/javascript" src="ts/windows/init.js"></script>
</body> </body>
</html> </html>

View file

@ -1268,7 +1268,7 @@ async function showSettingsWindow() {
frame: true, frame: true,
resizable: false, resizable: false,
title: getResolvedMessagesLocale().i18n('signalDesktopPreferences'), title: getResolvedMessagesLocale().i18n('signalDesktopPreferences'),
titleBarStyle: nonMainTitleBarStyle, titleBarStyle: mainTitleBarStyle,
titleBarOverlay, titleBarOverlay,
autoHideMenuBar: true, autoHideMenuBar: true,
backgroundColor: await getBackgroundColor(), backgroundColor: await getBackgroundColor(),

View file

@ -33,7 +33,6 @@
type="application/javascript" type="application/javascript"
src="ts/windows/applyTheme.js" src="ts/windows/applyTheme.js"
></script> ></script>
<script type="text/javascript" src="ts/set_os_class.js"></script>
<script type="application/javascript" src="ts/windows/init.js"></script> <script type="application/javascript" src="ts/windows/init.js"></script>
</body> </body>
</html> </html>

View file

@ -52,7 +52,7 @@
"danger:local": "./danger/danger.sh local --base main", "danger:local": "./danger/danger.sh local --base main",
"danger:ci": "./danger/danger.sh ci --base origin/main", "danger:ci": "./danger/danger.sh ci --base origin/main",
"format": "pprettier --write '**/*.{ts,tsx,d.ts,js,json,html,scss,md,yml,yaml}' '!node_modules/**'", "format": "pprettier --write '**/*.{ts,tsx,d.ts,js,json,html,scss,md,yml,yaml}' '!node_modules/**'",
"svgo": "svgo images/**/*.svg", "svgo": "svgo --multipass images/**/*.svg",
"transpile": "run-p check:types build:esbuild", "transpile": "run-p check:types build:esbuild",
"check:types": "tsc --noEmit", "check:types": "tsc --noEmit",
"clean-transpile-once": "rimraf app/**/*.js app/*.js sticker-creator/**/*.js sticker-creator/*.js ts/**/*.js ts/*.js tsconfig.tsbuildinfo", "clean-transpile-once": "rimraf app/**/*.js app/*.js sticker-creator/**/*.js sticker-creator/*.js ts/**/*.js ts/*.js tsconfig.tsbuildinfo",

View file

@ -33,7 +33,6 @@
type="application/javascript" type="application/javascript"
src="ts/windows/applyTheme.js" src="ts/windows/applyTheme.js"
></script> ></script>
<script type="text/javascript" src="ts/set_os_class.js"></script>
<script type="application/javascript" src="ts/windows/init.js"></script> <script type="application/javascript" src="ts/windows/init.js"></script>
</body> </body>
</html> </html>

View file

@ -14,6 +14,5 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="text/javascript" src="../../js/components.js"></script> <script type="text/javascript" src="../../js/components.js"></script>
<script type="text/javascript" src="../../ts/set_os_class.js"></script>
</body> </body>
</html> </html>

View file

@ -7,7 +7,7 @@
height: 20px; height: 20px;
width: 20px; width: 20px;
input[type='checkbox'] { input {
cursor: pointer; cursor: pointer;
height: 0; height: 0;
position: absolute; position: absolute;
@ -39,25 +39,6 @@
} }
} }
&:checked {
&::before {
background: $color-ultramarine;
border: 1.5px solid $color-ultramarine;
}
&::after {
border: solid $color-white;
border-width: 0 2px 2px 0;
content: '';
display: block;
height: 11px;
left: 7px;
position: absolute;
top: 3px;
transform: rotate(45deg);
width: 6px;
}
}
&:disabled { &:disabled {
cursor: inherit; cursor: inherit;
} }
@ -87,6 +68,98 @@
} }
} }
} }
&:checked {
&::after {
content: '';
display: block;
position: absolute;
}
}
}
input[type='checkbox'] {
&:checked {
&::before {
background: $color-ultramarine;
border: 1.5px solid $color-ultramarine;
}
&::after {
border: solid $color-white;
border-width: 0 2px 2px 0;
height: 11px;
left: 7px;
top: 3px;
transform: rotate(45deg);
width: 6px;
}
}
}
input[type='radio'] {
&:checked {
&::before {
border: 2px solid $color-ultramarine;
}
&::after {
background: $color-ultramarine;
top: 4px;
left: 4px;
width: 12px;
height: 12px;
border-radius: 6px;
}
}
}
&--small {
height: 18px;
width: 18px;
input {
&::before {
height: 18px;
width: 18px;
}
}
input[type='checkbox'] {
&:checked {
&::before {
background: $color-ultramarine;
border: 1.5px solid $color-ultramarine;
}
&::after {
border: solid $color-white;
border-width: 0 2px 2px 0;
height: 10px;
left: 7px;
top: 3px;
transform: rotate(45deg);
width: 5px;
}
}
}
input[type='radio'] {
&:checked {
&::before {
border: 2px solid $color-ultramarine;
}
&::after {
background: $color-ultramarine;
top: 4px;
left: 4px;
width: 10px;
height: 10px;
border-radius: 5px;
}
}
}
} }
} }
} }

View file

@ -15,6 +15,7 @@
.Preferences { .Preferences {
display: flex; display: flex;
overflow: hidden; overflow: hidden;
user-select: none;
@include light-theme { @include light-theme {
background: $color-white; background: $color-white;
} }
@ -23,7 +24,7 @@
} }
&__page-selector { &__page-selector {
padding-top: 76px; padding-top: calc(24px + var(--title-bar-drag-area-height));
min-width: 240px; min-width: 240px;
@include light-theme { @include light-theme {
background: $color-gray-02; background: $color-gray-02;
@ -133,10 +134,19 @@
@include font-body-1-bold; @include font-body-1-bold;
align-items: center; align-items: center;
display: flex; display: flex;
height: 76px; height: 48px;
padding: 42px 0 14px 0; margin-top: var(--title-bar-drag-area-height);
margin-bottom: 24px;
text-align: center; text-align: center;
border-bottom: 1px solid $color-gray-15;
@include light-theme {
border-color: $color-gray-15;
}
@include dark-theme {
border-color: $color-gray-65;
}
&--header { &--header {
flex-grow: 1; flex-grow: 1;
text-align: center; text-align: center;
@ -144,7 +154,7 @@
} }
&__settings-row { &__settings-row {
padding-bottom: 12px; padding-bottom: 20px;
h3 { h3 {
@include font-body-1-bold; @include font-body-1-bold;
@ -164,6 +174,30 @@
margin-bottom: 24px; margin-bottom: 24px;
} }
&__link {
@include button-reset;
padding: 0px 0 28px 0;
width: 100%;
h3 {
@include font-body-1;
font-weight: 400;
margin: 0;
margin-bottom: 8px;
}
}
&__link:not(:last-child) {
border-bottom: 1px solid $color-gray-15;
@include light-theme {
border-color: $color-gray-15;
}
@include dark-theme {
border-color: $color-gray-65;
}
margin-bottom: 24px;
}
&__control { &__control {
align-items: center; align-items: center;
display: flex; display: flex;
@ -255,4 +289,15 @@
&__stories-off { &__stories-off {
min-width: 140px; min-width: 140px;
} }
&__settings-radio__label {
display: flex;
flex-direction: row;
gap: 16px;
height: 40px;
align-items: center;
&:last-child {
margin-bottom: 8px;
}
}
} }

View file

@ -36,3 +36,13 @@ export const getName = (): string => {
} }
return 'Linux'; return 'Linux';
}; };
export const getClassName = (): string => {
if (isMacOS()) {
return 'os-macos';
}
if (isWindows()) {
return 'os-windows';
}
return 'os-linux';
};

View file

@ -5,7 +5,7 @@ import React from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import type { Props } from './CircleCheckbox'; import type { Props } from './CircleCheckbox';
import { CircleCheckbox } from './CircleCheckbox'; import { CircleCheckbox, Variant } from './CircleCheckbox';
const createProps = (): Props => ({ const createProps = (): Props => ({
checked: false, checked: false,
@ -28,3 +28,53 @@ export function Checked(): JSX.Element {
export function Disabled(): JSX.Element { export function Disabled(): JSX.Element {
return <CircleCheckbox {...createProps()} disabled />; return <CircleCheckbox {...createProps()} disabled />;
} }
export function SmallNormal(): JSX.Element {
return <CircleCheckbox variant={Variant.Small} {...createProps()} />;
}
export function SmallChecked(): JSX.Element {
return <CircleCheckbox variant={Variant.Small} {...createProps()} checked />;
}
export function SmallDisabled(): JSX.Element {
return <CircleCheckbox variant={Variant.Small} {...createProps()} disabled />;
}
export function RadioNormal(): JSX.Element {
return <CircleCheckbox isRadio {...createProps()} />;
}
export function RadioChecked(): JSX.Element {
return <CircleCheckbox isRadio {...createProps()} checked />;
}
export function RadioDisabled(): JSX.Element {
return <CircleCheckbox isRadio {...createProps()} disabled />;
}
export function SmallRadioNormal(): JSX.Element {
return <CircleCheckbox variant={Variant.Small} isRadio {...createProps()} />;
}
export function SmallRadioChecked(): JSX.Element {
return (
<CircleCheckbox
variant={Variant.Small}
isRadio
{...createProps()}
checked
/>
);
}
export function SmallRadioDisabled(): JSX.Element {
return (
<CircleCheckbox
variant={Variant.Small}
isRadio
{...createProps()}
disabled
/>
);
}

View file

@ -2,15 +2,24 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { getClassNamesFor } from '../util/getClassNamesFor'; import { getClassNamesFor } from '../util/getClassNamesFor';
import { missingCaseError } from '../util/missingCaseError';
export enum Variant {
Normal = 'Normal',
Small = 'Small',
}
export type Props = { export type Props = {
id?: string; id?: string;
variant?: Variant;
checked?: boolean; checked?: boolean;
disabled?: boolean; disabled?: boolean;
isRadio?: boolean; isRadio?: boolean;
name?: string; name?: string;
moduleClassName?: string;
onChange?: (value: boolean) => unknown; onChange?: (value: boolean) => unknown;
onClick?: () => unknown; onClick?: () => unknown;
}; };
@ -24,17 +33,28 @@ export type Props = {
*/ */
export function CircleCheckbox({ export function CircleCheckbox({
id, id,
variant = Variant.Normal,
checked, checked,
disabled, disabled,
isRadio, isRadio,
moduleClassName,
name, name,
onChange, onChange,
onClick, onClick,
}: Props): JSX.Element { }: Props): JSX.Element {
const getClassName = getClassNamesFor('CircleCheckbox'); const getClassName = getClassNamesFor('CircleCheckbox', moduleClassName);
let variantModifier: string;
if (variant === Variant.Normal) {
variantModifier = getClassName('__checkbox--normal');
} else if (variant === Variant.Small) {
variantModifier = getClassName('__checkbox--small');
} else {
throw missingCaseError(variant);
}
return ( return (
<div className={getClassName('__checkbox')}> <div className={classNames(getClassName('__checkbox'), variantModifier)}>
<input <input
checked={Boolean(checked)} checked={Boolean(checked)}
disabled={disabled} disabled={disabled}

View file

@ -96,7 +96,7 @@ const getDefaultArgs = (): PropsDataType => ({
isAutoLaunchSupported: true, isAutoLaunchSupported: true,
isHideMenuBarSupported: true, isHideMenuBarSupported: true,
isNotificationAttentionSupported: true, isNotificationAttentionSupported: true,
isPhoneNumberSharingSupported: false, isPhoneNumberSharingSupported: true,
isSyncSupported: true, isSyncSupported: true,
isSystemTraySupported: true, isSystemTraySupported: true,
isMinimizeToAndStartInSystemTraySupported: true, isMinimizeToAndStartInSystemTraySupported: true,
@ -163,6 +163,8 @@ export default {
onSpellCheckChange: { action: true }, onSpellCheckChange: { action: true },
onThemeChange: { action: true }, onThemeChange: { action: true },
onUniversalExpireTimerChange: { action: true }, onUniversalExpireTimerChange: { action: true },
onWhoCanSeeMeChange: { action: true },
onWhoCanFindMeChange: { action: true },
onZoomFactorChange: { action: true }, onZoomFactorChange: { action: true },
removeCustomColor: { action: true }, removeCustomColor: { action: true },
removeCustomColorOnConversations: { action: true }, removeCustomColorOnConversations: { action: true },
@ -195,3 +197,23 @@ CustomUniversalExpireTimer.args = {
CustomUniversalExpireTimer.story = { CustomUniversalExpireTimer.story = {
name: 'Custom universalExpireTimer', name: 'Custom universalExpireTimer',
}; };
export const PNPSharingDisabled = Template.bind({});
PNPSharingDisabled.args = {
whoCanSeeMe: PhoneNumberSharingMode.Nobody,
whoCanFindMe: PhoneNumberDiscoverability.Discoverable,
isPhoneNumberSharingSupported: true,
};
PNPSharingDisabled.story = {
name: 'PNP Sharing Disabled',
};
export const PNPDiscoverabilityDisabled = Template.bind({});
PNPDiscoverabilityDisabled.args = {
whoCanSeeMe: PhoneNumberSharingMode.Nobody,
whoCanFindMe: PhoneNumberDiscoverability.NotDiscoverable,
isPhoneNumberSharingSupported: true,
};
PNPDiscoverabilityDisabled.story = {
name: 'PNP Discoverability Disabled',
};

View file

@ -2,10 +2,11 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import React, { useEffect, useState, useCallback } from 'react'; import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { noop } from 'lodash'; import { noop } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import type { AudioDevice } from '@signalapp/ringrtc'; import type { AudioDevice } from '@signalapp/ringrtc';
import uuid from 'uuid';
import type { MediaDeviceSettings } from '../types/Calling'; import type { MediaDeviceSettings } from '../types/Calling';
import type { import type {
@ -17,6 +18,10 @@ import type { ThemeSettingType } from '../types/StorageUIKeys';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonVariant } from './Button';
import { ChatColorPicker } from './ChatColorPicker'; import { ChatColorPicker } from './ChatColorPicker';
import { Checkbox } from './Checkbox'; import { Checkbox } from './Checkbox';
import {
CircleCheckbox,
Variant as CircleCheckboxVariant,
} from './CircleCheckbox';
import { ConfirmationDialog } from './ConfirmationDialog'; import { ConfirmationDialog } from './ConfirmationDialog';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import type { import type {
@ -159,6 +164,8 @@ type PropsFunctionType = {
onSpellCheckChange: CheckboxChangeHandlerType; onSpellCheckChange: CheckboxChangeHandlerType;
onThemeChange: SelectChangeHandlerType<ThemeType>; onThemeChange: SelectChangeHandlerType<ThemeType>;
onUniversalExpireTimerChange: SelectChangeHandlerType<number>; onUniversalExpireTimerChange: SelectChangeHandlerType<number>;
onWhoCanSeeMeChange: SelectChangeHandlerType<PhoneNumberSharingMode>;
onWhoCanFindMeChange: SelectChangeHandlerType<PhoneNumberDiscoverability>;
onZoomFactorChange: SelectChangeHandlerType<ZoomFactorType>; onZoomFactorChange: SelectChangeHandlerType<ZoomFactorType>;
// Localization // Localization
@ -178,6 +185,7 @@ enum Page {
// Sub pages // Sub pages
ChatColor = 'ChatColor', ChatColor = 'ChatColor',
PNP = 'PNP',
} }
const DEFAULT_ZOOM_FACTORS = [ const DEFAULT_ZOOM_FACTORS = [
@ -278,6 +286,8 @@ export function Preferences({
onSpellCheckChange, onSpellCheckChange,
onThemeChange, onThemeChange,
onUniversalExpireTimerChange, onUniversalExpireTimerChange,
onWhoCanSeeMeChange,
onWhoCanFindMeChange,
onZoomFactorChange, onZoomFactorChange,
removeCustomColor, removeCustomColor,
removeCustomColorOnConversations, removeCustomColorOnConversations,
@ -845,6 +855,20 @@ export function Preferences({
{i18n('Preferences__button--privacy')} {i18n('Preferences__button--privacy')}
</div> </div>
</div> </div>
{isPhoneNumberSharingSupported ? (
<button
type="button"
className="Preferences__link"
onClick={() => setPage(Page.PNP)}
>
<h3 className="Preferences__padding">
{i18n('icu:Preferences__pnp__row--title')}
</h3>
<div className="Preferences__padding Preferences__description">
{i18n('icu:Preferences__pnp__row--body')}
</div>
</button>
) : null}
<SettingsRow> <SettingsRow>
<Control <Control
left={i18n('Preferences--blocked')} left={i18n('Preferences--blocked')}
@ -859,61 +883,6 @@ export function Preferences({
} }
/> />
</SettingsRow> </SettingsRow>
{isPhoneNumberSharingSupported ? (
<SettingsRow title={i18n('Preferences__who-can--title')}>
<Control
left={i18n('Preferences--see-me')}
right={
<Select
ariaLabel={i18n('Preferences--see-me')}
disabled
onChange={noop}
options={[
{
text: i18n('Preferences__who-can--everybody'),
value: PhoneNumberSharingMode.Everybody,
},
{
text: i18n('Preferences__who-can--contacts'),
value: PhoneNumberSharingMode.ContactsOnly,
},
{
text: i18n('Preferences__who-can--nobody'),
value: PhoneNumberSharingMode.Nobody,
},
]}
value={whoCanSeeMe}
/>
}
/>
<Control
left={i18n('Preferences--find-me')}
right={
<Select
ariaLabel={i18n('Preferences--find-me')}
disabled
onChange={noop}
options={[
{
text: i18n('Preferences__who-can--everybody'),
value: PhoneNumberDiscoverability.Discoverable,
},
{
text: i18n('Preferences__who-can--nobody'),
value: PhoneNumberDiscoverability.NotDiscoverable,
},
]}
value={whoCanFindMe}
/>
}
/>
<div className="Preferences__padding">
<div className="Preferences__description">
{i18n('Preferences__privacy--description')}
</div>
</div>
</SettingsRow>
) : null}
<SettingsRow title={i18n('Preferences--messaging')}> <SettingsRow title={i18n('Preferences--messaging')}>
<Checkbox <Checkbox
checked={hasReadReceipts} checked={hasReadReceipts}
@ -1120,6 +1089,82 @@ export function Preferences({
/> />
</> </>
); );
} else if (page === Page.PNP) {
settings = (
<>
<div className="Preferences__title">
<button
aria-label={i18n('goBack')}
className="Preferences__back-icon"
onClick={() => setPage(Page.Privacy)}
type="button"
/>
<div className="Preferences__title--header">
{i18n('icu:Preferences__pnp--page-title')}
</div>
</div>
<SettingsRow title={i18n('icu:Preferences__pnp__sharing--title')}>
<SettingsRadio
onChange={onWhoCanSeeMeChange}
options={[
{
text: i18n('icu:Preferences__pnp__sharing__everyone'),
value: PhoneNumberSharingMode.Everybody,
},
{
text: i18n('icu:Preferences__pnp__sharing__nobody'),
value: PhoneNumberSharingMode.Nobody,
},
]}
value={whoCanSeeMe}
/>
<div className="Preferences__padding">
<div className="Preferences__description">
{whoCanSeeMe === PhoneNumberSharingMode.Everybody
? i18n('icu:Preferences__pnp__sharing--description--everyone')
: i18n('icu:Preferences__pnp__sharing--description--nobody')}
</div>
</div>
</SettingsRow>
<SettingsRow
title={i18n('icu:Preferences__pnp__discoverability--title')}
>
<SettingsRadio
onChange={onWhoCanFindMeChange}
options={[
{
text: i18n('icu:Preferences__pnp__discoverability__everyone'),
value: PhoneNumberDiscoverability.Discoverable,
},
...(whoCanSeeMe === PhoneNumberSharingMode.Nobody
? [
{
text: i18n(
'icu:Preferences__pnp__discoverability__nobody'
),
value: PhoneNumberDiscoverability.NotDiscoverable,
},
]
: []),
]}
value={whoCanFindMe}
/>
<div className="Preferences__padding">
<div className="Preferences__description">
{whoCanFindMe === PhoneNumberDiscoverability.Discoverable
? i18n(
'icu:Preferences__pnp__discoverability--description--everyone'
)
: i18n(
'icu:Preferences__pnp__discoverability--description--nobody'
)}
</div>
</div>
</SettingsRow>
</>
);
} }
return ( return (
@ -1128,6 +1173,7 @@ export function Preferences({
theme={theme} theme={theme}
executeMenuRole={executeMenuRole} executeMenuRole={executeMenuRole}
> >
<div className="module-title-bar-drag-area" />
<div className="Preferences"> <div className="Preferences">
<div className="Preferences__page-selector"> <div className="Preferences__page-selector">
<button <button
@ -1191,7 +1237,8 @@ export function Preferences({
className={classNames({ className={classNames({
Preferences__button: true, Preferences__button: true,
'Preferences__button--privacy': true, 'Preferences__button--privacy': true,
'Preferences__button--selected': page === Page.Privacy, 'Preferences__button--selected':
page === Page.Privacy || page === Page.PNP,
})} })}
onClick={() => setPage(Page.Privacy)} onClick={() => setPage(Page.Privacy)}
> >
@ -1207,12 +1254,14 @@ export function Preferences({
function SettingsRow({ function SettingsRow({
children, children,
title, title,
className,
}: { }: {
children: ReactNode; children: ReactNode;
title?: string; title?: string;
className?: string;
}): JSX.Element { }): JSX.Element {
return ( return (
<div className="Preferences__settings-row"> <div className={classNames('Preferences__settings-row', className)}>
{title && <h3 className="Preferences__padding">{title}</h3>} {title && <h3 className="Preferences__padding">{title}</h3>}
{children} {children}
</div> </div>
@ -1250,6 +1299,49 @@ function Control({
return <div className="Preferences__control">{content}</div>; return <div className="Preferences__control">{content}</div>;
} }
type SettingsRadioOptionType<Enum> = Readonly<{
text: string;
value: Enum;
}>;
function SettingsRadio<Enum>({
value,
options,
onChange,
}: {
value: Enum;
options: ReadonlyArray<SettingsRadioOptionType<Enum>>;
onChange: (value: Enum) => void;
}): JSX.Element {
const htmlIds = useMemo(() => {
return Array.from({ length: options.length }, () => uuid());
}, [options.length]);
return (
<div className="Preferences__padding">
{options.map(({ text, value: optionValue }, i) => {
const htmlId = htmlIds[i];
return (
<label
className="Preferences__settings-radio__label"
key={htmlId}
htmlFor={htmlId}
>
<CircleCheckbox
isRadio
variant={CircleCheckboxVariant.Small}
id={htmlId}
checked={value === optionValue}
onChange={() => onChange(optionValue)}
/>
{text}
</label>
);
})}
</div>
);
}
function localizeDefault(i18n: LocalizerType, deviceLabel: string): string { function localizeDefault(i18n: LocalizerType, deviceLabel: string): string {
return deviceLabel.toLowerCase().startsWith('default') return deviceLabel.toLowerCase().startsWith('default')
? deviceLabel.replace( ? deviceLabel.replace(

View file

@ -2,11 +2,12 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import uuid from 'uuid';
import type { LocalizerType } from '../../../types/Util'; import type { LocalizerType } from '../../../types/Util';
import { getMuteOptions } from '../../../util/getMuteOptions'; import { getMuteOptions } from '../../../util/getMuteOptions';
import { parseIntOrThrow } from '../../../util/parseIntOrThrow'; import { parseIntOrThrow } from '../../../util/parseIntOrThrow';
import { Checkbox } from '../../Checkbox'; import { CircleCheckbox, Variant } from '../../CircleCheckbox';
import { Modal } from '../../Modal'; import { Modal } from '../../Modal';
import { Button, ButtonVariant } from '../../Button'; import { Button, ButtonVariant } from '../../Button';
@ -30,11 +31,13 @@ export function ConversationNotificationsModal({
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const muteOptions = useMemo( const muteOptions = useMemo(
() => () =>
getMuteOptions(muteExpiresAt, i18n).map(({ disabled, name, value }) => ({ getMuteOptions(muteExpiresAt, i18n)
.map(({ disabled, name, value }) => ({
disabled, disabled,
text: name, text: name,
value, value,
})), }))
.filter(x => x.value > 0),
[i18n, muteExpiresAt] [i18n, muteExpiresAt]
); );
@ -49,6 +52,10 @@ export function ConversationNotificationsModal({
onClose(); onClose();
}; };
const htmlIds = useMemo(() => {
return Array.from({ length: muteOptions.length }, () => uuid());
}, [muteOptions.length]);
return ( return (
<Modal <Modal
modalName="ConversationNotificationsModal" modalName="ConversationNotificationsModal"
@ -67,19 +74,24 @@ export function ConversationNotificationsModal({
</> </>
} }
> >
{muteOptions {muteOptions.map((option, i) => (
.filter(x => x.value > 0) <label
.map(option => ( className="Preferences__settings-radio__label"
<Checkbox key={htmlIds[i]}
htmlFor={htmlIds[i]}
>
<CircleCheckbox
id={htmlIds[i]}
checked={muteExpirationValue === option.value} checked={muteExpirationValue === option.value}
variant={Variant.Small}
disabled={option.disabled} disabled={option.disabled}
isRadio isRadio
key={option.value}
label={option.text}
moduleClassName="ConversationDetails__radio" moduleClassName="ConversationDetails__radio"
name="mute" name="mute"
onChange={value => value && setMuteExpirationValue(option.value)} onChange={value => value && setMuteExpirationValue(option.value)}
/> />
{option.text}
</label>
))} ))}
</Modal> </Modal>
); );

View file

@ -67,8 +67,6 @@ export class SettingsChannel extends EventEmitter {
// Getters only. These are set by the primary device // Getters only. These are set by the primary device
this.installSetting('blockedCount', { setter: false }); this.installSetting('blockedCount', { setter: false });
this.installSetting('linkPreviewSetting', { setter: false }); this.installSetting('linkPreviewSetting', { setter: false });
this.installSetting('phoneNumberDiscoverabilitySetting', { setter: false });
this.installSetting('phoneNumberSharingSetting', { setter: false });
this.installSetting('readReceiptSetting', { setter: false }); this.installSetting('readReceiptSetting', { setter: false });
this.installSetting('typingIndicatorSetting', { setter: false }); this.installSetting('typingIndicatorSetting', { setter: false });
@ -109,6 +107,9 @@ export class SettingsChannel extends EventEmitter {
this.installSetting('hasStoriesDisabled'); this.installSetting('hasStoriesDisabled');
this.installSetting('zoomFactor'); this.installSetting('zoomFactor');
this.installSetting('phoneNumberDiscoverabilitySetting');
this.installSetting('phoneNumberSharingSetting');
installPermissionsHandler({ session, userConfig }); installPermissionsHandler({ session, userConfig });
// These ones are different because its single source of truth is userConfig, // These ones are different because its single source of truth is userConfig,

View file

@ -296,9 +296,6 @@ export function toAccountRecord(
PHONE_NUMBER_SHARING_MODE_ENUM.EVERYBODY; PHONE_NUMBER_SHARING_MODE_ENUM.EVERYBODY;
break; break;
case PhoneNumberSharingMode.ContactsOnly: case PhoneNumberSharingMode.ContactsOnly:
accountRecord.phoneNumberSharingMode =
PHONE_NUMBER_SHARING_MODE_ENUM.CONTACTS_ONLY;
break;
case PhoneNumberSharingMode.Nobody: case PhoneNumberSharingMode.Nobody:
accountRecord.phoneNumberSharingMode = accountRecord.phoneNumberSharingMode =
PHONE_NUMBER_SHARING_MODE_ENUM.NOBODY; PHONE_NUMBER_SHARING_MODE_ENUM.NOBODY;
@ -1222,8 +1219,6 @@ export async function mergeAccountRecord(
phoneNumberSharingModeToStore = PhoneNumberSharingMode.Everybody; phoneNumberSharingModeToStore = PhoneNumberSharingMode.Everybody;
break; break;
case PHONE_NUMBER_SHARING_MODE_ENUM.CONTACTS_ONLY: case PHONE_NUMBER_SHARING_MODE_ENUM.CONTACTS_ONLY:
phoneNumberSharingModeToStore = PhoneNumberSharingMode.ContactsOnly;
break;
case PHONE_NUMBER_SHARING_MODE_ENUM.NOBODY: case PHONE_NUMBER_SHARING_MODE_ENUM.NOBODY:
phoneNumberSharingModeToStore = PhoneNumberSharingMode.Nobody; phoneNumberSharingModeToStore = PhoneNumberSharingMode.Nobody;
break; break;

View file

@ -7,7 +7,7 @@ import type { MenuItemConstructorOptions } from 'electron';
import type { MenuActionType } from '../../types/menu'; import type { MenuActionType } from '../../types/menu';
import { App } from '../../components/App'; import { App } from '../../components/App';
import { getName as getOSName } from '../../OS'; import { getName as getOSName, getClassName as getOSClassName } from '../../OS';
import { SmartCallManager } from './CallManager'; import { SmartCallManager } from './CallManager';
import { SmartGlobalModalContainer } from './GlobalModalContainer'; import { SmartGlobalModalContainer } from './GlobalModalContainer';
import { SmartLightbox } from './Lightbox'; import { SmartLightbox } from './Lightbox';
@ -39,18 +39,6 @@ function renderInbox(): JSX.Element {
const mapStateToProps = (state: StateType) => { const mapStateToProps = (state: StateType) => {
const i18n = getIntl(state); const i18n = getIntl(state);
const { osName } = state.user;
let osClassName = '';
if (osName === 'windows') {
osClassName = 'os-windows';
} else if (osName === 'macos') {
osClassName = 'os-macos';
} else if (osName === 'linux') {
osClassName = 'os-linux';
}
return { return {
...state.app, ...state.app,
i18n, i18n,
@ -60,7 +48,7 @@ const mapStateToProps = (state: StateType) => {
menuOptions: getMenuOptions(state), menuOptions: getMenuOptions(state),
hasCustomTitleBar: window.SignalContext.OS.hasCustomTitleBar(), hasCustomTitleBar: window.SignalContext.OS.hasCustomTitleBar(),
OS: getOSName(), OS: getOSName(),
osClassName, osClassName: getOSClassName(),
hideMenuBar: getHideMenuBar(state), hideMenuBar: getHideMenuBar(state),
renderCallManager: () => ( renderCallManager: () => (
<ModalContainer className="module-calling__modal-container"> <ModalContainer className="module-calling__modal-container">

View file

@ -494,6 +494,7 @@ const URL_CALLS = {
keys: 'v2/keys', keys: 'v2/keys',
messages: 'v1/messages', messages: 'v1/messages',
multiRecipient: 'v1/messages/multi_recipient', multiRecipient: 'v1/messages/multi_recipient',
phoneNumberDiscoverability: 'v2/accounts/phone_number_discoverability',
profile: 'v1/profile', profile: 'v1/profile',
registerCapabilities: 'v1/devices/capabilities', registerCapabilities: 'v1/devices/capabilities',
reportMessage: 'v1/messages/report', reportMessage: 'v1/messages/report',
@ -541,6 +542,9 @@ const WEBSOCKET_CALLS = new Set<keyof typeof URL_CALLS>([
// Storage // Storage
'storageToken', 'storageToken',
// Account V2
'phoneNumberDiscoverability',
]); ]);
type InitializeOptionsType = { type InitializeOptionsType = {
@ -979,6 +983,7 @@ export type WebAPIType = {
urgent?: boolean; urgent?: boolean;
} }
) => Promise<MultiRecipient200ResponseType>; ) => Promise<MultiRecipient200ResponseType>;
setPhoneNumberDiscoverability: (newValue: boolean) => Promise<void>;
setSignedPreKey: ( setSignedPreKey: (
signedPreKey: SignedPreKeyType, signedPreKey: SignedPreKeyType,
uuidKind: UUIDKind uuidKind: UUIDKind
@ -1272,6 +1277,7 @@ export function initialize({
sendMessages, sendMessages,
sendMessagesUnauth, sendMessagesUnauth,
sendWithSenderKey, sendWithSenderKey,
setPhoneNumberDiscoverability,
setSignedPreKey, setSignedPreKey,
startRegistration, startRegistration,
unregisterRequestHandler, unregisterRequestHandler,
@ -2027,6 +2033,16 @@ export function initialize({
}); });
} }
async function setPhoneNumberDiscoverability(newValue: boolean) {
await _ajax({
call: 'phoneNumberDiscoverability',
httpType: 'PUT',
jsonData: {
discoverableByPhoneNumber: newValue,
},
});
}
async function setSignedPreKey( async function setSignedPreKey(
signedPreKey: SignedPreKeyType, signedPreKey: SignedPreKeyType,
uuidKind: UUIDKind uuidKind: UUIDKind

View file

@ -28,7 +28,7 @@ import { renderClearingDataView } from '../shims/renderClearingDataView';
import * as universalExpireTimer from './universalExpireTimer'; import * as universalExpireTimer from './universalExpireTimer';
import { PhoneNumberDiscoverability } from './phoneNumberDiscoverability'; import { PhoneNumberDiscoverability } from './phoneNumberDiscoverability';
import { PhoneNumberSharingMode } from './phoneNumberSharingMode'; import { PhoneNumberSharingMode } from './phoneNumberSharingMode';
import { assertDev } from './assert'; import { strictAssert, assertDev } from './assert';
import * as durations from './durations'; import * as durations from './durations';
import type { DurationInSeconds } from './durations'; import type { DurationInSeconds } from './durations';
import { isPhoneNumberSharingEnabled } from './isPhoneNumberSharingEnabled'; import { isPhoneNumberSharingEnabled } from './isPhoneNumberSharingEnabled';
@ -136,8 +136,6 @@ type ValuesWithSetters = Omit<
| 'blockedCount' | 'blockedCount'
| 'defaultConversationColor' | 'defaultConversationColor'
| 'linkPreviewSetting' | 'linkPreviewSetting'
| 'phoneNumberDiscoverabilitySetting'
| 'phoneNumberSharingSetting'
| 'readReceiptSetting' | 'readReceiptSetting'
| 'typingIndicatorSetting' | 'typingIndicatorSetting'
| 'deviceName' | 'deviceName'
@ -177,6 +175,18 @@ export type IPCEventsType = IPCEventsGettersType &
export function createIPCEvents( export function createIPCEvents(
overrideEvents: Partial<IPCEventsType> = {} overrideEvents: Partial<IPCEventsType> = {}
): 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');
};
return { return {
getDeviceName: () => window.textsecure.storage.user.getDeviceName(), getDeviceName: () => window.textsecure.storage.user.getDeviceName(),
@ -185,6 +195,22 @@ export function createIPCEvents(
webFrame.setZoomFactor(zoomFactor); webFrame.setZoomFactor(zoomFactor);
}, },
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
)
);
}
account.captureChange('phoneNumberSharingMode');
await Promise.all(promises);
},
getHasStoriesDisabled: () => getHasStoriesDisabled: () =>
window.storage.get('hasStoriesDisabled', false), window.storage.get('hasStoriesDisabled', false),
setHasStoriesDisabled: async (value: boolean) => { setHasStoriesDisabled: async (value: boolean) => {
@ -202,6 +228,8 @@ export function createIPCEvents(
}, },
setStoryViewReceiptsEnabled: async (value: boolean) => { setStoryViewReceiptsEnabled: async (value: boolean) => {
await window.storage.put('storyViewReceiptsEnabled', value); await window.storage.put('storyViewReceiptsEnabled', value);
const account = window.ConversationController.getOurConversationOrThrow();
account.captureChange('storyViewReceiptsEnabled');
}, },
getPreferredAudioInputDevice: () => getPreferredAudioInputDevice: () =>

View file

@ -4,5 +4,8 @@
import * as RemoteConfig from '../RemoteConfig'; import * as RemoteConfig from '../RemoteConfig';
export function isPhoneNumberSharingEnabled(): boolean { export function isPhoneNumberSharingEnabled(): boolean {
return Boolean(RemoteConfig.isEnabled('desktop.internalUser')); return Boolean(
RemoteConfig.isEnabled('desktop.internalUser') ||
RemoteConfig.isEnabled('desktop.pnp')
);
} }

View file

@ -4,7 +4,6 @@
import type { ConversationAttributesType } from '../model-types.d'; import type { ConversationAttributesType } from '../model-types.d';
import { makeEnumParser } from './enum'; import { makeEnumParser } from './enum';
import { isInSystemContacts } from './isInSystemContacts';
import { missingCaseError } from './missingCaseError'; import { missingCaseError } from './missingCaseError';
import { isDirectConversation, isMe } from './whatTypeOfConversation'; import { isDirectConversation, isMe } from './whatTypeOfConversation';
@ -35,7 +34,6 @@ export const shouldSharePhoneNumberWith = (
case PhoneNumberSharingMode.Everybody: case PhoneNumberSharingMode.Everybody:
return true; return true;
case PhoneNumberSharingMode.ContactsOnly: case PhoneNumberSharingMode.ContactsOnly:
return isInSystemContacts(conversation);
case PhoneNumberSharingMode.Nobody: case PhoneNumberSharingMode.Nobody:
return false; return false;
default: default:

View file

@ -29,7 +29,13 @@ import { createSetting } from '../util/preload';
import { initialize as initializeLogging } from '../logging/set_up_renderer_logging'; import { initialize as initializeLogging } from '../logging/set_up_renderer_logging';
import { waitForSettingsChange } from './waitForSettingsChange'; import { waitForSettingsChange } from './waitForSettingsChange';
import { createNativeThemeListener } from '../context/createNativeThemeListener'; import { createNativeThemeListener } from '../context/createNativeThemeListener';
import { isWindows, isLinux, isMacOS, hasCustomTitleBar } from '../OS'; import {
isWindows,
isLinux,
isMacOS,
hasCustomTitleBar,
getClassName,
} from '../OS';
const activeWindowService = new ActiveWindowService(); const activeWindowService = new ActiveWindowService();
activeWindowService.initialize(window.document, ipcRenderer); activeWindowService.initialize(window.document, ipcRenderer);
@ -79,6 +85,7 @@ export type SignalContextType = {
isLinux: typeof isLinux; isLinux: typeof isLinux;
isMacOS: typeof isMacOS; isMacOS: typeof isMacOS;
hasCustomTitleBar: typeof hasCustomTitleBar; hasCustomTitleBar: typeof hasCustomTitleBar;
getClassName: typeof getClassName;
}; };
config: RendererConfigType; config: RendererConfigType;
getAppInstance: () => string | undefined; getAppInstance: () => string | undefined;
@ -108,6 +115,7 @@ export const SignalContext: SignalContextType = {
isLinux, isLinux,
isMacOS, isMacOS,
hasCustomTitleBar, hasCustomTitleBar,
getClassName,
}, },
bytes: new Bytes(), bytes: new Bytes(),
config, config,

View file

@ -1,6 +1,11 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
document.body.classList.add(window.SignalContext.OS.getClassName());
if (window.SignalContext.OS.hasCustomTitleBar()) {
document.body.classList.add('os-has-custom-titlebar');
}
if (window.SignalContext.renderWindow) { if (window.SignalContext.renderWindow) {
window.SignalContext.renderWindow(); window.SignalContext.renderWindow();
} else { } else {

View file

@ -15,6 +15,7 @@ import '../../backbone/reliable_trigger';
import type { FeatureFlagType } from '../../window.d'; import type { FeatureFlagType } from '../../window.d';
import type { StorageAccessType } from '../../types/Storage.d'; import type { StorageAccessType } from '../../types/Storage.d';
import type { CdsLookupOptionsType } from '../../textsecure/WebAPI';
import { start as startConversationController } from '../../ConversationController'; import { start as startConversationController } from '../../ConversationController';
import { MessageController } from '../../util/MessageController'; import { MessageController } from '../../util/MessageController';
import { Environment, getEnvironment } from '../../environment'; import { Environment, getEnvironment } from '../../environment';
@ -46,6 +47,8 @@ startConversationController();
if (!isProduction(window.SignalContext.getVersion())) { if (!isProduction(window.SignalContext.getVersion())) {
const SignalDebug = { const SignalDebug = {
Data: window.Signal.Data, Data: window.Signal.Data,
cdsLookup: (options: CdsLookupOptionsType) =>
window.textsecure.server?.cdsLookup(options),
getConversation: (id: string) => window.ConversationController.get(id), getConversation: (id: string) => window.ConversationController.get(id),
getMessageById: (id: string) => window.MessageController.getById(id), getMessageById: (id: string) => window.MessageController.getById(id),
getReduxState: () => window.reduxStore.getState(), getReduxState: () => window.reduxStore.getState(),

View file

@ -24,12 +24,6 @@ installSetting('blockedCount', {
installSetting('linkPreviewSetting', { installSetting('linkPreviewSetting', {
setter: false, setter: false,
}); });
installSetting('phoneNumberDiscoverabilitySetting', {
setter: false,
});
installSetting('phoneNumberSharingSetting', {
setter: false,
});
installSetting('readReceiptSetting', { installSetting('readReceiptSetting', {
setter: false, setter: false,
}); });
@ -63,6 +57,8 @@ installSetting('sentMediaQualitySetting');
installSetting('themeSetting'); installSetting('themeSetting');
installSetting('universalExpireTimer'); installSetting('universalExpireTimer');
installSetting('zoomFactor'); installSetting('zoomFactor');
installSetting('phoneNumberDiscoverabilitySetting');
installSetting('phoneNumberSharingSetting');
// Media Settings // Media Settings
installCallback('getAvailableIODevices'); installCallback('getAvailableIODevices');

View file

@ -58,12 +58,9 @@ const settingLinkPreview = createSetting('linkPreviewSetting', {
setter: false, setter: false,
}); });
const settingPhoneNumberDiscoverability = createSetting( const settingPhoneNumberDiscoverability = createSetting(
'phoneNumberDiscoverabilitySetting', 'phoneNumberDiscoverabilitySetting'
{ setter: false }
); );
const settingPhoneNumberSharing = createSetting('phoneNumberSharingSetting', { const settingPhoneNumberSharing = createSetting('phoneNumberSharingSetting');
setter: false,
});
const settingReadReceipts = createSetting('readReceiptSetting', { const settingReadReceipts = createSetting('readReceiptSetting', {
setter: false, setter: false,
}); });
@ -363,6 +360,9 @@ const renderPreferences = async () => {
); );
}, },
onWhoCanFindMeChange: reRender(settingPhoneNumberDiscoverability.setValue),
onWhoCanSeeMeChange: reRender(settingPhoneNumberSharing.setValue),
// Zoom factor change doesn't require immediate rerender since it will: // Zoom factor change doesn't require immediate rerender since it will:
// 1. Update the zoom factor in the main window // 1. Update the zoom factor in the main window
// 2. Trigger `preferred-size-changed` in the main process // 2. Trigger `preferred-size-changed` in the main process