Render disappearing message timers generically
This commit is contained in:
parent
c1730e055f
commit
736075322c
16 changed files with 372 additions and 307 deletions
|
@ -1538,6 +1538,33 @@ Signal Desktop makes use of the following open source projects.
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
## humanize-duration
|
||||||
|
|
||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <https://unlicense.org/>
|
||||||
|
|
||||||
## intl-tel-input
|
## intl-tel-input
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
|
@ -1657,106 +1657,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timerOption_0_seconds": {
|
"disappearingMessages__off": {
|
||||||
"message": "off",
|
"message": "off",
|
||||||
"description": "Label for option to turn off message expiration in the timer menu"
|
"description": "Label for option to turn off message expiration in the timer menu"
|
||||||
},
|
},
|
||||||
"timerOption_5_seconds": {
|
|
||||||
"message": "5 seconds",
|
|
||||||
"description": "Label for a selectable option in the message expiration timer menu"
|
|
||||||
},
|
|
||||||
"timerOption_10_seconds": {
|
|
||||||
"message": "10 seconds",
|
|
||||||
"description": "Label for a selectable option in the message expiration timer menu"
|
|
||||||
},
|
|
||||||
"timerOption_30_seconds": {
|
|
||||||
"message": "30 seconds",
|
|
||||||
"description": "Label for a selectable option in the message expiration timer menu"
|
|
||||||
},
|
|
||||||
"timerOption_1_minute": {
|
|
||||||
"message": "1 minute",
|
|
||||||
"description": "Label for a selectable option in the message expiration timer menu"
|
|
||||||
},
|
|
||||||
"timerOption_5_minutes": {
|
|
||||||
"message": "5 minutes",
|
|
||||||
"description": "Label for a selectable option in the message expiration timer menu"
|
|
||||||
},
|
|
||||||
"timerOption_30_minutes": {
|
|
||||||
"message": "30 minutes",
|
|
||||||
"description": "Label for a selectable option in the message expiration timer menu"
|
|
||||||
},
|
|
||||||
"timerOption_1_hour": {
|
|
||||||
"message": "1 hour",
|
|
||||||
"description": "Label for a selectable option in the message expiration timer menu"
|
|
||||||
},
|
|
||||||
"timerOption_6_hours": {
|
|
||||||
"message": "6 hours",
|
|
||||||
"description": "Label for a selectable option in the message expiration timer menu"
|
|
||||||
},
|
|
||||||
"timerOption_12_hours": {
|
|
||||||
"message": "12 hours",
|
|
||||||
"description": "Label for a selectable option in the message expiration timer menu"
|
|
||||||
},
|
|
||||||
"timerOption_1_day": {
|
|
||||||
"message": "1 day",
|
|
||||||
"description": "Label for a selectable option in the message expiration timer menu"
|
|
||||||
},
|
|
||||||
"timerOption_1_week": {
|
|
||||||
"message": "1 week",
|
|
||||||
"description": "Label for a selectable option in the message expiration timer menu"
|
|
||||||
},
|
|
||||||
"disappearingMessages": {
|
"disappearingMessages": {
|
||||||
"message": "Disappearing messages",
|
"message": "Disappearing messages",
|
||||||
"description": "Conversation menu option to enable disappearing messages"
|
"description": "Conversation menu option to enable disappearing messages"
|
||||||
},
|
},
|
||||||
"timerOption_0_seconds_abbreviated": {
|
|
||||||
"message": "off",
|
|
||||||
"description": "Short format indicating current timer setting in the conversation list snippet"
|
|
||||||
},
|
|
||||||
"timerOption_5_seconds_abbreviated": {
|
|
||||||
"message": "5s",
|
|
||||||
"description": "Very short format indicating current timer setting in the conversation header"
|
|
||||||
},
|
|
||||||
"timerOption_10_seconds_abbreviated": {
|
|
||||||
"message": "10s",
|
|
||||||
"description": "Very short format indicating current timer setting in the conversation header"
|
|
||||||
},
|
|
||||||
"timerOption_30_seconds_abbreviated": {
|
|
||||||
"message": "30s",
|
|
||||||
"description": "Very short format indicating current timer setting in the conversation header"
|
|
||||||
},
|
|
||||||
"timerOption_1_minute_abbreviated": {
|
|
||||||
"message": "1m",
|
|
||||||
"description": "Very short format indicating current timer setting in the conversation header"
|
|
||||||
},
|
|
||||||
"timerOption_5_minutes_abbreviated": {
|
|
||||||
"message": "5m",
|
|
||||||
"description": "Very short format indicating current timer setting in the conversation header"
|
|
||||||
},
|
|
||||||
"timerOption_30_minutes_abbreviated": {
|
|
||||||
"message": "30m",
|
|
||||||
"description": "Very short format indicating current timer setting in the conversation header"
|
|
||||||
},
|
|
||||||
"timerOption_1_hour_abbreviated": {
|
|
||||||
"message": "1h",
|
|
||||||
"description": "Very short format indicating current timer setting in the conversation header"
|
|
||||||
},
|
|
||||||
"timerOption_6_hours_abbreviated": {
|
|
||||||
"message": "6h",
|
|
||||||
"description": "Very short format indicating current timer setting in the conversation header"
|
|
||||||
},
|
|
||||||
"timerOption_12_hours_abbreviated": {
|
|
||||||
"message": "12h",
|
|
||||||
"description": "Very short format indicating current timer setting in the conversation header"
|
|
||||||
},
|
|
||||||
"timerOption_1_day_abbreviated": {
|
|
||||||
"message": "1d",
|
|
||||||
"description": "Very short format indicating current timer setting in the conversation header"
|
|
||||||
},
|
|
||||||
"timerOption_1_week_abbreviated": {
|
|
||||||
"message": "1w",
|
|
||||||
"description": "Very short format indicating current timer setting in the conversation header"
|
|
||||||
},
|
|
||||||
"disappearingMessagesDisabled": {
|
"disappearingMessagesDisabled": {
|
||||||
"message": "Disappearing messages disabled",
|
"message": "Disappearing messages disabled",
|
||||||
"description": "Displayed in the left pane when the timer is turned off"
|
"description": "Displayed in the left pane when the timer is turned off"
|
||||||
|
|
|
@ -97,6 +97,7 @@
|
||||||
"google-libphonenumber": "3.2.17",
|
"google-libphonenumber": "3.2.17",
|
||||||
"got": "8.3.2",
|
"got": "8.3.2",
|
||||||
"history": "4.9.0",
|
"history": "4.9.0",
|
||||||
|
"humanize-duration": "3.26.0",
|
||||||
"intl-tel-input": "12.1.15",
|
"intl-tel-input": "12.1.15",
|
||||||
"jquery": "3.5.0",
|
"jquery": "3.5.0",
|
||||||
"js-yaml": "3.13.1",
|
"js-yaml": "3.13.1",
|
||||||
|
@ -186,6 +187,7 @@
|
||||||
"@types/google-libphonenumber": "7.4.14",
|
"@types/google-libphonenumber": "7.4.14",
|
||||||
"@types/got": "9.4.1",
|
"@types/got": "9.4.1",
|
||||||
"@types/history": "4.7.2",
|
"@types/history": "4.7.2",
|
||||||
|
"@types/humanize-duration": "^3.18.1",
|
||||||
"@types/jquery": "3.5.0",
|
"@types/jquery": "3.5.0",
|
||||||
"@types/js-yaml": "3.12.0",
|
"@types/js-yaml": "3.12.0",
|
||||||
"@types/linkify-it": "2.1.0",
|
"@types/linkify-it": "2.1.0",
|
||||||
|
|
|
@ -19,10 +19,7 @@ import { InContactsIcon } from '../InContactsIcon';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
import { ConversationType } from '../../state/ducks/conversations';
|
import { ConversationType } from '../../state/ducks/conversations';
|
||||||
import { MuteOption, getMuteOptions } from '../../util/getMuteOptions';
|
import { MuteOption, getMuteOptions } from '../../util/getMuteOptions';
|
||||||
import {
|
import * as expirationTimer from '../../util/expirationTimer';
|
||||||
ExpirationTimerOptions,
|
|
||||||
TimerOption,
|
|
||||||
} from '../../util/ExpirationTimerOptions';
|
|
||||||
import { isMuted } from '../../util/isMuted';
|
import { isMuted } from '../../util/isMuted';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
|
|
||||||
|
@ -219,16 +216,13 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
private renderExpirationLength(): ReactNode {
|
private renderExpirationLength(): ReactNode {
|
||||||
const { i18n, expireTimer } = this.props;
|
const { i18n, expireTimer } = this.props;
|
||||||
|
|
||||||
const expirationSettingName = expireTimer
|
if (!expireTimer) {
|
||||||
? ExpirationTimerOptions.getAbbreviated(i18n, expireTimer)
|
|
||||||
: undefined;
|
|
||||||
if (!expirationSettingName) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-ConversationHeader__header__info__subtitle__expiration">
|
<div className="module-ConversationHeader__header__info__subtitle__expiration">
|
||||||
{expirationSettingName}
|
{expirationTimer.format(i18n, expireTimer)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -434,16 +428,18 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
<ContextMenu id={triggerId}>
|
<ContextMenu id={triggerId}>
|
||||||
{disableTimerChanges ? null : (
|
{disableTimerChanges ? null : (
|
||||||
<SubMenu title={disappearingTitle}>
|
<SubMenu title={disappearingTitle}>
|
||||||
{ExpirationTimerOptions.map((item: typeof TimerOption) => (
|
{expirationTimer.DEFAULT_DURATIONS_IN_SECONDS.map(
|
||||||
<MenuItem
|
(seconds: number) => (
|
||||||
key={item.get('seconds')}
|
<MenuItem
|
||||||
onClick={() => {
|
key={seconds}
|
||||||
onSetDisappearingMessages(item.get('seconds'));
|
onClick={() => {
|
||||||
}}
|
onSetDisappearingMessages(seconds);
|
||||||
>
|
}}
|
||||||
{item.getName(i18n)}
|
>
|
||||||
</MenuItem>
|
{expirationTimer.format(i18n, seconds)}
|
||||||
))}
|
</MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
)}
|
)}
|
||||||
<SubMenu title={muteTitle}>
|
<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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import * as moment from 'moment';
|
||||||
import { storiesOf } from '@storybook/react';
|
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 { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
@ -30,60 +31,69 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
text('profileName', overrideProps.profileName || '') || undefined,
|
text('profileName', overrideProps.profileName || '') || undefined,
|
||||||
title: text('title', overrideProps.title || ''),
|
title: text('title', overrideProps.title || ''),
|
||||||
name: text('name', overrideProps.name || '') || undefined,
|
name: text('name', overrideProps.name || '') || undefined,
|
||||||
disabled: boolean('disabled', overrideProps.disabled || false),
|
...(boolean('disabled', overrideProps.disabled || false)
|
||||||
timespan: text('timespan', overrideProps.timespan || ''),
|
? {
|
||||||
|
disabled: true,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
disabled: false,
|
||||||
|
expireTimer: number(
|
||||||
|
'expireTimer',
|
||||||
|
('expireTimer' in overrideProps ? overrideProps.expireTimer : 0) || 0
|
||||||
|
),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
story.add('Set By Other', () => {
|
story.add('Set By Other', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
|
expireTimer: moment.duration(1, 'hour').asSeconds(),
|
||||||
type: 'fromOther',
|
type: 'fromOther',
|
||||||
phoneNumber: '(202) 555-1000',
|
phoneNumber: '(202) 555-1000',
|
||||||
profileName: 'Mr. Fire',
|
profileName: 'Mr. Fire',
|
||||||
title: 'Mr. Fire',
|
title: 'Mr. Fire',
|
||||||
timespan: '1 hour',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TimerNotification {...props} />
|
<TimerNotification {...props} />
|
||||||
<div style={{ padding: '1em' }} />
|
<div style={{ padding: '1em' }} />
|
||||||
<TimerNotification {...props} disabled timespan="Off" />
|
<TimerNotification {...props} disabled />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
story.add('Set By You', () => {
|
story.add('Set By You', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
|
expireTimer: moment.duration(1, 'hour').asSeconds(),
|
||||||
type: 'fromMe',
|
type: 'fromMe',
|
||||||
phoneNumber: '(202) 555-1000',
|
phoneNumber: '(202) 555-1000',
|
||||||
profileName: 'Mr. Fire',
|
profileName: 'Mr. Fire',
|
||||||
title: 'Mr. Fire',
|
title: 'Mr. Fire',
|
||||||
timespan: '1 hour',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TimerNotification {...props} />
|
<TimerNotification {...props} />
|
||||||
<div style={{ padding: '1em' }} />
|
<div style={{ padding: '1em' }} />
|
||||||
<TimerNotification {...props} disabled timespan="Off" />
|
<TimerNotification {...props} disabled />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
story.add('Set By Sync', () => {
|
story.add('Set By Sync', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
|
expireTimer: moment.duration(1, 'hour').asSeconds(),
|
||||||
type: 'fromSync',
|
type: 'fromSync',
|
||||||
phoneNumber: '(202) 555-1000',
|
phoneNumber: '(202) 555-1000',
|
||||||
profileName: 'Mr. Fire',
|
profileName: 'Mr. Fire',
|
||||||
title: 'Mr. Fire',
|
title: 'Mr. Fire',
|
||||||
timespan: '1 hour',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TimerNotification {...props} />
|
<TimerNotification {...props} />
|
||||||
<div style={{ padding: '1em' }} />
|
<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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React, { FunctionComponent, ReactNode } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { ContactName } from './ContactName';
|
import { ContactName } from './ContactName';
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
import * as expirationTimer from '../../util/expirationTimer';
|
||||||
|
|
||||||
export type TimerNotificationType =
|
export type TimerNotificationType =
|
||||||
| 'fromOther'
|
| 'fromOther'
|
||||||
|
@ -14,15 +15,22 @@ export type TimerNotificationType =
|
||||||
| 'fromSync'
|
| 'fromSync'
|
||||||
| 'fromMember';
|
| 'fromMember';
|
||||||
|
|
||||||
|
// We can't always use destructuring assignment because of the complexity of this props
|
||||||
|
// type.
|
||||||
|
/* eslint-disable react/destructuring-assignment */
|
||||||
export type PropsData = {
|
export type PropsData = {
|
||||||
type: TimerNotificationType;
|
type: TimerNotificationType;
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
title: string;
|
title: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
disabled: boolean;
|
} & (
|
||||||
timespan: string;
|
| { disabled: true }
|
||||||
};
|
| {
|
||||||
|
disabled: false;
|
||||||
|
expireTimer: number;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
type PropsHousekeeping = {
|
type PropsHousekeeping = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -30,82 +38,74 @@ type PropsHousekeeping = {
|
||||||
|
|
||||||
export type Props = PropsData & PropsHousekeeping;
|
export type Props = PropsData & PropsHousekeeping;
|
||||||
|
|
||||||
export class TimerNotification extends React.Component<Props> {
|
export const TimerNotification: FunctionComponent<Props> = props => {
|
||||||
public renderContents(): JSX.Element | string | null {
|
const { disabled, i18n, name, phoneNumber, profileName, title, type } = props;
|
||||||
const {
|
|
||||||
i18n,
|
|
||||||
name,
|
|
||||||
phoneNumber,
|
|
||||||
profileName,
|
|
||||||
title,
|
|
||||||
timespan,
|
|
||||||
type,
|
|
||||||
disabled,
|
|
||||||
} = this.props;
|
|
||||||
const changeKey = disabled
|
|
||||||
? 'disabledDisappearingMessages'
|
|
||||||
: 'theyChangedTheTimer';
|
|
||||||
|
|
||||||
switch (type) {
|
let changeKey: string;
|
||||||
case 'fromOther':
|
let timespan: string;
|
||||||
return (
|
if (props.disabled) {
|
||||||
<Intl
|
changeKey = 'disabledDisappearingMessages';
|
||||||
i18n={i18n}
|
timespan = ''; // Set to the empty string to satisfy types
|
||||||
id={changeKey}
|
} else {
|
||||||
components={{
|
changeKey = 'theyChangedTheTimer';
|
||||||
name: (
|
timespan = expirationTimer.format(i18n, props.expireTimer);
|
||||||
<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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
let message: ReactNode;
|
||||||
const { timespan, disabled } = this.props;
|
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 (
|
return (
|
||||||
<div className="module-timer-notification">
|
<div className="module-timer-notification">
|
||||||
<div className="module-timer-notification__icon-container">
|
<div className="module-timer-notification__icon-container">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-timer-notification__icon',
|
'module-timer-notification__icon',
|
||||||
disabled ? 'module-timer-notification__icon--disabled' : null
|
disabled ? 'module-timer-notification__icon--disabled' : null
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="module-timer-notification__icon-label">
|
<div className="module-timer-notification__icon-label">{timespan}</div>
|
||||||
{timespan}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="module-timer-notification__message">
|
|
||||||
{this.renderContents()}
|
|
||||||
</div>
|
|
||||||
</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 { ConversationType } from '../../../state/ducks/conversations';
|
||||||
import { assert } from '../../../util/assert';
|
import { assert } from '../../../util/assert';
|
||||||
import {
|
import * as expirationTimer from '../../../util/expirationTimer';
|
||||||
ExpirationTimerOptions,
|
|
||||||
TimerOption,
|
|
||||||
} from '../../../util/ExpirationTimerOptions';
|
|
||||||
import { LocalizerType } from '../../../types/Util';
|
import { LocalizerType } from '../../../types/Util';
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../LightboxGallery';
|
||||||
import { missingCaseError } from '../../../util/missingCaseError';
|
import { missingCaseError } from '../../../util/missingCaseError';
|
||||||
|
@ -228,15 +226,20 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
onChange={updateExpireTimer}
|
onChange={updateExpireTimer}
|
||||||
value={conversation.expireTimer || 0}
|
value={conversation.expireTimer || 0}
|
||||||
>
|
>
|
||||||
{ExpirationTimerOptions.map((item: typeof TimerOption) => (
|
{expirationTimer.DEFAULT_DURATIONS_IN_SECONDS.map(
|
||||||
<option
|
(seconds: number) => {
|
||||||
value={item.get('seconds')}
|
const label = expirationTimer.format(i18n, seconds);
|
||||||
key={item.get('seconds')}
|
return (
|
||||||
aria-label={item.getName(i18n)}
|
<option
|
||||||
>
|
value={seconds}
|
||||||
{item.getName(i18n)}
|
key={seconds}
|
||||||
</option>
|
aria-label={label}
|
||||||
))}
|
>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
} from '../components/conversation/Message';
|
} from '../components/conversation/Message';
|
||||||
import { OwnProps as SmartMessageDetailPropsType } from '../state/smart/MessageDetail';
|
import { OwnProps as SmartMessageDetailPropsType } from '../state/smart/MessageDetail';
|
||||||
import { CallbackResultType } from '../textsecure/SendMessage';
|
import { CallbackResultType } from '../textsecure/SendMessage';
|
||||||
import { ExpirationTimerOptions } from '../util/ExpirationTimerOptions';
|
import * as expirationTimer from '../util/expirationTimer';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { ColorType } from '../types/Colors';
|
import { ColorType } from '../types/Colors';
|
||||||
import { CallMode } from '../types/Calling';
|
import { CallMode } from '../types/Calling';
|
||||||
|
@ -629,10 +629,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { expireTimer, fromSync, source, sourceUuid } = timerUpdate;
|
const { expireTimer, fromSync, source, sourceUuid } = timerUpdate;
|
||||||
const timespan = ExpirationTimerOptions.getName(
|
|
||||||
window.i18n,
|
|
||||||
expireTimer || 0
|
|
||||||
);
|
|
||||||
const disabled = !expireTimer;
|
const disabled = !expireTimer;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
@ -645,9 +641,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
const basicProps = {
|
const basicProps = {
|
||||||
...formattedContact,
|
...formattedContact,
|
||||||
type: 'fromOther' as TimerNotificationType,
|
|
||||||
timespan,
|
|
||||||
disabled,
|
disabled,
|
||||||
|
expireTimer,
|
||||||
|
type: 'fromOther' as TimerNotificationType,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (fromSync) {
|
if (fromSync) {
|
||||||
|
@ -1552,7 +1548,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: window.i18n('timerSetTo', [
|
text: window.i18n('timerSetTo', [
|
||||||
ExpirationTimerOptions.getAbbreviated(window.i18n, expireTimer || 0),
|
expirationTimer.format(window.i18n, expireTimer),
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,16 @@ export function getEmptyState(): UserStateType {
|
||||||
platform: 'missing',
|
platform: 'missing',
|
||||||
interactionMode: 'mouse',
|
interactionMode: 'mouse',
|
||||||
theme: ThemeType.light,
|
theme: ThemeType.light,
|
||||||
i18n: () => 'missing',
|
i18n: Object.assign(
|
||||||
|
() => {
|
||||||
|
throw new Error('i18n not yet set up');
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getLocale() {
|
||||||
|
throw new Error('i18n not yet set up');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
132
ts/test-both/util/expirationTimer_test.ts
Normal file
132
ts/test-both/util/expirationTimer_test.ts
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
import esMessages from '../../../_locales/es/messages.json';
|
||||||
|
import nbMessages from '../../../_locales/nb/messages.json';
|
||||||
|
import nnMessages from '../../../_locales/nn/messages.json';
|
||||||
|
import ptBrMessages from '../../../_locales/pt_BR/messages.json';
|
||||||
|
import zhCnMessages from '../../../_locales/zh_CN/messages.json';
|
||||||
|
|
||||||
|
import * as expirationTimer from '../../util/expirationTimer';
|
||||||
|
|
||||||
|
describe('expiration timer utilities', () => {
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
describe('DEFAULT_DURATIONS_IN_SECONDS', () => {
|
||||||
|
const { DEFAULT_DURATIONS_IN_SECONDS } = expirationTimer;
|
||||||
|
|
||||||
|
it('includes at least 3 durations', () => {
|
||||||
|
assert.isAtLeast(DEFAULT_DURATIONS_IN_SECONDS.length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes 1 hour as seconds', () => {
|
||||||
|
const oneHour = moment.duration(1, 'hour').asSeconds();
|
||||||
|
assert.include(DEFAULT_DURATIONS_IN_SECONDS, oneHour);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format', () => {
|
||||||
|
const { format } = expirationTimer;
|
||||||
|
|
||||||
|
it('handles an undefined duration', () => {
|
||||||
|
assert.strictEqual(format(i18n, undefined), 'off');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles no duration', () => {
|
||||||
|
assert.strictEqual(format(i18n, 0), 'off');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats durations', () => {
|
||||||
|
new Map<number, string>([
|
||||||
|
[1, '1 second'],
|
||||||
|
[2, '2 seconds'],
|
||||||
|
[30, '30 seconds'],
|
||||||
|
[59, '59 seconds'],
|
||||||
|
[moment.duration(1, 'm').asSeconds(), '1 minute'],
|
||||||
|
[moment.duration(5, 'm').asSeconds(), '5 minutes'],
|
||||||
|
[moment.duration(1, 'h').asSeconds(), '1 hour'],
|
||||||
|
[moment.duration(8, 'h').asSeconds(), '8 hours'],
|
||||||
|
[moment.duration(1, 'd').asSeconds(), '1 day'],
|
||||||
|
[moment.duration(6, 'd').asSeconds(), '6 days'],
|
||||||
|
[moment.duration(8, 'd').asSeconds(), '8 days'],
|
||||||
|
[moment.duration(30, 'd').asSeconds(), '30 days'],
|
||||||
|
[moment.duration(365, 'd').asSeconds(), '365 days'],
|
||||||
|
[moment.duration(1, 'w').asSeconds(), '1 week'],
|
||||||
|
[moment.duration(3, 'w').asSeconds(), '3 weeks'],
|
||||||
|
[moment.duration(52, 'w').asSeconds(), '52 weeks'],
|
||||||
|
]).forEach((expected, input) => {
|
||||||
|
assert.strictEqual(format(i18n, input), expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats other languages successfully', () => {
|
||||||
|
const esI18n = setupI18n('es', esMessages);
|
||||||
|
assert.strictEqual(format(esI18n, 120), '2 minutos');
|
||||||
|
|
||||||
|
const zhCnI18n = setupI18n('zh_CN', zhCnMessages);
|
||||||
|
assert.strictEqual(format(zhCnI18n, 60), '1 分钟');
|
||||||
|
|
||||||
|
// The underlying library supports the "pt" locale, not the "pt_BR" locale. That's
|
||||||
|
// what we're testing here.
|
||||||
|
const ptBrI18n = setupI18n('pt_BR', ptBrMessages);
|
||||||
|
assert.strictEqual(
|
||||||
|
format(ptBrI18n, moment.duration(5, 'days').asSeconds()),
|
||||||
|
'5 dias'
|
||||||
|
);
|
||||||
|
|
||||||
|
// The underlying library supports the Norwegian language, which is a macrolanguage
|
||||||
|
// for Bokmål and Nynorsk.
|
||||||
|
[setupI18n('nb', nbMessages), setupI18n('nn', nnMessages)].forEach(
|
||||||
|
norwegianI18n => {
|
||||||
|
assert.strictEqual(
|
||||||
|
format(norwegianI18n, moment.duration(6, 'hours').asSeconds()),
|
||||||
|
'6 timer'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to English if the locale is not supported', () => {
|
||||||
|
const badI18n = setupI18n('bogus', {});
|
||||||
|
assert.strictEqual(format(badI18n, 120), '2 minutes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles a "mix" of units gracefully', () => {
|
||||||
|
// We don't expect there to be a "mix" of units, but we shouldn't choke if a bad
|
||||||
|
// client gives us an unexpected timestamp.
|
||||||
|
const mix = moment
|
||||||
|
.duration(6, 'days')
|
||||||
|
.add(moment.duration(2, 'hours'))
|
||||||
|
.asSeconds();
|
||||||
|
assert.strictEqual(format(i18n, mix), '6 days, 2 hours');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles negative numbers gracefully', () => {
|
||||||
|
// The proto helps enforce non-negative numbers by specifying a u32, but because
|
||||||
|
// JavaScript lacks such a type, we test it here.
|
||||||
|
assert.strictEqual(format(i18n, -1), '1 second');
|
||||||
|
assert.strictEqual(format(i18n, -120), '2 minutes');
|
||||||
|
assert.strictEqual(format(i18n, -0), 'off');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles fractional seconds gracefully', () => {
|
||||||
|
// The proto helps enforce integer numbers by specifying a u32, but this function
|
||||||
|
// shouldn't choke if bad data is passed somehow.
|
||||||
|
assert.strictEqual(format(i18n, 4.2), '4 seconds');
|
||||||
|
assert.strictEqual(format(i18n, 4.8), '4 seconds');
|
||||||
|
assert.strictEqual(format(i18n, 0.2), '1 second');
|
||||||
|
assert.strictEqual(format(i18n, 0.8), '1 second');
|
||||||
|
|
||||||
|
// If multiple things go wrong and we pass a fractional negative number, we still
|
||||||
|
// shouldn't explode.
|
||||||
|
assert.strictEqual(format(i18n, -4.2), '4 seconds');
|
||||||
|
assert.strictEqual(format(i18n, -4.8), '4 seconds');
|
||||||
|
assert.strictEqual(format(i18n, -0.2), '1 second');
|
||||||
|
assert.strictEqual(format(i18n, -0.8), '1 second');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -67,7 +67,7 @@ describe('MentionCompletion', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const options: MentionCompletionOptions = {
|
const options: MentionCompletionOptions = {
|
||||||
i18n: sinon.stub(),
|
i18n: Object.assign(sinon.stub(), { getLocale: sinon.stub() }),
|
||||||
me,
|
me,
|
||||||
memberRepositoryRef,
|
memberRepositoryRef,
|
||||||
setMentionPickerElement: sinon.stub(),
|
setMentionPickerElement: sinon.stub(),
|
||||||
|
|
|
@ -20,10 +20,10 @@ export type ReplacementValuesType = {
|
||||||
[key: string]: string | undefined;
|
[key: string]: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LocalizerType = (
|
export type LocalizerType = {
|
||||||
key: string,
|
(key: string, values?: Array<string | null> | ReplacementValuesType): string;
|
||||||
values?: Array<string | null> | ReplacementValuesType
|
getLocale(): string;
|
||||||
) => string;
|
};
|
||||||
|
|
||||||
export enum ThemeType {
|
export enum ThemeType {
|
||||||
'light' = 'light',
|
'light' = 'light',
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import * as Backbone from 'backbone';
|
|
||||||
import * as moment from 'moment';
|
|
||||||
import { LocalizerType } from '../types/Util';
|
|
||||||
|
|
||||||
type ExpirationTime = [
|
|
||||||
number,
|
|
||||||
(
|
|
||||||
| 'second'
|
|
||||||
| 'seconds'
|
|
||||||
| 'minute'
|
|
||||||
| 'minutes'
|
|
||||||
| 'hour'
|
|
||||||
| 'hours'
|
|
||||||
| 'day'
|
|
||||||
| 'week'
|
|
||||||
)
|
|
||||||
];
|
|
||||||
const EXPIRATION_TIMES: Array<ExpirationTime> = [
|
|
||||||
[0, 'seconds'],
|
|
||||||
[5, 'seconds'],
|
|
||||||
[10, 'seconds'],
|
|
||||||
[30, 'seconds'],
|
|
||||||
[1, 'minute'],
|
|
||||||
[5, 'minutes'],
|
|
||||||
[30, 'minutes'],
|
|
||||||
[1, 'hour'],
|
|
||||||
[6, 'hours'],
|
|
||||||
[12, 'hours'],
|
|
||||||
[1, 'day'],
|
|
||||||
[1, 'week'],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const TimerOption = Backbone.Model.extend({
|
|
||||||
getName(i18n: LocalizerType) {
|
|
||||||
return (
|
|
||||||
i18n(['timerOption', this.get('time'), this.get('unit')].join('_')) ||
|
|
||||||
moment.duration(this.get('time'), this.get('unit')).humanize()
|
|
||||||
);
|
|
||||||
},
|
|
||||||
getAbbreviated(i18n: LocalizerType) {
|
|
||||||
return i18n(
|
|
||||||
['timerOption', this.get('time'), this.get('unit'), 'abbreviated'].join(
|
|
||||||
'_'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ExpirationTimerOptions = new (Backbone.Collection.extend({
|
|
||||||
model: TimerOption,
|
|
||||||
getName(i18n: LocalizerType, seconds = 0) {
|
|
||||||
const o = this.findWhere({ seconds });
|
|
||||||
if (o) {
|
|
||||||
return o.getName(i18n);
|
|
||||||
}
|
|
||||||
return [seconds, 'seconds'].join(' ');
|
|
||||||
},
|
|
||||||
getAbbreviated(i18n: LocalizerType, seconds = 0) {
|
|
||||||
const o = this.findWhere({ seconds });
|
|
||||||
if (o) {
|
|
||||||
return o.getAbbreviated(i18n);
|
|
||||||
}
|
|
||||||
return [seconds, 's'].join('');
|
|
||||||
},
|
|
||||||
}))(
|
|
||||||
EXPIRATION_TIMES.map(o => {
|
|
||||||
const duration = moment.duration(o[0], o[1]); // 5, 'seconds'
|
|
||||||
return {
|
|
||||||
time: o[0],
|
|
||||||
unit: o[1],
|
|
||||||
seconds: duration.asSeconds(),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
49
ts/util/expirationTimer.ts
Normal file
49
ts/util/expirationTimer.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as moment from 'moment';
|
||||||
|
import humanizeDuration from 'humanize-duration';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
const SECONDS_PER_WEEK = 604800;
|
||||||
|
export const DEFAULT_DURATIONS_IN_SECONDS = [
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
10,
|
||||||
|
30,
|
||||||
|
moment.duration(1, 'minute').asSeconds(),
|
||||||
|
moment.duration(5, 'minutes').asSeconds(),
|
||||||
|
moment.duration(30, 'minutes').asSeconds(),
|
||||||
|
moment.duration(1, 'hour').asSeconds(),
|
||||||
|
moment.duration(6, 'hours').asSeconds(),
|
||||||
|
moment.duration(12, 'hours').asSeconds(),
|
||||||
|
moment.duration(1, 'day').asSeconds(),
|
||||||
|
moment.duration(1, 'week').asSeconds(),
|
||||||
|
];
|
||||||
|
|
||||||
|
export function format(i18n: LocalizerType, dirtySeconds?: number): string {
|
||||||
|
let seconds = Math.abs(dirtySeconds || 0);
|
||||||
|
if (!seconds) {
|
||||||
|
return i18n('disappearingMessages__off');
|
||||||
|
}
|
||||||
|
seconds = Math.max(Math.floor(seconds), 1);
|
||||||
|
|
||||||
|
const locale: string = i18n.getLocale();
|
||||||
|
const localeWithoutRegion: string = locale.split('_', 1)[0];
|
||||||
|
const fallbacks: Array<string> = [];
|
||||||
|
if (localeWithoutRegion !== locale) {
|
||||||
|
fallbacks.push(localeWithoutRegion);
|
||||||
|
}
|
||||||
|
if (localeWithoutRegion === 'nb' || localeWithoutRegion === 'nn') {
|
||||||
|
fallbacks.push('no');
|
||||||
|
}
|
||||||
|
if (localeWithoutRegion !== 'en') {
|
||||||
|
fallbacks.push('en');
|
||||||
|
}
|
||||||
|
|
||||||
|
return humanizeDuration(seconds * 1000, {
|
||||||
|
units: seconds % SECONDS_PER_WEEK === 0 ? ['w'] : ['d', 'h', 'm', 's'],
|
||||||
|
language: locale,
|
||||||
|
...(fallbacks.length ? { fallbacks } : {}),
|
||||||
|
});
|
||||||
|
}
|
|
@ -16492,7 +16492,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/ConversationHeader.js",
|
"path": "ts/components/conversation/ConversationHeader.js",
|
||||||
"line": " this.menuTriggerRef = react_1.default.createRef();",
|
"line": " this.menuTriggerRef = react_1.default.createRef();",
|
||||||
"lineNumber": 32,
|
"lineNumber": 51,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-08-28T16:12:19.904Z",
|
"updated": "2020-08-28T16:12:19.904Z",
|
||||||
"reasonDetail": "Used to reference popup menu"
|
"reasonDetail": "Used to reference popup menu"
|
||||||
|
@ -16501,7 +16501,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/ConversationHeader.tsx",
|
"path": "ts/components/conversation/ConversationHeader.tsx",
|
||||||
"line": " this.menuTriggerRef = React.createRef();",
|
"line": " this.menuTriggerRef = React.createRef();",
|
||||||
"lineNumber": 112,
|
"lineNumber": 109,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-20T20:10:43.540Z",
|
"updated": "2020-05-20T20:10:43.540Z",
|
||||||
"reasonDetail": "Used to reference popup menu"
|
"reasonDetail": "Used to reference popup menu"
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -2344,6 +2344,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/humanize-duration@^3.18.1":
|
||||||
|
version "3.18.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/humanize-duration/-/humanize-duration-3.18.1.tgz#10090d596053703e7de0ac43a37b96cd9fc78309"
|
||||||
|
integrity sha512-MUgbY3CF7hg/a/jogixmAufLjJBQT7WEf8Q+kYJkOc47ytngg1IuZobCngdTjAgY83JWEogippge5O5fplaQlw==
|
||||||
|
|
||||||
"@types/integer@*":
|
"@types/integer@*":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/integer/-/integer-1.0.1.tgz#025d87e30d97f539fcc6087372af7d3672ffbbe6"
|
resolved "https://registry.yarnpkg.com/@types/integer/-/integer-1.0.1.tgz#025d87e30d97f539fcc6087372af7d3672ffbbe6"
|
||||||
|
@ -9607,6 +9612,11 @@ human-signals@^2.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
|
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
|
||||||
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
|
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
|
||||||
|
|
||||||
|
humanize-duration@3.26.0:
|
||||||
|
version "3.26.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.26.0.tgz#4d77f6b3d2fe0ca1ff14623ccc2b2f8b48ab1aaf"
|
||||||
|
integrity sha512-SddekX3p5ApvPY6bbAYppGKe874jP6iFZXYtrQToDV4R0j2UpTYPqwTFM2QpXpuw9DhS/eXTUnKYTF9TbXAJ6A==
|
||||||
|
|
||||||
iconv-corefoundation@^1.1.5:
|
iconv-corefoundation@^1.1.5:
|
||||||
version "1.1.5"
|
version "1.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-corefoundation/-/iconv-corefoundation-1.1.5.tgz#90596d444a579aeb109f5ca113f6bb665a41be2b"
|
resolved "https://registry.yarnpkg.com/iconv-corefoundation/-/iconv-corefoundation-1.1.5.tgz#90596d444a579aeb109f5ca113f6bb665a41be2b"
|
||||||
|
|
Loading…
Reference in a new issue