Render disappearing message timers generically

This commit is contained in:
Evan Hahn 2021-05-03 18:24:40 -05:00 committed by GitHub
parent c1730e055f
commit 736075322c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 372 additions and 307 deletions

View file

@ -19,10 +19,7 @@ import { InContactsIcon } from '../InContactsIcon';
import { LocalizerType } from '../../types/Util';
import { ConversationType } from '../../state/ducks/conversations';
import { MuteOption, getMuteOptions } from '../../util/getMuteOptions';
import {
ExpirationTimerOptions,
TimerOption,
} from '../../util/ExpirationTimerOptions';
import * as expirationTimer from '../../util/expirationTimer';
import { isMuted } from '../../util/isMuted';
import { missingCaseError } from '../../util/missingCaseError';
@ -219,16 +216,13 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
private renderExpirationLength(): ReactNode {
const { i18n, expireTimer } = this.props;
const expirationSettingName = expireTimer
? ExpirationTimerOptions.getAbbreviated(i18n, expireTimer)
: undefined;
if (!expirationSettingName) {
if (!expireTimer) {
return null;
}
return (
<div className="module-ConversationHeader__header__info__subtitle__expiration">
{expirationSettingName}
{expirationTimer.format(i18n, expireTimer)}
</div>
);
}
@ -434,16 +428,18 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
<ContextMenu id={triggerId}>
{disableTimerChanges ? null : (
<SubMenu title={disappearingTitle}>
{ExpirationTimerOptions.map((item: typeof TimerOption) => (
<MenuItem
key={item.get('seconds')}
onClick={() => {
onSetDisappearingMessages(item.get('seconds'));
}}
>
{item.getName(i18n)}
</MenuItem>
))}
{expirationTimer.DEFAULT_DURATIONS_IN_SECONDS.map(
(seconds: number) => (
<MenuItem
key={seconds}
onClick={() => {
onSetDisappearingMessages(seconds);
}}
>
{expirationTimer.format(i18n, seconds)}
</MenuItem>
)
)}
</SubMenu>
)}
<SubMenu title={muteTitle}>

View file

@ -1,9 +1,10 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as moment from 'moment';
import { storiesOf } from '@storybook/react';
import { boolean, select, text } from '@storybook/addon-knobs';
import { boolean, number, select, text } from '@storybook/addon-knobs';
import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json';
@ -30,60 +31,69 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
text('profileName', overrideProps.profileName || '') || undefined,
title: text('title', overrideProps.title || ''),
name: text('name', overrideProps.name || '') || undefined,
disabled: boolean('disabled', overrideProps.disabled || false),
timespan: text('timespan', overrideProps.timespan || ''),
...(boolean('disabled', overrideProps.disabled || false)
? {
disabled: true,
}
: {
disabled: false,
expireTimer: number(
'expireTimer',
('expireTimer' in overrideProps ? overrideProps.expireTimer : 0) || 0
),
}),
});
story.add('Set By Other', () => {
const props = createProps({
expireTimer: moment.duration(1, 'hour').asSeconds(),
type: 'fromOther',
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
title: 'Mr. Fire',
timespan: '1 hour',
});
return (
<>
<TimerNotification {...props} />
<div style={{ padding: '1em' }} />
<TimerNotification {...props} disabled timespan="Off" />
<TimerNotification {...props} disabled />
</>
);
});
story.add('Set By You', () => {
const props = createProps({
expireTimer: moment.duration(1, 'hour').asSeconds(),
type: 'fromMe',
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
title: 'Mr. Fire',
timespan: '1 hour',
});
return (
<>
<TimerNotification {...props} />
<div style={{ padding: '1em' }} />
<TimerNotification {...props} disabled timespan="Off" />
<TimerNotification {...props} disabled />
</>
);
});
story.add('Set By Sync', () => {
const props = createProps({
expireTimer: moment.duration(1, 'hour').asSeconds(),
type: 'fromSync',
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
title: 'Mr. Fire',
timespan: '1 hour',
});
return (
<>
<TimerNotification {...props} />
<div style={{ padding: '1em' }} />
<TimerNotification {...props} disabled timespan="Off" />
<TimerNotification {...props} disabled />
</>
);
});

View file

@ -1,12 +1,13 @@
// Copyright 2018-2020 Signal Messenger, LLC
// Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { FunctionComponent, ReactNode } from 'react';
import classNames from 'classnames';
import { ContactName } from './ContactName';
import { Intl } from '../Intl';
import { LocalizerType } from '../../types/Util';
import * as expirationTimer from '../../util/expirationTimer';
export type TimerNotificationType =
| 'fromOther'
@ -14,15 +15,22 @@ export type TimerNotificationType =
| 'fromSync'
| 'fromMember';
// We can't always use destructuring assignment because of the complexity of this props
// type.
/* eslint-disable react/destructuring-assignment */
export type PropsData = {
type: TimerNotificationType;
phoneNumber?: string;
profileName?: string;
title: string;
name?: string;
disabled: boolean;
timespan: string;
};
} & (
| { disabled: true }
| {
disabled: false;
expireTimer: number;
}
);
type PropsHousekeeping = {
i18n: LocalizerType;
@ -30,82 +38,74 @@ type PropsHousekeeping = {
export type Props = PropsData & PropsHousekeeping;
export class TimerNotification extends React.Component<Props> {
public renderContents(): JSX.Element | string | null {
const {
i18n,
name,
phoneNumber,
profileName,
title,
timespan,
type,
disabled,
} = this.props;
const changeKey = disabled
? 'disabledDisappearingMessages'
: 'theyChangedTheTimer';
export const TimerNotification: FunctionComponent<Props> = props => {
const { disabled, i18n, name, phoneNumber, profileName, title, type } = props;
switch (type) {
case 'fromOther':
return (
<Intl
i18n={i18n}
id={changeKey}
components={{
name: (
<ContactName
key="external-1"
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
name={name}
i18n={i18n}
/>
),
time: timespan,
}}
/>
);
case 'fromMe':
return disabled
? i18n('youDisabledDisappearingMessages')
: i18n('youChangedTheTimer', [timespan]);
case 'fromSync':
return disabled
? i18n('disappearingMessagesDisabled')
: i18n('timerSetOnSync', [timespan]);
case 'fromMember':
return disabled
? i18n('disappearingMessagesDisabledByMember')
: i18n('timerSetByMember', [timespan]);
default:
window.log.warn('TimerNotification: unsupported type provided:', type);
return null;
}
let changeKey: string;
let timespan: string;
if (props.disabled) {
changeKey = 'disabledDisappearingMessages';
timespan = ''; // Set to the empty string to satisfy types
} else {
changeKey = 'theyChangedTheTimer';
timespan = expirationTimer.format(i18n, props.expireTimer);
}
public render(): JSX.Element {
const { timespan, disabled } = this.props;
let message: ReactNode;
switch (type) {
case 'fromOther':
message = (
<Intl
i18n={i18n}
id={changeKey}
components={{
name: (
<ContactName
key="external-1"
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
name={name}
i18n={i18n}
/>
),
time: timespan,
}}
/>
);
break;
case 'fromMe':
message = disabled
? i18n('youDisabledDisappearingMessages')
: i18n('youChangedTheTimer', [timespan]);
break;
case 'fromSync':
message = disabled
? i18n('disappearingMessagesDisabled')
: i18n('timerSetOnSync', [timespan]);
break;
case 'fromMember':
message = disabled
? i18n('disappearingMessagesDisabledByMember')
: i18n('timerSetByMember', [timespan]);
break;
default:
window.log.warn('TimerNotification: unsupported type provided:', type);
break;
}
return (
<div className="module-timer-notification">
<div className="module-timer-notification__icon-container">
<div
className={classNames(
'module-timer-notification__icon',
disabled ? 'module-timer-notification__icon--disabled' : null
)}
/>
<div className="module-timer-notification__icon-label">
{timespan}
</div>
</div>
<div className="module-timer-notification__message">
{this.renderContents()}
</div>
return (
<div className="module-timer-notification">
<div className="module-timer-notification__icon-container">
<div
className={classNames(
'module-timer-notification__icon',
disabled ? 'module-timer-notification__icon--disabled' : null
)}
/>
<div className="module-timer-notification__icon-label">{timespan}</div>
</div>
);
}
}
<div className="module-timer-notification__message">{message}</div>
</div>
);
};

View file

@ -5,10 +5,8 @@ import React, { useState, ReactNode } from 'react';
import { ConversationType } from '../../../state/ducks/conversations';
import { assert } from '../../../util/assert';
import {
ExpirationTimerOptions,
TimerOption,
} from '../../../util/ExpirationTimerOptions';
import * as expirationTimer from '../../../util/expirationTimer';
import { LocalizerType } from '../../../types/Util';
import { MediaItemType } from '../../LightboxGallery';
import { missingCaseError } from '../../../util/missingCaseError';
@ -228,15 +226,20 @@ export const ConversationDetails: React.ComponentType<Props> = ({
onChange={updateExpireTimer}
value={conversation.expireTimer || 0}
>
{ExpirationTimerOptions.map((item: typeof TimerOption) => (
<option
value={item.get('seconds')}
key={item.get('seconds')}
aria-label={item.getName(i18n)}
>
{item.getName(i18n)}
</option>
))}
{expirationTimer.DEFAULT_DURATIONS_IN_SECONDS.map(
(seconds: number) => {
const label = expirationTimer.format(i18n, seconds);
return (
<option
value={seconds}
key={seconds}
aria-label={label}
>
{label}
</option>
);
}
)}
</select>
</div>
}