Default disappearing message timeout fixes
This commit is contained in:
parent
c9415dcf67
commit
cd28e71bc6
25 changed files with 456 additions and 164 deletions
|
@ -6,10 +6,10 @@ 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 { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
import { EXPIRE_TIMERS } from '../../test-both/util/expireTimers';
|
||||
import { EXPIRE_TIMERS } from '../test-both/util/expireTimers';
|
||||
|
||||
const story = storiesOf('Components/DisappearingTimeDialog', module);
|
||||
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { Select } from '../Select';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { Theme } from '../../util/theme';
|
||||
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';
|
||||
|
37
ts/components/DisappearingTimerSelect.stories.tsx
Normal file
37
ts/components/DisappearingTimerSelect.stories.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import { DisappearingTimerSelect } from './DisappearingTimerSelect';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const story = storiesOf('Components/DisappearingTimerSelect', module);
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
type Props = {
|
||||
initialValue: number;
|
||||
};
|
||||
|
||||
const TimerSelectWrap: React.FC<Props> = ({ initialValue }) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
return (
|
||||
<DisappearingTimerSelect
|
||||
i18n={i18n}
|
||||
value={value}
|
||||
onChange={newValue => setValue(newValue)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
story.add('Initial value: 1 day', () => (
|
||||
<TimerSelectWrap initialValue={24 * 3600} />
|
||||
));
|
||||
|
||||
story.add('Initial value 3 days (Custom time)', () => (
|
||||
<TimerSelectWrap initialValue={3 * 24 * 3600} />
|
||||
));
|
106
ts/components/DisappearingTimerSelect.tsx
Normal file
106
ts/components/DisappearingTimerSelect.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState, ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import * as expirationTimer from '../util/expirationTimer';
|
||||
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
|
||||
|
||||
import { Select } from './Select';
|
||||
|
||||
const CSS_MODULE = 'module-disappearing-timer-select';
|
||||
|
||||
export type Props = {
|
||||
i18n: LocalizerType;
|
||||
|
||||
value?: number;
|
||||
onChange(value: number): void;
|
||||
};
|
||||
|
||||
export const DisappearingTimerSelect: React.FC<Props> = (props: Props) => {
|
||||
const { i18n, value = 0, onChange } = props;
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
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(
|
||||
value
|
||||
);
|
||||
|
||||
const onSelectChange = (newValue: string) => {
|
||||
const intValue = parseInt(newValue, 10);
|
||||
if (intValue === -1) {
|
||||
setIsModalOpen(true);
|
||||
} else {
|
||||
onChange(intValue);
|
||||
}
|
||||
};
|
||||
|
||||
// Custom time...
|
||||
expirationTimerOptions = [
|
||||
...expirationTimerOptions,
|
||||
{
|
||||
value: -1,
|
||||
text: i18n(
|
||||
isCustomTimeSelected
|
||||
? 'selectedCustomDisappearingTimeOption'
|
||||
: 'customDisappearingTimeOption'
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
let modalNode: ReactNode = null;
|
||||
if (isModalOpen) {
|
||||
modalNode = (
|
||||
<DisappearingTimeDialog
|
||||
i18n={i18n}
|
||||
initialValue={value}
|
||||
onSubmit={newValue => {
|
||||
setIsModalOpen(false);
|
||||
onChange(newValue);
|
||||
}}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let info: ReactNode;
|
||||
if (isCustomTimeSelected) {
|
||||
info = (
|
||||
<div className={`${CSS_MODULE}__info`}>
|
||||
{expirationTimer.format(i18n, value)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
CSS_MODULE,
|
||||
isCustomTimeSelected ? `${CSS_MODULE}--custom-time` : false
|
||||
)}
|
||||
>
|
||||
<Select
|
||||
onChange={onSelectChange}
|
||||
value={isCustomTimeSelected ? -1 : value}
|
||||
options={expirationTimerOptions}
|
||||
/>
|
||||
{info}
|
||||
{modalNode}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -126,6 +126,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
setComposeSearchTerm: action('setComposeSearchTerm'),
|
||||
setComposeGroupAvatar: action('setComposeGroupAvatar'),
|
||||
setComposeGroupName: action('setComposeGroupName'),
|
||||
setComposeGroupExpireTimer: action('setComposeGroupExpireTimer'),
|
||||
showArchivedConversations: action('showArchivedConversations'),
|
||||
showInbox: action('showInbox'),
|
||||
startComposing: action('startComposing'),
|
||||
|
@ -514,3 +515,53 @@ story.add('Captcha dialog: pending', () => (
|
|||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
// Set group metadata
|
||||
|
||||
story.add('Group Metadata: No Timer', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.SetGroupMetadata,
|
||||
groupAvatar: undefined,
|
||||
groupName: 'Group 1',
|
||||
groupExpireTimer: 0,
|
||||
hasError: false,
|
||||
isCreating: false,
|
||||
selectedContacts: defaultConversations,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Group Metadata: Regular Timer', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.SetGroupMetadata,
|
||||
groupAvatar: undefined,
|
||||
groupName: 'Group 1',
|
||||
groupExpireTimer: 24 * 3600,
|
||||
hasError: false,
|
||||
isCreating: false,
|
||||
selectedContacts: defaultConversations,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Group Metadata: Custom Timer', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.SetGroupMetadata,
|
||||
groupAvatar: undefined,
|
||||
groupName: 'Group 1',
|
||||
groupExpireTimer: 7 * 3600,
|
||||
hasError: false,
|
||||
isCreating: false,
|
||||
selectedContacts: defaultConversations,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -98,6 +98,7 @@ export type PropsType = {
|
|||
setComposeSearchTerm: (composeSearchTerm: string) => void;
|
||||
setComposeGroupAvatar: (_: undefined | ArrayBuffer) => void;
|
||||
setComposeGroupName: (_: string) => void;
|
||||
setComposeGroupExpireTimer: (_: number) => void;
|
||||
showArchivedConversations: () => void;
|
||||
showInbox: () => void;
|
||||
startComposing: () => void;
|
||||
|
@ -139,6 +140,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
setComposeSearchTerm,
|
||||
setComposeGroupAvatar,
|
||||
setComposeGroupName,
|
||||
setComposeGroupExpireTimer,
|
||||
showArchivedConversations,
|
||||
showInbox,
|
||||
startComposing,
|
||||
|
@ -342,6 +344,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
i18n,
|
||||
setComposeGroupAvatar,
|
||||
setComposeGroupName,
|
||||
setComposeGroupExpireTimer,
|
||||
onChangeComposeSearchTerm: event => {
|
||||
setComposeSearchTerm(event.target.value);
|
||||
},
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from 'react-contextmenu';
|
||||
|
||||
import { Emojify } from './Emojify';
|
||||
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
|
||||
import { DisappearingTimeDialog } from '../DisappearingTimeDialog';
|
||||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { InContactsIcon } from '../InContactsIcon';
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ export type Props = {
|
|||
expireTimer: number;
|
||||
};
|
||||
|
||||
const CSS_MODULE = 'module-universal-timer-notification';
|
||||
|
||||
export const UniversalTimerNotification: React.FC<Props> = props => {
|
||||
const { i18n, expireTimer } = props;
|
||||
|
||||
|
@ -18,11 +20,19 @@ export const UniversalTimerNotification: React.FC<Props> = props => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const timeValue = expirationTimer.format(i18n, expireTimer);
|
||||
|
||||
return (
|
||||
<div className="module-universal-timer-notification">
|
||||
{i18n('UniversalTimerNotification__text', {
|
||||
timeValue: expirationTimer.format(i18n, expireTimer),
|
||||
})}
|
||||
<div className={CSS_MODULE}>
|
||||
<div className={`${CSS_MODULE}__icon-container`}>
|
||||
<div className={`${CSS_MODULE}__icon`} />
|
||||
<div className={`${CSS_MODULE}__icon-label`}>{timeValue}</div>
|
||||
</div>
|
||||
<div className={`${CSS_MODULE}__message`}>
|
||||
{i18n('UniversalTimerNotification__text', {
|
||||
timeValue,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,15 +5,12 @@ import React, { useState, ReactNode } from 'react';
|
|||
|
||||
import { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { assert } from '../../../util/assert';
|
||||
import * as expirationTimer from '../../../util/expirationTimer';
|
||||
|
||||
import { LocalizerType } from '../../../types/Util';
|
||||
import { MediaItemType } from '../../LightboxGallery';
|
||||
import { missingCaseError } from '../../../util/missingCaseError';
|
||||
|
||||
import { Select } from '../../Select';
|
||||
|
||||
import { DisappearingTimeDialog } from '../DisappearingTimeDialog';
|
||||
import { DisappearingTimerSelect } from '../../DisappearingTimerSelect';
|
||||
|
||||
import { PanelRow } from './PanelRow';
|
||||
import { PanelSection } from './PanelSection';
|
||||
|
@ -39,7 +36,6 @@ enum ModalState {
|
|||
EditingGroupDescription,
|
||||
EditingGroupTitle,
|
||||
AddingGroupMembers,
|
||||
CustomDisappearingTimeout,
|
||||
}
|
||||
|
||||
export type StateProps = {
|
||||
|
@ -114,15 +110,6 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
setAddGroupMembersRequestState,
|
||||
] = useState<RequestState>(RequestState.Inactive);
|
||||
|
||||
const updateExpireTimer = (value: string) => {
|
||||
const intValue = parseInt(value, 10);
|
||||
if (intValue === -1) {
|
||||
setModalState(ModalState.CustomDisappearingTimeout);
|
||||
} else {
|
||||
setDisappearingMessages(intValue);
|
||||
}
|
||||
};
|
||||
|
||||
if (conversation === undefined) {
|
||||
throw new Error('ConversationDetails rendered without a conversation');
|
||||
}
|
||||
|
@ -218,55 +205,10 @@ 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: number = conversation.expireTimer || 0;
|
||||
|
||||
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">
|
||||
<ConversationDetailsHeader
|
||||
|
@ -297,17 +239,12 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
info={i18n('ConversationDetails--disappearing-messages-info')}
|
||||
label={i18n('ConversationDetails--disappearing-messages-label')}
|
||||
right={
|
||||
<Select
|
||||
onChange={updateExpireTimer}
|
||||
value={isCustomTimeSelected ? -1 : expireTimer}
|
||||
options={expirationTimerOptions}
|
||||
<DisappearingTimerSelect
|
||||
i18n={i18n}
|
||||
value={conversation.expireTimer || 0}
|
||||
onChange={setDisappearingMessages}
|
||||
/>
|
||||
}
|
||||
rightInfo={
|
||||
isCustomTimeSelected
|
||||
? expirationTimer.format(i18n, expireTimer)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<PanelRow
|
||||
|
|
|
@ -13,7 +13,6 @@ export type Props = {
|
|||
label: string | React.ReactNode;
|
||||
info?: string;
|
||||
right?: string | React.ReactNode;
|
||||
rightInfo?: string;
|
||||
actions?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
@ -28,7 +27,6 @@ export const PanelRow: React.ComponentType<Props> = ({
|
|||
label,
|
||||
info,
|
||||
right,
|
||||
rightInfo,
|
||||
actions,
|
||||
onClick,
|
||||
}) => {
|
||||
|
@ -39,14 +37,7 @@ 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}
|
||||
{rightInfo !== undefined ? (
|
||||
<div className={bem('right-info')}>{rightInfo}</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{right !== undefined ? <div className={bem('right')}>{right}</div> : null}
|
||||
{actions !== undefined ? (
|
||||
<div className={alwaysShowActions ? '' : bem('actions')}>{actions}</div>
|
||||
) : null}
|
||||
|
|
|
@ -50,6 +50,7 @@ export abstract class LeftPaneHelper<T> {
|
|||
i18n: LocalizerType;
|
||||
setComposeGroupAvatar: (_: undefined | ArrayBuffer) => unknown;
|
||||
setComposeGroupName: (_: string) => unknown;
|
||||
setComposeGroupExpireTimer: (_: number) => void;
|
||||
onChangeComposeSearchTerm: (
|
||||
event: ChangeEvent<HTMLInputElement>
|
||||
) => unknown;
|
||||
|
|
|
@ -6,6 +6,7 @@ import React, { ReactChild } from 'react';
|
|||
import { LeftPaneHelper } from './LeftPaneHelper';
|
||||
import { Row, RowType } from '../ConversationList';
|
||||
import { PropsDataType as ContactListItemPropsType } from '../conversationList/ContactListItem';
|
||||
import { DisappearingTimerSelect } from '../DisappearingTimerSelect';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { AvatarInput } from '../AvatarInput';
|
||||
import { Alert } from '../Alert';
|
||||
|
@ -16,6 +17,7 @@ import { GroupTitleInput } from '../GroupTitleInput';
|
|||
export type LeftPaneSetGroupMetadataPropsType = {
|
||||
groupAvatar: undefined | ArrayBuffer;
|
||||
groupName: string;
|
||||
groupExpireTimer: number;
|
||||
hasError: boolean;
|
||||
isCreating: boolean;
|
||||
selectedContacts: ReadonlyArray<ContactListItemPropsType>;
|
||||
|
@ -28,6 +30,8 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
|
|||
|
||||
private readonly groupName: string;
|
||||
|
||||
private readonly groupExpireTimer: number;
|
||||
|
||||
private readonly hasError: boolean;
|
||||
|
||||
private readonly isCreating: boolean;
|
||||
|
@ -37,6 +41,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
|
|||
constructor({
|
||||
groupAvatar,
|
||||
groupName,
|
||||
groupExpireTimer,
|
||||
isCreating,
|
||||
hasError,
|
||||
selectedContacts,
|
||||
|
@ -45,6 +50,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
|
|||
|
||||
this.groupAvatar = groupAvatar;
|
||||
this.groupName = groupName;
|
||||
this.groupExpireTimer = groupExpireTimer;
|
||||
this.hasError = hasError;
|
||||
this.isCreating = isCreating;
|
||||
this.selectedContacts = selectedContacts;
|
||||
|
@ -89,12 +95,14 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
|
|||
createGroup,
|
||||
i18n,
|
||||
setComposeGroupAvatar,
|
||||
setComposeGroupExpireTimer,
|
||||
setComposeGroupName,
|
||||
}: Readonly<{
|
||||
clearGroupCreationError: () => unknown;
|
||||
createGroup: () => unknown;
|
||||
i18n: LocalizerType;
|
||||
setComposeGroupAvatar: (_: undefined | ArrayBuffer) => unknown;
|
||||
setComposeGroupExpireTimer: (_: number) => void;
|
||||
setComposeGroupName: (_: string) => unknown;
|
||||
}>): ReactChild {
|
||||
const disabled = this.isCreating;
|
||||
|
@ -128,6 +136,17 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
|
|||
value={this.groupName}
|
||||
/>
|
||||
|
||||
<section className="module-left-pane__header__form__expire-timer">
|
||||
<div className="module-left-pane__header__form__expire-timer__label">
|
||||
{i18n('disappearingMessages')}
|
||||
</div>
|
||||
<DisappearingTimerSelect
|
||||
i18n={i18n}
|
||||
value={this.groupExpireTimer}
|
||||
onChange={setComposeGroupExpireTimer}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{this.hasError && (
|
||||
<Alert
|
||||
body={i18n('setGroupMetadata__error-message')}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue