PNP Settings
This commit is contained in:
parent
5bcf71ef2c
commit
5d110964b9
27 changed files with 562 additions and 149 deletions
|
@ -5,7 +5,7 @@ import React from 'react';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import type { Props } from './CircleCheckbox';
|
||||
import { CircleCheckbox } from './CircleCheckbox';
|
||||
import { CircleCheckbox, Variant } from './CircleCheckbox';
|
||||
|
||||
const createProps = (): Props => ({
|
||||
checked: false,
|
||||
|
@ -28,3 +28,53 @@ export function Checked(): JSX.Element {
|
|||
export function Disabled(): JSX.Element {
|
||||
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
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,15 +2,24 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
||||
export enum Variant {
|
||||
Normal = 'Normal',
|
||||
Small = 'Small',
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
id?: string;
|
||||
variant?: Variant;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
isRadio?: boolean;
|
||||
name?: string;
|
||||
moduleClassName?: string;
|
||||
onChange?: (value: boolean) => unknown;
|
||||
onClick?: () => unknown;
|
||||
};
|
||||
|
@ -24,17 +33,28 @@ export type Props = {
|
|||
*/
|
||||
export function CircleCheckbox({
|
||||
id,
|
||||
variant = Variant.Normal,
|
||||
checked,
|
||||
disabled,
|
||||
isRadio,
|
||||
moduleClassName,
|
||||
name,
|
||||
onChange,
|
||||
onClick,
|
||||
}: 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 (
|
||||
<div className={getClassName('__checkbox')}>
|
||||
<div className={classNames(getClassName('__checkbox'), variantModifier)}>
|
||||
<input
|
||||
checked={Boolean(checked)}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -96,7 +96,7 @@ const getDefaultArgs = (): PropsDataType => ({
|
|||
isAutoLaunchSupported: true,
|
||||
isHideMenuBarSupported: true,
|
||||
isNotificationAttentionSupported: true,
|
||||
isPhoneNumberSharingSupported: false,
|
||||
isPhoneNumberSharingSupported: true,
|
||||
isSyncSupported: true,
|
||||
isSystemTraySupported: true,
|
||||
isMinimizeToAndStartInSystemTraySupported: true,
|
||||
|
@ -163,6 +163,8 @@ export default {
|
|||
onSpellCheckChange: { action: true },
|
||||
onThemeChange: { action: true },
|
||||
onUniversalExpireTimerChange: { action: true },
|
||||
onWhoCanSeeMeChange: { action: true },
|
||||
onWhoCanFindMeChange: { action: true },
|
||||
onZoomFactorChange: { action: true },
|
||||
removeCustomColor: { action: true },
|
||||
removeCustomColorOnConversations: { action: true },
|
||||
|
@ -195,3 +197,23 @@ CustomUniversalExpireTimer.args = {
|
|||
CustomUniversalExpireTimer.story = {
|
||||
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',
|
||||
};
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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 classNames from 'classnames';
|
||||
import type { AudioDevice } from '@signalapp/ringrtc';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import type { MediaDeviceSettings } from '../types/Calling';
|
||||
import type {
|
||||
|
@ -17,6 +18,10 @@ import type { ThemeSettingType } from '../types/StorageUIKeys';
|
|||
import { Button, ButtonVariant } from './Button';
|
||||
import { ChatColorPicker } from './ChatColorPicker';
|
||||
import { Checkbox } from './Checkbox';
|
||||
import {
|
||||
CircleCheckbox,
|
||||
Variant as CircleCheckboxVariant,
|
||||
} from './CircleCheckbox';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type {
|
||||
|
@ -159,6 +164,8 @@ type PropsFunctionType = {
|
|||
onSpellCheckChange: CheckboxChangeHandlerType;
|
||||
onThemeChange: SelectChangeHandlerType<ThemeType>;
|
||||
onUniversalExpireTimerChange: SelectChangeHandlerType<number>;
|
||||
onWhoCanSeeMeChange: SelectChangeHandlerType<PhoneNumberSharingMode>;
|
||||
onWhoCanFindMeChange: SelectChangeHandlerType<PhoneNumberDiscoverability>;
|
||||
onZoomFactorChange: SelectChangeHandlerType<ZoomFactorType>;
|
||||
|
||||
// Localization
|
||||
|
@ -178,6 +185,7 @@ enum Page {
|
|||
|
||||
// Sub pages
|
||||
ChatColor = 'ChatColor',
|
||||
PNP = 'PNP',
|
||||
}
|
||||
|
||||
const DEFAULT_ZOOM_FACTORS = [
|
||||
|
@ -278,6 +286,8 @@ export function Preferences({
|
|||
onSpellCheckChange,
|
||||
onThemeChange,
|
||||
onUniversalExpireTimerChange,
|
||||
onWhoCanSeeMeChange,
|
||||
onWhoCanFindMeChange,
|
||||
onZoomFactorChange,
|
||||
removeCustomColor,
|
||||
removeCustomColorOnConversations,
|
||||
|
@ -845,6 +855,20 @@ export function Preferences({
|
|||
{i18n('Preferences__button--privacy')}
|
||||
</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>
|
||||
<Control
|
||||
left={i18n('Preferences--blocked')}
|
||||
|
@ -859,61 +883,6 @@ export function Preferences({
|
|||
}
|
||||
/>
|
||||
</SettingsRow>
|
||||
{isPhoneNumberSharingSupported ? (
|
||||
<SettingsRow title={i18n('Preferences__who-can--title')}>
|
||||
<Control
|
||||
left={i18n('Preferences--see-me')}
|
||||
right={
|
||||
<Select
|
||||
ariaLabel={i18n('Preferences--see-me')}
|
||||
disabled
|
||||
onChange={noop}
|
||||
options={[
|
||||
{
|
||||
text: i18n('Preferences__who-can--everybody'),
|
||||
value: PhoneNumberSharingMode.Everybody,
|
||||
},
|
||||
{
|
||||
text: i18n('Preferences__who-can--contacts'),
|
||||
value: PhoneNumberSharingMode.ContactsOnly,
|
||||
},
|
||||
{
|
||||
text: i18n('Preferences__who-can--nobody'),
|
||||
value: PhoneNumberSharingMode.Nobody,
|
||||
},
|
||||
]}
|
||||
value={whoCanSeeMe}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Control
|
||||
left={i18n('Preferences--find-me')}
|
||||
right={
|
||||
<Select
|
||||
ariaLabel={i18n('Preferences--find-me')}
|
||||
disabled
|
||||
onChange={noop}
|
||||
options={[
|
||||
{
|
||||
text: i18n('Preferences__who-can--everybody'),
|
||||
value: PhoneNumberDiscoverability.Discoverable,
|
||||
},
|
||||
{
|
||||
text: i18n('Preferences__who-can--nobody'),
|
||||
value: PhoneNumberDiscoverability.NotDiscoverable,
|
||||
},
|
||||
]}
|
||||
value={whoCanFindMe}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div className="Preferences__padding">
|
||||
<div className="Preferences__description">
|
||||
{i18n('Preferences__privacy--description')}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
) : null}
|
||||
<SettingsRow title={i18n('Preferences--messaging')}>
|
||||
<Checkbox
|
||||
checked={hasReadReceipts}
|
||||
|
@ -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 (
|
||||
|
@ -1128,6 +1173,7 @@ export function Preferences({
|
|||
theme={theme}
|
||||
executeMenuRole={executeMenuRole}
|
||||
>
|
||||
<div className="module-title-bar-drag-area" />
|
||||
<div className="Preferences">
|
||||
<div className="Preferences__page-selector">
|
||||
<button
|
||||
|
@ -1191,7 +1237,8 @@ export function Preferences({
|
|||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--privacy': true,
|
||||
'Preferences__button--selected': page === Page.Privacy,
|
||||
'Preferences__button--selected':
|
||||
page === Page.Privacy || page === Page.PNP,
|
||||
})}
|
||||
onClick={() => setPage(Page.Privacy)}
|
||||
>
|
||||
|
@ -1207,12 +1254,14 @@ export function Preferences({
|
|||
function SettingsRow({
|
||||
children,
|
||||
title,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
className?: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="Preferences__settings-row">
|
||||
<div className={classNames('Preferences__settings-row', className)}>
|
||||
{title && <h3 className="Preferences__padding">{title}</h3>}
|
||||
{children}
|
||||
</div>
|
||||
|
@ -1250,6 +1299,49 @@ function Control({
|
|||
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 {
|
||||
return deviceLabel.toLowerCase().startsWith('default')
|
||||
? deviceLabel.replace(
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import type { LocalizerType } from '../../../types/Util';
|
||||
import { getMuteOptions } from '../../../util/getMuteOptions';
|
||||
import { parseIntOrThrow } from '../../../util/parseIntOrThrow';
|
||||
import { Checkbox } from '../../Checkbox';
|
||||
import { CircleCheckbox, Variant } from '../../CircleCheckbox';
|
||||
import { Modal } from '../../Modal';
|
||||
import { Button, ButtonVariant } from '../../Button';
|
||||
|
||||
|
@ -30,11 +31,13 @@ export function ConversationNotificationsModal({
|
|||
}: PropsType): JSX.Element {
|
||||
const muteOptions = useMemo(
|
||||
() =>
|
||||
getMuteOptions(muteExpiresAt, i18n).map(({ disabled, name, value }) => ({
|
||||
disabled,
|
||||
text: name,
|
||||
value,
|
||||
})),
|
||||
getMuteOptions(muteExpiresAt, i18n)
|
||||
.map(({ disabled, name, value }) => ({
|
||||
disabled,
|
||||
text: name,
|
||||
value,
|
||||
}))
|
||||
.filter(x => x.value > 0),
|
||||
[i18n, muteExpiresAt]
|
||||
);
|
||||
|
||||
|
@ -49,6 +52,10 @@ export function ConversationNotificationsModal({
|
|||
onClose();
|
||||
};
|
||||
|
||||
const htmlIds = useMemo(() => {
|
||||
return Array.from({ length: muteOptions.length }, () => uuid());
|
||||
}, [muteOptions.length]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
modalName="ConversationNotificationsModal"
|
||||
|
@ -67,20 +74,25 @@ export function ConversationNotificationsModal({
|
|||
</>
|
||||
}
|
||||
>
|
||||
{muteOptions
|
||||
.filter(x => x.value > 0)
|
||||
.map(option => (
|
||||
<Checkbox
|
||||
{muteOptions.map((option, i) => (
|
||||
<label
|
||||
className="Preferences__settings-radio__label"
|
||||
key={htmlIds[i]}
|
||||
htmlFor={htmlIds[i]}
|
||||
>
|
||||
<CircleCheckbox
|
||||
id={htmlIds[i]}
|
||||
checked={muteExpirationValue === option.value}
|
||||
variant={Variant.Small}
|
||||
disabled={option.disabled}
|
||||
isRadio
|
||||
key={option.value}
|
||||
label={option.text}
|
||||
moduleClassName="ConversationDetails__radio"
|
||||
name="mute"
|
||||
onChange={value => value && setMuteExpirationValue(option.value)}
|
||||
/>
|
||||
))}
|
||||
{option.text}
|
||||
</label>
|
||||
))}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue