Render disappearing message timers generically
This commit is contained in:
parent
c1730e055f
commit
736075322c
16 changed files with 372 additions and 307 deletions
|
@ -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}>
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue