Universal Disappearing Messages

This commit is contained in:
Fedor Indutny 2021-06-01 13:45:43 -07:00 committed by GitHub
parent c63871d71b
commit 19f8042cd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1224 additions and 191 deletions

View file

@ -14,6 +14,7 @@ export type ActionSpec = {
};
export type OwnProps = {
readonly moduleClassName?: string;
readonly actions?: Array<ActionSpec>;
readonly cancelText?: string;
readonly children?: React.ReactNode;
@ -22,6 +23,7 @@ export type OwnProps = {
readonly onClose: () => unknown;
readonly title?: string | React.ReactNode;
readonly theme?: Theme;
readonly hasXButton?: boolean;
};
export type Props = OwnProps;
@ -48,6 +50,7 @@ function getButtonVariant(
export const ConfirmationDialog = React.memo(
({
moduleClassName,
actions = [],
cancelText,
children,
@ -56,6 +59,7 @@ export const ConfirmationDialog = React.memo(
onClose,
theme,
title,
hasXButton,
}: Props) => {
const cancelAndClose = React.useCallback(() => {
if (onCancel) {
@ -76,7 +80,14 @@ export const ConfirmationDialog = React.memo(
const hasActions = Boolean(actions.length);
return (
<Modal i18n={i18n} onClose={cancelAndClose} title={title} theme={theme}>
<Modal
moduleClassName={moduleClassName}
i18n={i18n}
onClose={cancelAndClose}
title={title}
theme={theme}
hasXButton={hasXButton}
>
{children}
<Modal.ButtonFooter>
<Button

View file

@ -0,0 +1,31 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import { Select } from './Select';
const story = storiesOf('Components/Select', module);
story.add('Normal', () => {
const [value, setValue] = useState(0);
const onChange = action('onChange');
return (
<Select
options={[
{ value: 1, text: '1' },
{ value: 2, text: '2' },
{ value: 3, text: '3' },
]}
value={value}
onChange={newValue => {
onChange(newValue);
setValue(parseInt(newValue, 10));
}}
/>
);
});

39
ts/components/Select.tsx Normal file
View file

@ -0,0 +1,39 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import classNames from 'classnames';
export type Option = Readonly<{
text: string;
value: string | number;
}>;
export type PropsType = Readonly<{
moduleClassName?: string;
options: ReadonlyArray<Option>;
onChange(value: string): void;
value: string | number;
}>;
export function Select(props: PropsType): JSX.Element {
const { moduleClassName, value, options, onChange } = props;
const onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
onChange(event.target.value);
};
return (
<div className={classNames(['module-select', moduleClassName])}>
<select value={value} onChange={onSelectChange}>
{options.map(({ text, value: optionValue }) => {
return (
<option value={optionValue} key={optionValue} aria-label={text}>
{text}
</option>
);
})}
</select>
</div>
);
}

View file

@ -107,7 +107,7 @@ const stories: Array<ConversationHeaderStory> = [
name: 'Joyrey 🔥 Leppey',
phoneNumber: '(202) 555-0002',
type: 'direct',
id: '2',
id: '3',
acceptedMessageRequest: true,
},
},
@ -119,7 +119,7 @@ const stories: Array<ConversationHeaderStory> = [
isVerified: false,
phoneNumber: '(202) 555-0003',
type: 'direct',
id: '3',
id: '4',
title: '🔥Flames🔥',
profileName: '🔥Flames🔥',
acceptedMessageRequest: true,
@ -132,7 +132,7 @@ const stories: Array<ConversationHeaderStory> = [
title: '(202) 555-0011',
phoneNumber: '(202) 555-0011',
type: 'direct',
id: '11',
id: '5',
acceptedMessageRequest: true,
},
},
@ -145,7 +145,7 @@ const stories: Array<ConversationHeaderStory> = [
phoneNumber: '(202) 555-0004',
title: '(202) 555-0004',
type: 'direct',
id: '4',
id: '6',
acceptedMessageRequest: true,
},
},
@ -157,7 +157,7 @@ const stories: Array<ConversationHeaderStory> = [
title: '(202) 555-0005',
phoneNumber: '(202) 555-0005',
type: 'direct',
id: '5',
id: '7',
expireTimer: 10,
acceptedMessageRequest: true,
},
@ -170,10 +170,11 @@ const stories: Array<ConversationHeaderStory> = [
title: '(202) 555-0005',
phoneNumber: '(202) 555-0005',
type: 'direct',
id: '5',
expireTimer: 60,
id: '8',
expireTimer: 300,
acceptedMessageRequest: true,
isVerified: true,
canChangeTimer: true,
},
},
{
@ -184,7 +185,7 @@ const stories: Array<ConversationHeaderStory> = [
title: '(202) 555-0006',
phoneNumber: '(202) 555-0006',
type: 'direct',
id: '6',
id: '9',
acceptedMessageRequest: true,
muteExpiresAt: new Date('3000-10-18T11:11:11Z').valueOf(),
},
@ -197,7 +198,7 @@ const stories: Array<ConversationHeaderStory> = [
title: '(202) 555-0006',
phoneNumber: '(202) 555-0006',
type: 'direct',
id: '6',
id: '10',
acceptedMessageRequest: true,
isSMSOnly: true,
},
@ -217,7 +218,7 @@ const stories: Array<ConversationHeaderStory> = [
title: 'Typescript support group',
name: 'Typescript support group',
phoneNumber: '',
id: '1',
id: '11',
type: 'group',
expireTimer: 10,
acceptedMessageRequest: true,
@ -232,7 +233,7 @@ const stories: Array<ConversationHeaderStory> = [
title: 'Typescript support group',
name: 'Typescript support group',
phoneNumber: '',
id: '2',
id: '12',
type: 'group',
left: true,
expireTimer: 10,
@ -248,7 +249,7 @@ const stories: Array<ConversationHeaderStory> = [
title: 'Typescript support group',
name: 'Typescript support group',
phoneNumber: '',
id: '1',
id: '13',
type: 'group',
expireTimer: 10,
acceptedMessageRequest: true,
@ -263,7 +264,7 @@ const stories: Array<ConversationHeaderStory> = [
title: 'Way too many messages',
name: 'Way too many messages',
phoneNumber: '',
id: '1',
id: '14',
type: 'group',
expireTimer: 10,
acceptedMessageRequest: true,
@ -284,7 +285,7 @@ const stories: Array<ConversationHeaderStory> = [
color: 'blue',
title: '(202) 555-0007',
phoneNumber: '(202) 555-0007',
id: '7',
id: '15',
type: 'direct',
isMe: true,
acceptedMessageRequest: true,
@ -304,7 +305,7 @@ const stories: Array<ConversationHeaderStory> = [
color: 'blue',
title: '(202) 555-0007',
phoneNumber: '(202) 555-0007',
id: '7',
id: '16',
type: 'direct',
isMe: false,
acceptedMessageRequest: false,

View file

@ -13,6 +13,7 @@ import {
} from 'react-contextmenu';
import { Emojify } from './Emojify';
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
import { Avatar, AvatarSize } from '../Avatar';
import { InContactsIcon } from '../InContactsIcon';
@ -92,10 +93,18 @@ export type PropsType = PropsDataType &
PropsActionsType &
PropsHousekeepingType;
enum ModalState {
NothingOpen,
CustomDisappearingTimeout,
}
type StateType = {
isNarrow: boolean;
modalState: ModalState;
};
const TIMER_ITEM_CLASS = 'module-ConversationHeader__disappearing-timer__item';
export class ConversationHeader extends React.Component<PropsType, StateType> {
private showMenuBound: (event: React.MouseEvent<HTMLButtonElement>) => void;
@ -106,7 +115,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
public constructor(props: PropsType) {
super(props);
this.state = { isNarrow: false };
this.state = { isNarrow: false, modalState: ModalState.NothingOpen };
this.menuTriggerRef = React.createRef();
this.showMenuBound = this.showMenu.bind(this);
@ -355,6 +364,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
i18n,
acceptedMessageRequest,
canChangeTimer,
expireTimer,
isArchived,
isMe,
isPinned,
@ -427,23 +437,60 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
const hasGV2AdminEnabled = isGroup && groupVersion === 2;
const isActiveExpireTimer = (value: number): boolean => {
if (!expireTimer) {
return value === 0;
}
// Custom time...
if (value === -1) {
return !expirationTimer.DEFAULT_DURATIONS_SET.has(expireTimer);
}
return value === expireTimer;
};
const expireDurations: ReadonlyArray<ReactNode> = [
...expirationTimer.DEFAULT_DURATIONS_IN_SECONDS,
-1,
].map((seconds: number) => {
let text: string;
if (seconds === -1) {
text = i18n('customDisappearingTimeOption');
} else {
text = expirationTimer.format(i18n, seconds, {
capitalizeOff: true,
});
}
const onDurationClick = () => {
if (seconds === -1) {
this.setState({
modalState: ModalState.CustomDisappearingTimeout,
});
} else {
onSetDisappearingMessages(seconds);
}
};
return (
<MenuItem key={seconds} onClick={onDurationClick}>
<div
className={classNames(
TIMER_ITEM_CLASS,
isActiveExpireTimer(seconds) && `${TIMER_ITEM_CLASS}--active`
)}
>
{text}
</div>
</MenuItem>
);
});
return (
<ContextMenu id={triggerId}>
{disableTimerChanges ? null : (
<SubMenu title={disappearingTitle}>
{expirationTimer.DEFAULT_DURATIONS_IN_SECONDS.map(
(seconds: number) => (
<MenuItem
key={seconds}
onClick={() => {
onSetDisappearingMessages(seconds);
}}
>
{expirationTimer.format(i18n, seconds)}
</MenuItem>
)
)}
</SubMenu>
<SubMenu title={disappearingTitle}>{expireDurations}</SubMenu>
)}
<SubMenu title={muteTitle}>
{muteOptions.map(item => (
@ -578,36 +625,64 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
}
public render(): ReactNode {
const { id, isSMSOnly } = this.props;
const { isNarrow } = this.state;
const {
id,
isSMSOnly,
i18n,
onSetDisappearingMessages,
expireTimer,
} = this.props;
const { isNarrow, modalState } = this.state;
const triggerId = `conversation-${id}`;
let modalNode: ReactNode;
if (modalState === ModalState.NothingOpen) {
modalNode = undefined;
} else if (modalState === ModalState.CustomDisappearingTimeout) {
modalNode = (
<DisappearingTimeDialog
i18n={i18n}
initialValue={expireTimer}
onSubmit={value => {
this.setState({ modalState: ModalState.NothingOpen });
onSetDisappearingMessages(value);
}}
onClose={() => this.setState({ modalState: ModalState.NothingOpen })}
/>
);
} else {
throw missingCaseError(modalState);
}
return (
<Measure
bounds
onResize={({ bounds }) => {
if (!bounds || !bounds.width) {
return;
}
this.setState({ isNarrow: bounds.width < 500 });
}}
>
{({ measureRef }) => (
<div
className={classNames('module-ConversationHeader', {
'module-ConversationHeader--narrow': isNarrow,
})}
ref={measureRef}
>
{this.renderBackButton()}
{this.renderHeader()}
{!isSMSOnly && this.renderOutgoingCallButtons()}
{this.renderSearchButton()}
{this.renderMoreButton(triggerId)}
{this.renderMenu(triggerId)}
</div>
)}
</Measure>
<>
{modalNode}
<Measure
bounds
onResize={({ bounds }) => {
if (!bounds || !bounds.width) {
return;
}
this.setState({ isNarrow: bounds.width < 500 });
}}
>
{({ measureRef }) => (
<div
className={classNames('module-ConversationHeader', {
'module-ConversationHeader--narrow': isNarrow,
})}
ref={measureRef}
>
{this.renderBackButton()}
{this.renderHeader()}
{!isSMSOnly && this.renderOutgoingCallButtons()}
{this.renderSearchButton()}
{this.renderMoreButton(triggerId)}
{this.renderMenu(triggerId)}
</div>
)}
</Measure>
</>
);
}
}

View file

@ -0,0 +1,29 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json';
import { EXPIRE_TIMERS } from '../../test-both/util/expireTimers';
const story = storiesOf('Components/DisappearingTimeDialog', module);
const i18n = setupI18n('en', enMessages);
EXPIRE_TIMERS.forEach(({ value, label }) => {
story.add(`Initial value: ${label}`, () => {
return (
<DisappearingTimeDialog
i18n={i18n}
initialValue={value}
onSubmit={action('onSubmit')}
onClose={action('onClose')}
/>
);
});
});

View file

@ -0,0 +1,124 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-restricted-syntax */
import React, { useState } from 'react';
import { ConfirmationDialog } from '../ConfirmationDialog';
import { Select } from '../Select';
import { LocalizerType } from '../../types/Util';
import { Theme } from '../../util/theme';
const CSS_MODULE = 'module-disappearing-time-dialog';
const DEFAULT_VALUE = 60;
export type PropsType = Readonly<{
i18n: LocalizerType;
theme?: Theme;
initialValue?: number;
onSubmit: (value: number) => void;
onClose: () => void;
}>;
const UNITS = ['seconds', 'minutes', 'hours', 'days', 'weeks'];
const UNIT_TO_MS = new Map<string, number>([
['seconds', 1],
['minutes', 60],
['hours', 60 * 60],
['days', 24 * 60 * 60],
['weeks', 7 * 24 * 60 * 60],
]);
const RANGES = new Map<string, [number, number]>([
['seconds', [1, 60]],
['minutes', [1, 60]],
['hours', [1, 24]],
['days', [1, 7]],
['weeks', [1, 5]],
]);
export function DisappearingTimeDialog(props: PropsType): JSX.Element {
const {
i18n,
theme,
initialValue = DEFAULT_VALUE,
onSubmit,
onClose,
} = props;
let initialUnit = 'seconds';
let initialUnitValue = 1;
for (const unit of UNITS) {
const ms = UNIT_TO_MS.get(unit) || 1;
if (initialValue < ms) {
break;
}
initialUnit = unit;
initialUnitValue = Math.floor(initialValue / ms);
}
const [unitValue, setUnitValue] = useState(initialUnitValue);
const [unit, setUnit] = useState(initialUnit);
const range = RANGES.get(unit) || [1, 1];
const values: Array<number> = [];
for (let i = range[0]; i < range[1]; i += 1) {
values.push(i);
}
return (
<ConfirmationDialog
moduleClassName={CSS_MODULE}
i18n={i18n}
theme={theme}
onClose={onClose}
title={i18n('DisappearingTimeDialog__title')}
hasXButton
actions={[
{
text: i18n('DisappearingTimeDialog__set'),
style: 'affirmative',
action() {
onSubmit(unitValue * (UNIT_TO_MS.get(unit) || 1));
},
},
]}
>
<p>{i18n('DisappearingTimeDialog__body')}</p>
<section className={`${CSS_MODULE}__time-boxes`}>
<Select
moduleClassName={`${CSS_MODULE}__time-boxes__value`}
value={unitValue}
onChange={newValue => setUnitValue(parseInt(newValue, 10))}
options={values.map(value => ({ value, text: value.toString() }))}
/>
<Select
moduleClassName={`${CSS_MODULE}__time-boxes__units`}
value={unit}
onChange={newUnit => {
setUnit(newUnit);
const ranges = RANGES.get(newUnit);
if (!ranges) {
return;
}
const [min, max] = ranges;
setUnitValue(Math.max(min, Math.min(max - 1, unitValue)));
}}
options={UNITS.map(unitName => {
return {
value: unitName,
text: i18n(`DisappearingTimeDialog__${unitName}`),
};
})}
/>
</section>
</ConfirmationDialog>
);
}

View file

@ -298,6 +298,9 @@ const renderItem = (id: string) => (
conversationId=""
conversationAccepted
renderContact={() => '*ContactName*'}
renderUniversalTimerNotification={() => (
<div>*UniversalTimerNotification*</div>
)}
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
{...actions()}
/>

View file

@ -10,6 +10,7 @@ import { EmojiPicker } from '../emoji/EmojiPicker';
import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json';
import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem';
import { UniversalTimerNotification } from './UniversalTimerNotification';
import { CallMode } from '../../types/Calling';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
@ -34,6 +35,10 @@ const renderContact = (conversationId: string) => (
<React.Fragment key={conversationId}>{conversationId}</React.Fragment>
);
const renderUniversalTimerNotification = () => (
<UniversalTimerNotification i18n={i18n} expireTimer={3600} />
);
const getDefaultProps = () => ({
conversationId: 'conversation-id',
conversationAccepted: true,
@ -73,6 +78,7 @@ const getDefaultProps = () => ({
returnToActiveCall: action('returnToActiveCall'),
renderContact,
renderUniversalTimerNotification,
renderEmojiPicker,
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
});
@ -115,6 +121,10 @@ storiesOf('Components/Conversation/TimelineItem', module)
sender: getDefaultConversation(),
},
},
{
type: 'universalTimerNotification',
data: null,
},
{
type: 'callHistory',
data: {

View file

@ -90,6 +90,10 @@ type TimerNotificationType = {
type: 'timerNotification';
data: TimerNotificationProps;
};
type UniversalTimerNotificationType = {
type: 'universalTimerNotification';
data: null;
};
type SafetyNumberNotificationType = {
type: 'safetyNumberNotification';
data: SafetyNumberNotificationProps;
@ -132,6 +136,7 @@ export type TimelineItemType =
| ResetSessionNotificationType
| SafetyNumberNotificationType
| TimerNotificationType
| UniversalTimerNotificationType
| UnsupportedMessageType
| VerificationNotificationType;
@ -143,6 +148,7 @@ type PropsLocalType = {
isSelected: boolean;
selectMessage: (messageId: string, conversationId: string) => unknown;
renderContact: SmartContactRendererType;
renderUniversalTimerNotification: () => JSX.Element;
i18n: LocalizerType;
interactionMode: InteractionModeType;
theme?: ThemeType;
@ -169,6 +175,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
theme,
messageSizeChanged,
renderContact,
renderUniversalTimerNotification,
returnToActiveCall,
selectMessage,
startCallingLobby,
@ -225,6 +232,8 @@ export class TimelineItem extends React.PureComponent<PropsType> {
notification = (
<TimerNotification {...this.props} {...item.data} i18n={i18n} />
);
} else if (item.type === 'universalTimerNotification') {
notification = renderUniversalTimerNotification();
} else if (item.type === 'safetyNumberNotification') {
notification = (
<SafetyNumberNotification {...this.props} {...item.data} i18n={i18n} />

View file

@ -0,0 +1,21 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { storiesOf } from '@storybook/react';
import { UniversalTimerNotification } from './UniversalTimerNotification';
import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json';
import { EXPIRE_TIMERS } from '../../test-both/util/expireTimers';
const story = storiesOf('Components/UniversalTimerNotification', module);
const i18n = setupI18n('en', enMessages);
EXPIRE_TIMERS.forEach(({ value, label }) => {
story.add(`Initial value: ${label}`, () => {
return <UniversalTimerNotification i18n={i18n} expireTimer={value} />;
});
});

View file

@ -0,0 +1,28 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { LocalizerType } from '../../types/Util';
import * as expirationTimer from '../../util/expirationTimer';
export type Props = {
i18n: LocalizerType;
expireTimer: number;
};
export const UniversalTimerNotification: React.FC<Props> = props => {
const { i18n, expireTimer } = props;
if (!expireTimer) {
return null;
}
return (
<div className="module-universal-timer-notification">
{i18n('UniversalTimerNotification__text', {
timeValue: expirationTimer.format(i18n, expireTimer),
})}
</div>
);
};

View file

@ -29,13 +29,18 @@ const conversation: ConversationType = getDefaultConversation({
conversationColor: 'ultramarine' as const,
});
const createProps = (hasGroupLink = false): Props => ({
const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
addMembers: async () => {
action('addMembers');
},
canEditGroupInfo: false,
candidateContactsToAdd: times(10, () => getDefaultConversation()),
conversation,
conversation: expireTimer
? {
...conversation,
expireTimer,
}
: conversation,
hasGroupLink,
i18n,
isAdmin: false,
@ -122,6 +127,12 @@ story.add('Group Editable', () => {
return <ConversationDetails {...props} canEditGroupInfo />;
});
story.add('Group Editable with custom disappearing timeout', () => {
const props = createProps(false, 3 * 24 * 60 * 60);
return <ConversationDetails {...props} canEditGroupInfo />;
});
story.add('Group Links On', () => {
const props = createProps(true);

View file

@ -11,6 +11,10 @@ import { LocalizerType } from '../../../types/Util';
import { MediaItemType } from '../../LightboxGallery';
import { missingCaseError } from '../../../util/missingCaseError';
import { Select } from '../../Select';
import { DisappearingTimeDialog } from '../DisappearingTimeDialog';
import { PanelRow } from './PanelRow';
import { PanelSection } from './PanelSection';
import { AddGroupMembersModal } from './AddGroupMembersModal';
@ -34,6 +38,7 @@ enum ModalState {
NothingOpen,
EditingGroupAttributes,
AddingGroupMembers,
CustomDisappearingTimeout,
}
export type StateProps = {
@ -71,10 +76,6 @@ export type StateProps = {
export type Props = StateProps;
const expirationTimerDefaultSet = new Set<number>(
expirationTimer.DEFAULT_DURATIONS_IN_SECONDS
);
export const ConversationDetails: React.ComponentType<Props> = ({
addMembers,
canEditGroupInfo,
@ -111,8 +112,13 @@ export const ConversationDetails: React.ComponentType<Props> = ({
setAddGroupMembersRequestState,
] = useState<RequestState>(RequestState.Inactive);
const updateExpireTimer = (event: React.ChangeEvent<HTMLSelectElement>) => {
setDisappearingMessages(parseInt(event.target.value, 10));
const updateExpireTimer = (value: string) => {
const intValue = parseInt(value, 10);
if (intValue === -1) {
setModalState(ModalState.CustomDisappearingTimeout);
} else {
setDisappearingMessages(intValue);
}
};
if (conversation === undefined) {
@ -204,16 +210,54 @@ export const ConversationDetails: React.ComponentType<Props> = ({
/>
);
break;
case ModalState.CustomDisappearingTimeout:
modalNode = (
<DisappearingTimeDialog
i18n={i18n}
initialValue={conversation.expireTimer}
onSubmit={value => {
setModalState(ModalState.NothingOpen);
setDisappearingMessages(value);
}}
onClose={() => setModalState(ModalState.NothingOpen)}
/>
);
break;
default:
throw missingCaseError(modalState);
}
const expireTimer = conversation.expireTimer || 0;
const expireTimer: number = conversation.expireTimer || 0;
let expirationTimerDurations = expirationTimer.DEFAULT_DURATIONS_IN_SECONDS;
if (!expirationTimerDefaultSet.has(expireTimer)) {
expirationTimerDurations = [...expirationTimerDurations, expireTimer];
}
let expirationTimerOptions: ReadonlyArray<{
readonly value: number;
readonly text: string;
}> = expirationTimer.DEFAULT_DURATIONS_IN_SECONDS.map(seconds => {
const text = expirationTimer.format(i18n, seconds, {
capitalizeOff: true,
});
return {
value: seconds,
text,
};
});
const isCustomTimeSelected = !expirationTimer.DEFAULT_DURATIONS_SET.has(
expireTimer
);
// Custom time...
expirationTimerOptions = [
...expirationTimerOptions,
{
value: -1,
text: i18n(
isCustomTimeSelected
? 'selectedCustomDisappearingTimeOption'
: 'customDisappearingTimeOption'
),
},
];
return (
<div className="conversation-details-panel">
@ -241,18 +285,16 @@ export const ConversationDetails: React.ComponentType<Props> = ({
info={i18n('ConversationDetails--disappearing-messages-info')}
label={i18n('ConversationDetails--disappearing-messages-label')}
right={
<div className="module-conversation-details-select">
<select onChange={updateExpireTimer} value={expireTimer}>
{expirationTimerDurations.map((seconds: number) => {
const label = expirationTimer.format(i18n, seconds);
return (
<option value={seconds} key={seconds} aria-label={label}>
{label}
</option>
);
})}
</select>
</div>
<Select
onChange={updateExpireTimer}
value={isCustomTimeSelected ? -1 : expireTimer}
options={expirationTimerOptions}
/>
}
rightInfo={
isCustomTimeSelected
? expirationTimer.format(i18n, expireTimer)
: undefined
}
/>
) : null}

View file

@ -13,6 +13,7 @@ export type Props = {
label: string | React.ReactNode;
info?: string;
right?: string | React.ReactNode;
rightInfo?: string;
actions?: React.ReactNode;
onClick?: () => void;
};
@ -27,6 +28,7 @@ export const PanelRow: React.ComponentType<Props> = ({
label,
info,
right,
rightInfo,
actions,
onClick,
}) => {
@ -37,7 +39,14 @@ export const PanelRow: React.ComponentType<Props> = ({
<div>{label}</div>
{info !== undefined ? <div className={bem('info')}>{info}</div> : null}
</div>
{right !== undefined ? <div className={bem('right')}>{right}</div> : null}
{right !== undefined ? (
<div className={bem('right')}>
{right}
{rightInfo !== undefined ? (
<div className={bem('right-info')}>{rightInfo}</div>
) : null}
</div>
) : null}
{actions !== undefined ? (
<div className={alwaysShowActions ? '' : bem('actions')}>{actions}</div>
) : null}