Use DurationInSeconds for expireTimer
This commit is contained in:
parent
cf57c7aaf0
commit
6be69a7ba8
59 changed files with 411 additions and 216 deletions
|
@ -7,6 +7,7 @@ import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import { Select } from './Select';
|
import { Select } from './Select';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import type { Theme } from '../util/theme';
|
import type { Theme } from '../util/theme';
|
||||||
|
import { DurationInSeconds } from '../util/durations';
|
||||||
|
|
||||||
const CSS_MODULE = 'module-disappearing-time-dialog';
|
const CSS_MODULE = 'module-disappearing-time-dialog';
|
||||||
|
|
||||||
|
@ -15,14 +16,14 @@ const DEFAULT_VALUE = 60;
|
||||||
export type PropsType = Readonly<{
|
export type PropsType = Readonly<{
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
initialValue?: number;
|
initialValue?: DurationInSeconds;
|
||||||
onSubmit: (value: number) => void;
|
onSubmit: (value: DurationInSeconds) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const UNITS = ['seconds', 'minutes', 'hours', 'days', 'weeks'];
|
const UNITS = ['seconds', 'minutes', 'hours', 'days', 'weeks'];
|
||||||
|
|
||||||
const UNIT_TO_MS = new Map<string, number>([
|
const UNIT_TO_SEC = new Map<string, number>([
|
||||||
['seconds', 1],
|
['seconds', 1],
|
||||||
['minutes', 60],
|
['minutes', 60],
|
||||||
['hours', 60 * 60],
|
['hours', 60 * 60],
|
||||||
|
@ -50,14 +51,14 @@ export function DisappearingTimeDialog(props: PropsType): JSX.Element {
|
||||||
let initialUnit = 'seconds';
|
let initialUnit = 'seconds';
|
||||||
let initialUnitValue = 1;
|
let initialUnitValue = 1;
|
||||||
for (const unit of UNITS) {
|
for (const unit of UNITS) {
|
||||||
const ms = UNIT_TO_MS.get(unit) || 1;
|
const sec = UNIT_TO_SEC.get(unit) || 1;
|
||||||
|
|
||||||
if (initialValue < ms) {
|
if (initialValue < sec) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
initialUnit = unit;
|
initialUnit = unit;
|
||||||
initialUnitValue = Math.floor(initialValue / ms);
|
initialUnitValue = Math.floor(initialValue / sec);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [unitValue, setUnitValue] = useState(initialUnitValue);
|
const [unitValue, setUnitValue] = useState(initialUnitValue);
|
||||||
|
@ -84,7 +85,11 @@ export function DisappearingTimeDialog(props: PropsType): JSX.Element {
|
||||||
text: i18n('DisappearingTimeDialog__set'),
|
text: i18n('DisappearingTimeDialog__set'),
|
||||||
style: 'affirmative',
|
style: 'affirmative',
|
||||||
action() {
|
action() {
|
||||||
onSubmit(unitValue * (UNIT_TO_MS.get(unit) || 1));
|
onSubmit(
|
||||||
|
DurationInSeconds.fromSeconds(
|
||||||
|
unitValue * (UNIT_TO_SEC.get(unit) ?? 1)
|
||||||
|
)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import React, { useState } from 'react';
|
||||||
|
|
||||||
import { DisappearingTimerSelect } from './DisappearingTimerSelect';
|
import { DisappearingTimerSelect } from './DisappearingTimerSelect';
|
||||||
import { setupI18n } from '../util/setupI18n';
|
import { setupI18n } from '../util/setupI18n';
|
||||||
|
import { DurationInSeconds } from '../util/durations';
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -23,7 +24,7 @@ const TimerSelectWrap: React.FC<Props> = ({ initialValue }) => {
|
||||||
return (
|
return (
|
||||||
<DisappearingTimerSelect
|
<DisappearingTimerSelect
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
value={value}
|
value={DurationInSeconds.fromSeconds(value)}
|
||||||
onChange={newValue => setValue(newValue)}
|
onChange={newValue => setValue(newValue)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import * as expirationTimer from '../util/expirationTimer';
|
import * as expirationTimer from '../util/expirationTimer';
|
||||||
|
import { DurationInSeconds } from '../util/durations';
|
||||||
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
|
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
|
||||||
|
|
||||||
import { Select } from './Select';
|
import { Select } from './Select';
|
||||||
|
@ -16,17 +17,17 @@ const CSS_MODULE = 'module-disappearing-timer-select';
|
||||||
export type Props = {
|
export type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
|
||||||
value?: number;
|
value?: DurationInSeconds;
|
||||||
onChange(value: number): void;
|
onChange(value: DurationInSeconds): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DisappearingTimerSelect: React.FC<Props> = (props: Props) => {
|
export const DisappearingTimerSelect: React.FC<Props> = (props: Props) => {
|
||||||
const { i18n, value = 0, onChange } = props;
|
const { i18n, value = DurationInSeconds.ZERO, onChange } = props;
|
||||||
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
let expirationTimerOptions: ReadonlyArray<{
|
let expirationTimerOptions: ReadonlyArray<{
|
||||||
readonly value: number;
|
readonly value: DurationInSeconds;
|
||||||
readonly text: string;
|
readonly text: string;
|
||||||
}> = expirationTimer.DEFAULT_DURATIONS_IN_SECONDS.map(seconds => {
|
}> = expirationTimer.DEFAULT_DURATIONS_IN_SECONDS.map(seconds => {
|
||||||
const text = expirationTimer.format(i18n, seconds, {
|
const text = expirationTimer.format(i18n, seconds, {
|
||||||
|
@ -42,7 +43,7 @@ export const DisappearingTimerSelect: React.FC<Props> = (props: Props) => {
|
||||||
!expirationTimer.DEFAULT_DURATIONS_SET.has(value);
|
!expirationTimer.DEFAULT_DURATIONS_SET.has(value);
|
||||||
|
|
||||||
const onSelectChange = (newValue: string) => {
|
const onSelectChange = (newValue: string) => {
|
||||||
const intValue = parseInt(newValue, 10);
|
const intValue = DurationInSeconds.fromSeconds(parseInt(newValue, 10));
|
||||||
if (intValue === -1) {
|
if (intValue === -1) {
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
} else {
|
} else {
|
||||||
|
@ -54,7 +55,7 @@ export const DisappearingTimerSelect: React.FC<Props> = (props: Props) => {
|
||||||
expirationTimerOptions = [
|
expirationTimerOptions = [
|
||||||
...expirationTimerOptions,
|
...expirationTimerOptions,
|
||||||
{
|
{
|
||||||
value: -1,
|
value: DurationInSeconds.fromSeconds(-1),
|
||||||
text: i18n(
|
text: i18n(
|
||||||
isCustomTimeSelected
|
isCustomTimeSelected
|
||||||
? 'selectedCustomDisappearingTimeOption'
|
? 'selectedCustomDisappearingTimeOption'
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { CrashReportDialog } from './CrashReportDialog';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import { MessageSearchResult } from './conversationList/MessageSearchResult';
|
import { MessageSearchResult } from './conversationList/MessageSearchResult';
|
||||||
import { setupI18n } from '../util/setupI18n';
|
import { setupI18n } from '../util/setupI18n';
|
||||||
|
import { DurationInSeconds } from '../util/durations';
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
import { ThemeType } from '../types/Util';
|
import { ThemeType } from '../types/Util';
|
||||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||||
|
@ -953,7 +954,7 @@ export const GroupMetadataNoTimer = (): JSX.Element => (
|
||||||
mode: LeftPaneMode.SetGroupMetadata,
|
mode: LeftPaneMode.SetGroupMetadata,
|
||||||
groupAvatar: undefined,
|
groupAvatar: undefined,
|
||||||
groupName: 'Group 1',
|
groupName: 'Group 1',
|
||||||
groupExpireTimer: 0,
|
groupExpireTimer: DurationInSeconds.ZERO,
|
||||||
hasError: false,
|
hasError: false,
|
||||||
isCreating: false,
|
isCreating: false,
|
||||||
isEditingAvatar: false,
|
isEditingAvatar: false,
|
||||||
|
@ -975,7 +976,7 @@ export const GroupMetadataRegularTimer = (): JSX.Element => (
|
||||||
mode: LeftPaneMode.SetGroupMetadata,
|
mode: LeftPaneMode.SetGroupMetadata,
|
||||||
groupAvatar: undefined,
|
groupAvatar: undefined,
|
||||||
groupName: 'Group 1',
|
groupName: 'Group 1',
|
||||||
groupExpireTimer: 24 * 3600,
|
groupExpireTimer: DurationInSeconds.DAY,
|
||||||
hasError: false,
|
hasError: false,
|
||||||
isCreating: false,
|
isCreating: false,
|
||||||
isEditingAvatar: false,
|
isEditingAvatar: false,
|
||||||
|
@ -997,7 +998,7 @@ export const GroupMetadataCustomTimer = (): JSX.Element => (
|
||||||
mode: LeftPaneMode.SetGroupMetadata,
|
mode: LeftPaneMode.SetGroupMetadata,
|
||||||
groupAvatar: undefined,
|
groupAvatar: undefined,
|
||||||
groupName: 'Group 1',
|
groupName: 'Group 1',
|
||||||
groupExpireTimer: 7 * 3600,
|
groupExpireTimer: DurationInSeconds.fromHours(7),
|
||||||
hasError: false,
|
hasError: false,
|
||||||
isCreating: false,
|
isCreating: false,
|
||||||
isEditingAvatar: false,
|
isEditingAvatar: false,
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { ScrollBehavior } from '../types/Util';
|
||||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||||
import { usePrevious } from '../hooks/usePrevious';
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
|
import type { DurationInSeconds } from '../util/durations';
|
||||||
import type { WidthBreakpoint } from './_util';
|
import type { WidthBreakpoint } from './_util';
|
||||||
import { getConversationListWidthBreakpoint } from './_util';
|
import { getConversationListWidthBreakpoint } from './_util';
|
||||||
import * as KeyboardLayout from '../services/keyboardLayout';
|
import * as KeyboardLayout from '../services/keyboardLayout';
|
||||||
|
@ -106,7 +107,7 @@ export type PropsType = {
|
||||||
savePreferredLeftPaneWidth: (_: number) => void;
|
savePreferredLeftPaneWidth: (_: number) => void;
|
||||||
searchInConversation: (conversationId: string) => unknown;
|
searchInConversation: (conversationId: string) => unknown;
|
||||||
setComposeGroupAvatar: (_: undefined | Uint8Array) => void;
|
setComposeGroupAvatar: (_: undefined | Uint8Array) => void;
|
||||||
setComposeGroupExpireTimer: (_: number) => void;
|
setComposeGroupExpireTimer: (_: DurationInSeconds) => void;
|
||||||
setComposeGroupName: (_: string) => void;
|
setComposeGroupName: (_: string) => void;
|
||||||
setComposeSearchTerm: (composeSearchTerm: string) => void;
|
setComposeSearchTerm: (composeSearchTerm: string) => void;
|
||||||
showArchivedConversations: () => void;
|
showArchivedConversations: () => void;
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { DEFAULT_CONVERSATION_COLOR } from '../types/Colors';
|
||||||
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
|
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
|
||||||
import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
|
import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
|
||||||
import { objectMap } from '../util/objectMap';
|
import { objectMap } from '../util/objectMap';
|
||||||
|
import { DurationInSeconds } from '../util/durations';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -107,7 +108,7 @@ const getDefaultArgs = (): PropsDataType => ({
|
||||||
selectedSpeaker: availableSpeakers[1],
|
selectedSpeaker: availableSpeakers[1],
|
||||||
shouldShowStoriesSettings: true,
|
shouldShowStoriesSettings: true,
|
||||||
themeSetting: 'system',
|
themeSetting: 'system',
|
||||||
universalExpireTimer: 3600,
|
universalExpireTimer: DurationInSeconds.HOUR,
|
||||||
whoCanFindMe: PhoneNumberDiscoverability.Discoverable,
|
whoCanFindMe: PhoneNumberDiscoverability.Discoverable,
|
||||||
whoCanSeeMe: PhoneNumberSharingMode.Everybody,
|
whoCanSeeMe: PhoneNumberSharingMode.Everybody,
|
||||||
zoomFactor: 1,
|
zoomFactor: 1,
|
||||||
|
@ -186,7 +187,7 @@ BlockedMany.args = {
|
||||||
|
|
||||||
export const CustomUniversalExpireTimer = Template.bind({});
|
export const CustomUniversalExpireTimer = Template.bind({});
|
||||||
CustomUniversalExpireTimer.args = {
|
CustomUniversalExpireTimer.args = {
|
||||||
universalExpireTimer: 9000,
|
universalExpireTimer: DurationInSeconds.fromSeconds(9000),
|
||||||
};
|
};
|
||||||
CustomUniversalExpireTimer.story = {
|
CustomUniversalExpireTimer.story = {
|
||||||
name: 'Custom universalExpireTimer',
|
name: 'Custom universalExpireTimer',
|
||||||
|
|
|
@ -37,6 +37,7 @@ import {
|
||||||
DEFAULT_DURATIONS_SET,
|
DEFAULT_DURATIONS_SET,
|
||||||
format as formatExpirationTimer,
|
format as formatExpirationTimer,
|
||||||
} from '../util/expirationTimer';
|
} from '../util/expirationTimer';
|
||||||
|
import { DurationInSeconds } from '../util/durations';
|
||||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||||
import { useUniqueId } from '../hooks/useUniqueId';
|
import { useUniqueId } from '../hooks/useUniqueId';
|
||||||
import { useTheme } from '../hooks/useTheme';
|
import { useTheme } from '../hooks/useTheme';
|
||||||
|
@ -76,7 +77,7 @@ export type PropsDataType = {
|
||||||
selectedMicrophone?: AudioDevice;
|
selectedMicrophone?: AudioDevice;
|
||||||
selectedSpeaker?: AudioDevice;
|
selectedSpeaker?: AudioDevice;
|
||||||
themeSetting: ThemeSettingType;
|
themeSetting: ThemeSettingType;
|
||||||
universalExpireTimer: number;
|
universalExpireTimer: DurationInSeconds;
|
||||||
whoCanFindMe: PhoneNumberDiscoverability;
|
whoCanFindMe: PhoneNumberDiscoverability;
|
||||||
whoCanSeeMe: PhoneNumberSharingMode;
|
whoCanSeeMe: PhoneNumberSharingMode;
|
||||||
zoomFactor: ZoomFactorType;
|
zoomFactor: ZoomFactorType;
|
||||||
|
@ -280,7 +281,7 @@ export const Preferences = ({
|
||||||
setGlobalDefaultConversationColor,
|
setGlobalDefaultConversationColor,
|
||||||
shouldShowStoriesSettings,
|
shouldShowStoriesSettings,
|
||||||
themeSetting,
|
themeSetting,
|
||||||
universalExpireTimer = 0,
|
universalExpireTimer = DurationInSeconds.ZERO,
|
||||||
whoCanFindMe,
|
whoCanFindMe,
|
||||||
whoCanSeeMe,
|
whoCanSeeMe,
|
||||||
zoomFactor,
|
zoomFactor,
|
||||||
|
@ -954,7 +955,7 @@ export const Preferences = ({
|
||||||
{
|
{
|
||||||
value: isCustomDisappearingMessageValue
|
value: isCustomDisappearingMessageValue
|
||||||
? universalExpireTimer
|
? universalExpireTimer
|
||||||
: -1,
|
: DurationInSeconds.fromSeconds(-1),
|
||||||
text: isCustomDisappearingMessageValue
|
text: isCustomDisappearingMessageValue
|
||||||
? formatExpirationTimer(i18n, universalExpireTimer)
|
? formatExpirationTimer(i18n, universalExpireTimer)
|
||||||
: i18n('selectedCustomDisappearingTimeOption'),
|
: i18n('selectedCustomDisappearingTimeOption'),
|
||||||
|
|
|
@ -13,9 +13,10 @@ import { Intl } from './Intl';
|
||||||
import { Modal } from './Modal';
|
import { Modal } from './Modal';
|
||||||
import { SendStatus } from '../messages/MessageSendState';
|
import { SendStatus } from '../messages/MessageSendState';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
|
import { formatDateTimeLong } from '../util/timestamp';
|
||||||
|
import { DurationInSeconds } from '../util/durations';
|
||||||
import { ThemeType } from '../types/Util';
|
import { ThemeType } from '../types/Util';
|
||||||
import { Time } from './Time';
|
import { Time } from './Time';
|
||||||
import { formatDateTimeLong } from '../util/timestamp';
|
|
||||||
import { groupBy } from '../util/mapUtil';
|
import { groupBy } from '../util/mapUtil';
|
||||||
import { format as formatRelativeTime } from '../util/expirationTimer';
|
import { format as formatRelativeTime } from '../util/expirationTimer';
|
||||||
|
|
||||||
|
@ -189,7 +190,7 @@ export const StoryDetailsModal = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeRemaining = expirationTimestamp
|
const timeRemaining = expirationTimestamp
|
||||||
? expirationTimestamp - Date.now()
|
? DurationInSeconds.fromMillis(expirationTimestamp - Date.now())
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -254,7 +255,7 @@ export const StoryDetailsModal = ({
|
||||||
id="StoryDetailsModal__disappears-in"
|
id="StoryDetailsModal__disappears-in"
|
||||||
components={[
|
components={[
|
||||||
<span className="StoryDetailsModal__debugger__button__text">
|
<span className="StoryDetailsModal__debugger__button__text">
|
||||||
{formatRelativeTime(i18n, timeRemaining / 1000, {
|
{formatRelativeTime(i18n, timeRemaining, {
|
||||||
largest: 2,
|
largest: 2,
|
||||||
})}
|
})}
|
||||||
</span>,
|
</span>,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { action } from '@storybook/addon-actions';
|
||||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||||
import { getRandomColor } from '../../test-both/helpers/getRandomColor';
|
import { getRandomColor } from '../../test-both/helpers/getRandomColor';
|
||||||
import { setupI18n } from '../../util/setupI18n';
|
import { setupI18n } from '../../util/setupI18n';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext';
|
import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext';
|
||||||
import {
|
import {
|
||||||
|
@ -152,7 +153,7 @@ export const PrivateConvo = (): JSX.Element => {
|
||||||
phoneNumber: '(202) 555-0005',
|
phoneNumber: '(202) 555-0005',
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
id: '7',
|
id: '7',
|
||||||
expireTimer: 10,
|
expireTimer: DurationInSeconds.fromSeconds(10),
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -165,7 +166,7 @@ export const PrivateConvo = (): JSX.Element => {
|
||||||
phoneNumber: '(202) 555-0005',
|
phoneNumber: '(202) 555-0005',
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
id: '8',
|
id: '8',
|
||||||
expireTimer: 300,
|
expireTimer: DurationInSeconds.fromSeconds(300),
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
canChangeTimer: true,
|
canChangeTimer: true,
|
||||||
|
@ -231,7 +232,7 @@ export const Group = (): JSX.Element => {
|
||||||
phoneNumber: '',
|
phoneNumber: '',
|
||||||
id: '11',
|
id: '11',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
expireTimer: 10,
|
expireTimer: DurationInSeconds.fromSeconds(10),
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
outgoingCallButtonStyle: OutgoingCallButtonStyle.JustVideo,
|
outgoingCallButtonStyle: OutgoingCallButtonStyle.JustVideo,
|
||||||
},
|
},
|
||||||
|
@ -247,7 +248,7 @@ export const Group = (): JSX.Element => {
|
||||||
id: '12',
|
id: '12',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
left: true,
|
left: true,
|
||||||
expireTimer: 10,
|
expireTimer: DurationInSeconds.fromSeconds(10),
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
outgoingCallButtonStyle: OutgoingCallButtonStyle.JustVideo,
|
outgoingCallButtonStyle: OutgoingCallButtonStyle.JustVideo,
|
||||||
},
|
},
|
||||||
|
@ -262,7 +263,7 @@ export const Group = (): JSX.Element => {
|
||||||
phoneNumber: '',
|
phoneNumber: '',
|
||||||
id: '13',
|
id: '13',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
expireTimer: 10,
|
expireTimer: DurationInSeconds.fromSeconds(10),
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
outgoingCallButtonStyle: OutgoingCallButtonStyle.Join,
|
outgoingCallButtonStyle: OutgoingCallButtonStyle.Join,
|
||||||
},
|
},
|
||||||
|
@ -277,7 +278,7 @@ export const Group = (): JSX.Element => {
|
||||||
phoneNumber: '',
|
phoneNumber: '',
|
||||||
id: '14',
|
id: '14',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
expireTimer: 10,
|
expireTimer: DurationInSeconds.fromSeconds(10),
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
outgoingCallButtonStyle: OutgoingCallButtonStyle.JustVideo,
|
outgoingCallButtonStyle: OutgoingCallButtonStyle.JustVideo,
|
||||||
muteExpiresAt: Infinity,
|
muteExpiresAt: Infinity,
|
||||||
|
|
|
@ -28,6 +28,7 @@ import * as expirationTimer from '../../util/expirationTimer';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||||
import { isConversationMuted } from '../../util/isConversationMuted';
|
import { isConversationMuted } from '../../util/isConversationMuted';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
import {
|
import {
|
||||||
useStartCallShortcuts,
|
useStartCallShortcuts,
|
||||||
useKeyboardShortcuts,
|
useKeyboardShortcuts,
|
||||||
|
@ -79,7 +80,7 @@ export type PropsDataType = {
|
||||||
|
|
||||||
export type PropsActionsType = {
|
export type PropsActionsType = {
|
||||||
onSetMuteNotifications: (seconds: number) => void;
|
onSetMuteNotifications: (seconds: number) => void;
|
||||||
onSetDisappearingMessages: (seconds: number) => void;
|
onSetDisappearingMessages: (seconds: DurationInSeconds) => void;
|
||||||
onDeleteMessages: () => void;
|
onDeleteMessages: () => void;
|
||||||
onSearchInConversation: () => void;
|
onSearchInConversation: () => void;
|
||||||
onOutgoingAudioCallInConversation: () => void;
|
onOutgoingAudioCallInConversation: () => void;
|
||||||
|
@ -406,8 +407,8 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
|
|
||||||
const expireDurations: ReadonlyArray<ReactNode> = [
|
const expireDurations: ReadonlyArray<ReactNode> = [
|
||||||
...expirationTimer.DEFAULT_DURATIONS_IN_SECONDS,
|
...expirationTimer.DEFAULT_DURATIONS_IN_SECONDS,
|
||||||
-1,
|
DurationInSeconds.fromSeconds(-1),
|
||||||
].map((seconds: number) => {
|
].map(seconds => {
|
||||||
let text: string;
|
let text: string;
|
||||||
|
|
||||||
if (seconds === -1) {
|
if (seconds === -1) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { SendStatus } from '../../messages/MessageSendState';
|
||||||
import { WidthBreakpoint } from '../_util';
|
import { WidthBreakpoint } from '../_util';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import { formatDateTimeLong } from '../../util/timestamp';
|
import { formatDateTimeLong } from '../../util/timestamp';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
import { format as formatRelativeTime } from '../../util/expirationTimer';
|
import { format as formatRelativeTime } from '../../util/expirationTimer';
|
||||||
|
|
||||||
export type Contact = Pick<
|
export type Contact = Pick<
|
||||||
|
@ -302,7 +303,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const timeRemaining = expirationTimestamp
|
const timeRemaining = expirationTimestamp
|
||||||
? expirationTimestamp - Date.now()
|
? DurationInSeconds.fromMillis(expirationTimestamp - Date.now())
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -422,7 +423,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
{i18n('MessageDetail--disappears-in')}
|
{i18n('MessageDetail--disappears-in')}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{formatRelativeTime(i18n, timeRemaining / 1000, {
|
{formatRelativeTime(i18n, timeRemaining, {
|
||||||
largest: 2,
|
largest: 2,
|
||||||
})}
|
})}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
// 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 { times } from 'lodash';
|
import { times } from 'lodash';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { text, boolean, number } from '@storybook/addon-knobs';
|
import { text, boolean, number } from '@storybook/addon-knobs';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
import { setupI18n } from '../../util/setupI18n';
|
import { setupI18n } from '../../util/setupI18n';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import type { PropsType } from './Timeline';
|
import type { PropsType } from './Timeline';
|
||||||
import { Timeline } from './Timeline';
|
import { Timeline } from './Timeline';
|
||||||
|
@ -135,7 +135,7 @@ const items: Record<string, TimelineItemType> = {
|
||||||
type: 'timerNotification',
|
type: 'timerNotification',
|
||||||
data: {
|
data: {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
expireTimer: moment.duration(2, 'hours').asSeconds(),
|
expireTimer: DurationInSeconds.fromHours(2),
|
||||||
title: "It's Me",
|
title: "It's Me",
|
||||||
type: 'fromMe',
|
type: 'fromMe',
|
||||||
},
|
},
|
||||||
|
@ -145,7 +145,7 @@ const items: Record<string, TimelineItemType> = {
|
||||||
type: 'timerNotification',
|
type: 'timerNotification',
|
||||||
data: {
|
data: {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
expireTimer: moment.duration(2, 'hours').asSeconds(),
|
expireTimer: DurationInSeconds.fromHours(2),
|
||||||
title: '(202) 555-0000',
|
title: '(202) 555-0000',
|
||||||
type: 'fromOther',
|
type: 'fromOther',
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
import { EmojiPicker } from '../emoji/EmojiPicker';
|
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||||
import { setupI18n } from '../../util/setupI18n';
|
import { setupI18n } from '../../util/setupI18n';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import type { PropsType as TimelineItemProps } from './TimelineItem';
|
import type { PropsType as TimelineItemProps } from './TimelineItem';
|
||||||
import { TimelineItem } from './TimelineItem';
|
import { TimelineItem } from './TimelineItem';
|
||||||
|
@ -43,7 +44,10 @@ const renderContact = (conversationId: string) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderUniversalTimerNotification = () => (
|
const renderUniversalTimerNotification = () => (
|
||||||
<UniversalTimerNotification i18n={i18n} expireTimer={3600} />
|
<UniversalTimerNotification
|
||||||
|
i18n={i18n}
|
||||||
|
expireTimer={DurationInSeconds.HOUR}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const getDefaultProps = () => ({
|
const getDefaultProps = () => ({
|
||||||
|
@ -138,7 +142,7 @@ export const Notification = (): JSX.Element => {
|
||||||
type: 'timerNotification',
|
type: 'timerNotification',
|
||||||
data: {
|
data: {
|
||||||
phoneNumber: '(202) 555-0000',
|
phoneNumber: '(202) 555-0000',
|
||||||
expireTimer: 60,
|
expireTimer: DurationInSeconds.MINUTE,
|
||||||
...getDefaultConversation(),
|
...getDefaultConversation(),
|
||||||
type: 'fromOther',
|
type: 'fromOther',
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
// 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 { boolean, number, select, text } from '@storybook/addon-knobs';
|
import { boolean, number, select, text } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
import { setupI18n } from '../../util/setupI18n';
|
import { setupI18n } from '../../util/setupI18n';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import type { Props } from './TimerNotification';
|
import type { Props } from './TimerNotification';
|
||||||
import { TimerNotification } from './TimerNotification';
|
import { TimerNotification } from './TimerNotification';
|
||||||
|
@ -34,16 +34,19 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
expireTimer: number(
|
expireTimer: DurationInSeconds.fromMillis(
|
||||||
'expireTimer',
|
number(
|
||||||
('expireTimer' in overrideProps ? overrideProps.expireTimer : 0) || 0
|
'expireTimer',
|
||||||
|
('expireTimer' in overrideProps ? overrideProps.expireTimer : 0) ||
|
||||||
|
0
|
||||||
|
)
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SetByOther = (): JSX.Element => {
|
export const SetByOther = (): JSX.Element => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
expireTimer: moment.duration(1, 'hour').asSeconds(),
|
expireTimer: DurationInSeconds.fromHours(1),
|
||||||
type: 'fromOther',
|
type: 'fromOther',
|
||||||
title: 'Mr. Fire',
|
title: 'Mr. Fire',
|
||||||
});
|
});
|
||||||
|
@ -61,7 +64,7 @@ export const SetByOtherWithALongName = (): JSX.Element => {
|
||||||
const longName = '🦴🧩📴'.repeat(50);
|
const longName = '🦴🧩📴'.repeat(50);
|
||||||
|
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
expireTimer: moment.duration(1, 'hour').asSeconds(),
|
expireTimer: DurationInSeconds.fromHours(1),
|
||||||
type: 'fromOther',
|
type: 'fromOther',
|
||||||
title: longName,
|
title: longName,
|
||||||
});
|
});
|
||||||
|
@ -81,7 +84,7 @@ SetByOtherWithALongName.story = {
|
||||||
|
|
||||||
export const SetByYou = (): JSX.Element => {
|
export const SetByYou = (): JSX.Element => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
expireTimer: moment.duration(1, 'hour').asSeconds(),
|
expireTimer: DurationInSeconds.fromHours(1),
|
||||||
type: 'fromMe',
|
type: 'fromMe',
|
||||||
title: 'Mr. Fire',
|
title: 'Mr. Fire',
|
||||||
});
|
});
|
||||||
|
@ -97,7 +100,7 @@ export const SetByYou = (): JSX.Element => {
|
||||||
|
|
||||||
export const SetBySync = (): JSX.Element => {
|
export const SetBySync = (): JSX.Element => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
expireTimer: moment.duration(1, 'hour').asSeconds(),
|
expireTimer: DurationInSeconds.fromHours(1),
|
||||||
type: 'fromSync',
|
type: 'fromSync',
|
||||||
title: 'Mr. Fire',
|
title: 'Mr. Fire',
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { SystemMessage } from './SystemMessage';
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import * as expirationTimer from '../../util/expirationTimer';
|
import * as expirationTimer from '../../util/expirationTimer';
|
||||||
|
import type { DurationInSeconds } from '../../util/durations';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
|
||||||
export type TimerNotificationType =
|
export type TimerNotificationType =
|
||||||
|
@ -27,7 +28,7 @@ export type PropsData = {
|
||||||
| { disabled: true }
|
| { disabled: true }
|
||||||
| {
|
| {
|
||||||
disabled: false;
|
disabled: false;
|
||||||
expireTimer: number;
|
expireTimer: DurationInSeconds;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -18,34 +18,34 @@ const i18n = setupI18n('en', enMessages);
|
||||||
export const Seconds = (): JSX.Element => (
|
export const Seconds = (): JSX.Element => (
|
||||||
<UniversalTimerNotification
|
<UniversalTimerNotification
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
expireTimer={EXPIRE_TIMERS[0].value / 1000}
|
expireTimer={EXPIRE_TIMERS[0].value}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Minutes = (): JSX.Element => (
|
export const Minutes = (): JSX.Element => (
|
||||||
<UniversalTimerNotification
|
<UniversalTimerNotification
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
expireTimer={EXPIRE_TIMERS[1].value / 1000}
|
expireTimer={EXPIRE_TIMERS[1].value}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Hours = (): JSX.Element => (
|
export const Hours = (): JSX.Element => (
|
||||||
<UniversalTimerNotification
|
<UniversalTimerNotification
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
expireTimer={EXPIRE_TIMERS[2].value / 1000}
|
expireTimer={EXPIRE_TIMERS[2].value}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Days = (): JSX.Element => (
|
export const Days = (): JSX.Element => (
|
||||||
<UniversalTimerNotification
|
<UniversalTimerNotification
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
expireTimer={EXPIRE_TIMERS[3].value / 1000}
|
expireTimer={EXPIRE_TIMERS[3].value}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Weeks = (): JSX.Element => (
|
export const Weeks = (): JSX.Element => (
|
||||||
<UniversalTimerNotification
|
<UniversalTimerNotification
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
expireTimer={EXPIRE_TIMERS[4].value / 1000}
|
expireTimer={EXPIRE_TIMERS[4].value}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,10 +6,11 @@ import React from 'react';
|
||||||
import { SystemMessage } from './SystemMessage';
|
import { SystemMessage } from './SystemMessage';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import * as expirationTimer from '../../util/expirationTimer';
|
import * as expirationTimer from '../../util/expirationTimer';
|
||||||
|
import type { DurationInSeconds } from '../../util/durations';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
expireTimer: number;
|
expireTimer: DurationInSeconds;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UniversalTimerNotification: React.FC<Props> = props => {
|
export const UniversalTimerNotification: React.FC<Props> = props => {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import type { ConversationType } from '../../../state/ducks/conversations';
|
||||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||||
import { makeFakeLookupConversationWithoutUuid } from '../../../test-both/helpers/fakeLookupConversationWithoutUuid';
|
import { makeFakeLookupConversationWithoutUuid } from '../../../test-both/helpers/fakeLookupConversationWithoutUuid';
|
||||||
import { ThemeType } from '../../../types/Util';
|
import { ThemeType } from '../../../types/Util';
|
||||||
|
import { DurationInSeconds } from '../../../util/durations';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -35,7 +36,10 @@ const conversation: ConversationType = getDefaultConversation({
|
||||||
|
|
||||||
const allCandidateContacts = times(10, () => getDefaultConversation());
|
const allCandidateContacts = times(10, () => getDefaultConversation());
|
||||||
|
|
||||||
const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
const createProps = (
|
||||||
|
hasGroupLink = false,
|
||||||
|
expireTimer?: DurationInSeconds
|
||||||
|
): Props => ({
|
||||||
addMembers: async () => {
|
addMembers: async () => {
|
||||||
action('addMembers');
|
action('addMembers');
|
||||||
},
|
},
|
||||||
|
@ -194,7 +198,7 @@ export const GroupEditable = (): JSX.Element => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GroupEditableWithCustomDisappearingTimeout = (): JSX.Element => {
|
export const GroupEditableWithCustomDisappearingTimeout = (): JSX.Element => {
|
||||||
const props = createProps(false, 3 * 24 * 60 * 60);
|
const props = createProps(false, DurationInSeconds.fromDays(3));
|
||||||
|
|
||||||
return <ConversationDetails {...props} canEditGroupInfo />;
|
return <ConversationDetails {...props} canEditGroupInfo />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,7 @@ import type { LocalizerType, ThemeType } from '../../../types/Util';
|
||||||
import type { MediaItemType } from '../../../types/MediaItem';
|
import type { MediaItemType } from '../../../types/MediaItem';
|
||||||
import type { BadgeType } from '../../../badges/types';
|
import type { BadgeType } from '../../../badges/types';
|
||||||
import { missingCaseError } from '../../../util/missingCaseError';
|
import { missingCaseError } from '../../../util/missingCaseError';
|
||||||
|
import { DurationInSeconds } from '../../../util/durations';
|
||||||
|
|
||||||
import { DisappearingTimerSelect } from '../../DisappearingTimerSelect';
|
import { DisappearingTimerSelect } from '../../DisappearingTimerSelect';
|
||||||
|
|
||||||
|
@ -79,7 +80,7 @@ export type StateProps = {
|
||||||
memberships: Array<GroupV2Membership>;
|
memberships: Array<GroupV2Membership>;
|
||||||
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
||||||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||||
setDisappearingMessages: (seconds: number) => void;
|
setDisappearingMessages: (seconds: DurationInSeconds) => void;
|
||||||
showAllMedia: () => void;
|
showAllMedia: () => void;
|
||||||
showChatColorEditor: () => void;
|
showChatColorEditor: () => void;
|
||||||
showGroupLinkManagement: () => void;
|
showGroupLinkManagement: () => void;
|
||||||
|
@ -410,7 +411,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
right={
|
right={
|
||||||
<DisappearingTimerSelect
|
<DisappearingTimerSelect
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
value={conversation.expireTimer || 0}
|
value={conversation.expireTimer || DurationInSeconds.ZERO}
|
||||||
onChange={setDisappearingMessages}
|
onChange={setDisappearingMessages}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
||||||
ReplaceAvatarActionType,
|
ReplaceAvatarActionType,
|
||||||
SaveAvatarToDiskActionType,
|
SaveAvatarToDiskActionType,
|
||||||
} from '../../types/Avatar';
|
} from '../../types/Avatar';
|
||||||
|
import type { DurationInSeconds } from '../../util/durations';
|
||||||
import type { ShowConversationType } from '../../state/ducks/conversations';
|
import type { ShowConversationType } from '../../state/ducks/conversations';
|
||||||
|
|
||||||
export enum FindDirection {
|
export enum FindDirection {
|
||||||
|
@ -73,7 +74,7 @@ export abstract class LeftPaneHelper<T> {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
removeSelectedContact: (_: string) => unknown;
|
removeSelectedContact: (_: string) => unknown;
|
||||||
setComposeGroupAvatar: (_: undefined | Uint8Array) => unknown;
|
setComposeGroupAvatar: (_: undefined | Uint8Array) => unknown;
|
||||||
setComposeGroupExpireTimer: (_: number) => void;
|
setComposeGroupExpireTimer: (_: DurationInSeconds) => void;
|
||||||
setComposeGroupName: (_: string) => unknown;
|
setComposeGroupName: (_: string) => unknown;
|
||||||
toggleComposeEditingAvatar: () => unknown;
|
toggleComposeEditingAvatar: () => unknown;
|
||||||
}>
|
}>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { RowType } from '../ConversationList';
|
||||||
import type { ContactListItemConversationType } from '../conversationList/ContactListItem';
|
import type { ContactListItemConversationType } from '../conversationList/ContactListItem';
|
||||||
import { DisappearingTimerSelect } from '../DisappearingTimerSelect';
|
import { DisappearingTimerSelect } from '../DisappearingTimerSelect';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
|
import type { DurationInSeconds } from '../../util/durations';
|
||||||
import { Alert } from '../Alert';
|
import { Alert } from '../Alert';
|
||||||
import { AvatarEditor } from '../AvatarEditor';
|
import { AvatarEditor } from '../AvatarEditor';
|
||||||
import { AvatarPreview } from '../AvatarPreview';
|
import { AvatarPreview } from '../AvatarPreview';
|
||||||
|
@ -28,7 +29,7 @@ import { AvatarColors } from '../../types/Colors';
|
||||||
export type LeftPaneSetGroupMetadataPropsType = {
|
export type LeftPaneSetGroupMetadataPropsType = {
|
||||||
groupAvatar: undefined | Uint8Array;
|
groupAvatar: undefined | Uint8Array;
|
||||||
groupName: string;
|
groupName: string;
|
||||||
groupExpireTimer: number;
|
groupExpireTimer: DurationInSeconds;
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
isCreating: boolean;
|
isCreating: boolean;
|
||||||
isEditingAvatar: boolean;
|
isEditingAvatar: boolean;
|
||||||
|
@ -41,7 +42,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
|
||||||
|
|
||||||
private readonly groupName: string;
|
private readonly groupName: string;
|
||||||
|
|
||||||
private readonly groupExpireTimer: number;
|
private readonly groupExpireTimer: DurationInSeconds;
|
||||||
|
|
||||||
private readonly hasError: boolean;
|
private readonly hasError: boolean;
|
||||||
|
|
||||||
|
@ -128,7 +129,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
|
||||||
createGroup: () => unknown;
|
createGroup: () => unknown;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
setComposeGroupAvatar: (_: undefined | Uint8Array) => unknown;
|
setComposeGroupAvatar: (_: undefined | Uint8Array) => unknown;
|
||||||
setComposeGroupExpireTimer: (_: number) => void;
|
setComposeGroupExpireTimer: (_: DurationInSeconds) => void;
|
||||||
setComposeGroupName: (_: string) => unknown;
|
setComposeGroupName: (_: string) => unknown;
|
||||||
toggleComposeEditingAvatar: () => unknown;
|
toggleComposeEditingAvatar: () => unknown;
|
||||||
}>): ReactChild {
|
}>): ReactChild {
|
||||||
|
|
24
ts/groups.ts
24
ts/groups.ts
|
@ -23,7 +23,7 @@ import dataInterface from './sql/Client';
|
||||||
import { toWebSafeBase64, fromWebSafeBase64 } from './util/webSafeBase64';
|
import { toWebSafeBase64, fromWebSafeBase64 } from './util/webSafeBase64';
|
||||||
import { assertDev, strictAssert } from './util/assert';
|
import { assertDev, strictAssert } from './util/assert';
|
||||||
import { isMoreRecentThan } from './util/timestamp';
|
import { isMoreRecentThan } from './util/timestamp';
|
||||||
import * as durations from './util/durations';
|
import { MINUTE, DurationInSeconds } from './util/durations';
|
||||||
import { normalizeUuid } from './util/normalizeUuid';
|
import { normalizeUuid } from './util/normalizeUuid';
|
||||||
import { dropNull } from './util/dropNull';
|
import { dropNull } from './util/dropNull';
|
||||||
import type {
|
import type {
|
||||||
|
@ -854,7 +854,7 @@ export function buildDisappearingMessagesTimerChange({
|
||||||
expireTimer,
|
expireTimer,
|
||||||
group,
|
group,
|
||||||
}: {
|
}: {
|
||||||
expireTimer: number;
|
expireTimer: DurationInSeconds;
|
||||||
group: ConversationAttributesType;
|
group: ConversationAttributesType;
|
||||||
}): Proto.GroupChange.Actions {
|
}): Proto.GroupChange.Actions {
|
||||||
const actions = new Proto.GroupChange.Actions();
|
const actions = new Proto.GroupChange.Actions();
|
||||||
|
@ -1458,7 +1458,7 @@ export async function modifyGroupV2({
|
||||||
}
|
}
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const timeoutTime = startTime + durations.MINUTE;
|
const timeoutTime = startTime + MINUTE;
|
||||||
|
|
||||||
const MAX_ATTEMPTS = 5;
|
const MAX_ATTEMPTS = 5;
|
||||||
|
|
||||||
|
@ -1725,7 +1725,7 @@ export async function createGroupV2(
|
||||||
options: Readonly<{
|
options: Readonly<{
|
||||||
name: string;
|
name: string;
|
||||||
avatar: undefined | Uint8Array;
|
avatar: undefined | Uint8Array;
|
||||||
expireTimer: undefined | number;
|
expireTimer: undefined | DurationInSeconds;
|
||||||
conversationIds: Array<string>;
|
conversationIds: Array<string>;
|
||||||
avatars?: Array<AvatarDataType>;
|
avatars?: Array<AvatarDataType>;
|
||||||
refreshedCredentials?: boolean;
|
refreshedCredentials?: boolean;
|
||||||
|
@ -2904,7 +2904,7 @@ type MaybeUpdatePropsType = Readonly<{
|
||||||
groupChange?: WrappedGroupChangeType;
|
groupChange?: WrappedGroupChangeType;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const FIVE_MINUTES = 5 * durations.MINUTE;
|
const FIVE_MINUTES = 5 * MINUTE;
|
||||||
|
|
||||||
export async function waitThenMaybeUpdateGroup(
|
export async function waitThenMaybeUpdateGroup(
|
||||||
options: MaybeUpdatePropsType,
|
options: MaybeUpdatePropsType,
|
||||||
|
@ -4625,7 +4625,7 @@ function extractDiffs({
|
||||||
Boolean(current.expireTimer) &&
|
Boolean(current.expireTimer) &&
|
||||||
old.expireTimer !== current.expireTimer)
|
old.expireTimer !== current.expireTimer)
|
||||||
) {
|
) {
|
||||||
const expireTimer = current.expireTimer || 0;
|
const expireTimer = current.expireTimer || DurationInSeconds.ZERO;
|
||||||
log.info(
|
log.info(
|
||||||
`extractDiffs/${logId}: generating change notifcation for new ${expireTimer} timer`
|
`extractDiffs/${logId}: generating change notifcation for new ${expireTimer} timer`
|
||||||
);
|
);
|
||||||
|
@ -4977,9 +4977,9 @@ async function applyGroupChange({
|
||||||
disappearingMessagesTimer &&
|
disappearingMessagesTimer &&
|
||||||
disappearingMessagesTimer.content === 'disappearingMessagesDuration'
|
disappearingMessagesTimer.content === 'disappearingMessagesDuration'
|
||||||
) {
|
) {
|
||||||
result.expireTimer = dropNull(
|
const duration = disappearingMessagesTimer.disappearingMessagesDuration;
|
||||||
disappearingMessagesTimer.disappearingMessagesDuration
|
result.expireTimer =
|
||||||
);
|
duration == null ? undefined : DurationInSeconds.fromSeconds(duration);
|
||||||
} else {
|
} else {
|
||||||
log.warn(
|
log.warn(
|
||||||
`applyGroupChange/${logId}: Clearing group expireTimer due to missing data.`
|
`applyGroupChange/${logId}: Clearing group expireTimer due to missing data.`
|
||||||
|
@ -5335,9 +5335,9 @@ async function applyGroupState({
|
||||||
disappearingMessagesTimer &&
|
disappearingMessagesTimer &&
|
||||||
disappearingMessagesTimer.content === 'disappearingMessagesDuration'
|
disappearingMessagesTimer.content === 'disappearingMessagesDuration'
|
||||||
) {
|
) {
|
||||||
result.expireTimer = dropNull(
|
const duration = disappearingMessagesTimer.disappearingMessagesDuration;
|
||||||
disappearingMessagesTimer.disappearingMessagesDuration
|
result.expireTimer =
|
||||||
);
|
duration == null ? undefined : DurationInSeconds.fromSeconds(duration);
|
||||||
} else {
|
} else {
|
||||||
result.expireTimer = undefined;
|
result.expireTimer = undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import type {
|
||||||
import { handleMessageSend } from '../../util/handleMessageSend';
|
import { handleMessageSend } from '../../util/handleMessageSend';
|
||||||
import { isConversationAccepted } from '../../util/isConversationAccepted';
|
import { isConversationAccepted } from '../../util/isConversationAccepted';
|
||||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
|
|
||||||
export async function sendDirectExpirationTimerUpdate(
|
export async function sendDirectExpirationTimerUpdate(
|
||||||
conversation: ConversationModel,
|
conversation: ConversationModel,
|
||||||
|
@ -77,7 +78,11 @@ export async function sendDirectExpirationTimerUpdate(
|
||||||
const sendType = 'expirationTimerUpdate';
|
const sendType = 'expirationTimerUpdate';
|
||||||
const flags = Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
const flags = Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||||
const proto = await messaging.getContentMessage({
|
const proto = await messaging.getContentMessage({
|
||||||
expireTimer,
|
// `expireTimer` is already in seconds
|
||||||
|
expireTimer:
|
||||||
|
expireTimer === undefined
|
||||||
|
? undefined
|
||||||
|
: DurationInSeconds.fromSeconds(expireTimer),
|
||||||
flags,
|
flags,
|
||||||
profileKey,
|
profileKey,
|
||||||
recipients: conversation.getRecipients(),
|
recipients: conversation.getRecipients(),
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { ourProfileKeyService } from '../../services/ourProfileKey';
|
||||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
||||||
import { isConversationAccepted } from '../../util/isConversationAccepted';
|
import { isConversationAccepted } from '../../util/isConversationAccepted';
|
||||||
import { sendToGroup } from '../../util/sendToGroup';
|
import { sendToGroup } from '../../util/sendToGroup';
|
||||||
|
import type { DurationInSeconds } from '../../util/durations';
|
||||||
import type { UUIDStringType } from '../../types/UUID';
|
import type { UUIDStringType } from '../../types/UUID';
|
||||||
|
|
||||||
export async function sendNormalMessage(
|
export async function sendNormalMessage(
|
||||||
|
@ -466,7 +467,7 @@ async function getMessageSendData({
|
||||||
body: undefined | string;
|
body: undefined | string;
|
||||||
contact?: Array<ContactWithHydratedAvatar>;
|
contact?: Array<ContactWithHydratedAvatar>;
|
||||||
deletedForEveryoneTimestamp: undefined | number;
|
deletedForEveryoneTimestamp: undefined | number;
|
||||||
expireTimer: undefined | number;
|
expireTimer: undefined | DurationInSeconds;
|
||||||
mentions: undefined | BodyRangesType;
|
mentions: undefined | BodyRangesType;
|
||||||
messageTimestamp: number;
|
messageTimestamp: number;
|
||||||
preview: Array<LinkPreviewType>;
|
preview: Array<LinkPreviewType>;
|
||||||
|
|
7
ts/model-types.d.ts
vendored
7
ts/model-types.d.ts
vendored
|
@ -32,6 +32,7 @@ import type { LinkPreviewType } from './types/message/LinkPreviews';
|
||||||
import type { StickerType } from './types/Stickers';
|
import type { StickerType } from './types/Stickers';
|
||||||
import type { StorySendMode } from './types/Stories';
|
import type { StorySendMode } from './types/Stories';
|
||||||
import type { MIMEType } from './types/MIME';
|
import type { MIMEType } from './types/MIME';
|
||||||
|
import type { DurationInSeconds } from './util/durations';
|
||||||
|
|
||||||
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
||||||
import MemberRoleEnum = Proto.Member.Role;
|
import MemberRoleEnum = Proto.Member.Role;
|
||||||
|
@ -130,7 +131,7 @@ export type MessageAttributesType = {
|
||||||
deletedForEveryoneTimestamp?: number;
|
deletedForEveryoneTimestamp?: number;
|
||||||
errors?: Array<CustomError>;
|
errors?: Array<CustomError>;
|
||||||
expirationStartTimestamp?: number | null;
|
expirationStartTimestamp?: number | null;
|
||||||
expireTimer?: number;
|
expireTimer?: DurationInSeconds;
|
||||||
groupMigration?: GroupMigrationType;
|
groupMigration?: GroupMigrationType;
|
||||||
group_update?: GroupV1Update;
|
group_update?: GroupV1Update;
|
||||||
hasAttachments?: boolean | 0 | 1;
|
hasAttachments?: boolean | 0 | 1;
|
||||||
|
@ -198,7 +199,7 @@ export type MessageAttributesType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
expirationTimerUpdate?: {
|
expirationTimerUpdate?: {
|
||||||
expireTimer: number;
|
expireTimer?: DurationInSeconds;
|
||||||
fromSync?: unknown;
|
fromSync?: unknown;
|
||||||
source?: string;
|
source?: string;
|
||||||
sourceUuid?: string;
|
sourceUuid?: string;
|
||||||
|
@ -381,7 +382,7 @@ export type ConversationAttributesType = {
|
||||||
} | null;
|
} | null;
|
||||||
avatars?: Array<AvatarDataType>;
|
avatars?: Array<AvatarDataType>;
|
||||||
description?: string;
|
description?: string;
|
||||||
expireTimer?: number;
|
expireTimer?: DurationInSeconds;
|
||||||
membersV2?: Array<GroupV2MemberType>;
|
membersV2?: Array<GroupV2MemberType>;
|
||||||
pendingMembersV2?: Array<GroupV2PendingMemberType>;
|
pendingMembersV2?: Array<GroupV2PendingMemberType>;
|
||||||
pendingAdminApprovalV2?: Array<GroupV2PendingAdminApprovalType>;
|
pendingAdminApprovalV2?: Array<GroupV2PendingAdminApprovalType>;
|
||||||
|
|
|
@ -80,7 +80,7 @@ import { updateConversationsWithUuidLookup } from '../updateConversationsWithUui
|
||||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
import { SendStatus } from '../messages/MessageSendState';
|
import { SendStatus } from '../messages/MessageSendState';
|
||||||
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||||
import * as durations from '../util/durations';
|
import { MINUTE, DurationInSeconds } from '../util/durations';
|
||||||
import {
|
import {
|
||||||
concat,
|
concat,
|
||||||
filter,
|
filter,
|
||||||
|
@ -155,7 +155,7 @@ const {
|
||||||
getNewerMessagesByConversation,
|
getNewerMessagesByConversation,
|
||||||
} = window.Signal.Data;
|
} = window.Signal.Data;
|
||||||
|
|
||||||
const FIVE_MINUTES = durations.MINUTE * 5;
|
const FIVE_MINUTES = MINUTE * 5;
|
||||||
|
|
||||||
const JOB_REPORTING_THRESHOLD_MS = 25;
|
const JOB_REPORTING_THRESHOLD_MS = 25;
|
||||||
const SEND_REPORTING_THRESHOLD_MS = 25;
|
const SEND_REPORTING_THRESHOLD_MS = 25;
|
||||||
|
@ -471,7 +471,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateExpirationTimerInGroupV2(
|
async updateExpirationTimerInGroupV2(
|
||||||
seconds?: number
|
seconds?: DurationInSeconds
|
||||||
): Promise<Proto.GroupChange.Actions | undefined> {
|
): Promise<Proto.GroupChange.Actions | undefined> {
|
||||||
const idLog = this.idForLogging();
|
const idLog = this.idForLogging();
|
||||||
const current = this.get('expireTimer');
|
const current = this.get('expireTimer');
|
||||||
|
@ -485,7 +485,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.Signal.Groups.buildDisappearingMessagesTimerChange({
|
return window.Signal.Groups.buildDisappearingMessagesTimerChange({
|
||||||
expireTimer: seconds || 0,
|
expireTimer: seconds || DurationInSeconds.ZERO,
|
||||||
group: this.attributes,
|
group: this.attributes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1382,7 +1382,7 @@ export class ConversationModel extends window.Backbone
|
||||||
if (!this.newMessageQueue) {
|
if (!this.newMessageQueue) {
|
||||||
this.newMessageQueue = new PQueue({
|
this.newMessageQueue = new PQueue({
|
||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
timeout: durations.MINUTE * 30,
|
timeout: MINUTE * 30,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3944,7 +3944,7 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
|
|
||||||
let expirationStartTimestamp: number | undefined;
|
let expirationStartTimestamp: number | undefined;
|
||||||
let expireTimer: number | undefined;
|
let expireTimer: DurationInSeconds | undefined;
|
||||||
|
|
||||||
// If it's a group story reply then let's match the expiration timers
|
// If it's a group story reply then let's match the expiration timers
|
||||||
// with the parent story's expiration.
|
// with the parent story's expiration.
|
||||||
|
@ -3952,7 +3952,7 @@ export class ConversationModel extends window.Backbone
|
||||||
const parentStory = await getMessageById(storyId);
|
const parentStory = await getMessageById(storyId);
|
||||||
expirationStartTimestamp =
|
expirationStartTimestamp =
|
||||||
parentStory?.expirationStartTimestamp || Date.now();
|
parentStory?.expirationStartTimestamp || Date.now();
|
||||||
expireTimer = parentStory?.expireTimer || durations.DAY;
|
expireTimer = parentStory?.expireTimer || DurationInSeconds.DAY;
|
||||||
} else {
|
} else {
|
||||||
await this.maybeApplyUniversalTimer();
|
await this.maybeApplyUniversalTimer();
|
||||||
expireTimer = this.get('expireTimer');
|
expireTimer = this.get('expireTimer');
|
||||||
|
@ -4431,7 +4431,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateExpirationTimer(
|
async updateExpirationTimer(
|
||||||
providedExpireTimer: number | undefined,
|
providedExpireTimer: DurationInSeconds | undefined,
|
||||||
{
|
{
|
||||||
reason,
|
reason,
|
||||||
receivedAt,
|
receivedAt,
|
||||||
|
@ -4479,7 +4479,7 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let expireTimer: number | undefined = providedExpireTimer;
|
let expireTimer: DurationInSeconds | undefined = providedExpireTimer;
|
||||||
let source = providedSource;
|
let source = providedSource;
|
||||||
if (this.get('left')) {
|
if (this.get('left')) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -172,7 +172,7 @@ import { parseBoostBadgeListFromServer } from '../badges/parseBadgesFromServer';
|
||||||
import { GiftBadgeStates } from '../components/conversation/Message';
|
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||||
import { downloadAttachment } from '../util/downloadAttachment';
|
import { downloadAttachment } from '../util/downloadAttachment';
|
||||||
import type { StickerWithHydratedData } from '../types/Stickers';
|
import type { StickerWithHydratedData } from '../types/Stickers';
|
||||||
import { SECOND } from '../util/durations';
|
import { DurationInSeconds } from '../util/durations';
|
||||||
import dataInterface from '../sql/Client';
|
import dataInterface from '../sql/Client';
|
||||||
|
|
||||||
function isSameUuid(
|
function isSameUuid(
|
||||||
|
@ -566,7 +566,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
const expireTimer = this.get('expireTimer');
|
const expireTimer = this.get('expireTimer');
|
||||||
const expirationStartTimestamp = this.get('expirationStartTimestamp');
|
const expirationStartTimestamp = this.get('expirationStartTimestamp');
|
||||||
const expirationLength = isNumber(expireTimer)
|
const expirationLength = isNumber(expireTimer)
|
||||||
? expireTimer * SECOND
|
? DurationInSeconds.toMillis(expireTimer)
|
||||||
: undefined;
|
: undefined;
|
||||||
const expirationTimestamp = expirationTimer.calculateExpirationTimestamp({
|
const expirationTimestamp = expirationTimer.calculateExpirationTimestamp({
|
||||||
expireTimer,
|
expireTimer,
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
} from '../util/universalExpireTimer';
|
} from '../util/universalExpireTimer';
|
||||||
import { ourProfileKeyService } from './ourProfileKey';
|
import { ourProfileKeyService } from './ourProfileKey';
|
||||||
import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation';
|
import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation';
|
||||||
|
import { DurationInSeconds } from '../util/durations';
|
||||||
import { isValidUuid, UUID, UUIDKind } from '../types/UUID';
|
import { isValidUuid, UUID, UUIDKind } from '../types/UUID';
|
||||||
import * as preferredReactionEmoji from '../reactions/preferredReactionEmoji';
|
import * as preferredReactionEmoji from '../reactions/preferredReactionEmoji';
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
@ -1178,7 +1179,9 @@ export async function mergeAccountRecord(
|
||||||
window.storage.put('preferredReactionEmoji', rawPreferredReactionEmoji);
|
window.storage.put('preferredReactionEmoji', rawPreferredReactionEmoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUniversalExpireTimer(universalExpireTimer || 0);
|
setUniversalExpireTimer(
|
||||||
|
DurationInSeconds.fromSeconds(universalExpireTimer || 0)
|
||||||
|
);
|
||||||
|
|
||||||
const PHONE_NUMBER_SHARING_MODE_ENUM =
|
const PHONE_NUMBER_SHARING_MODE_ENUM =
|
||||||
Proto.AccountRecord.PhoneNumberSharingMode;
|
Proto.AccountRecord.PhoneNumberSharingMode;
|
||||||
|
|
|
@ -15,6 +15,7 @@ import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
import { isNotNil } from '../util/isNotNil';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
import { dropNull } from '../util/dropNull';
|
import { dropNull } from '../util/dropNull';
|
||||||
|
import { DurationInSeconds } from '../util/durations';
|
||||||
import { isGroup } from '../util/whatTypeOfConversation';
|
import { isGroup } from '../util/whatTypeOfConversation';
|
||||||
import { SIGNAL_ACI } from '../types/SignalConversation';
|
import { SIGNAL_ACI } from '../types/SignalConversation';
|
||||||
|
|
||||||
|
@ -142,7 +143,7 @@ export function getStoriesForRedux(): Array<StoryDataType> {
|
||||||
async function repairUnexpiredStories(): Promise<void> {
|
async function repairUnexpiredStories(): Promise<void> {
|
||||||
strictAssert(storyData, 'Could not load stories');
|
strictAssert(storyData, 'Could not load stories');
|
||||||
|
|
||||||
const DAY_AS_SECONDS = durations.DAY / 1000;
|
const DAY_AS_SECONDS = DurationInSeconds.fromDays(1);
|
||||||
|
|
||||||
const storiesWithExpiry = storyData
|
const storiesWithExpiry = storyData
|
||||||
.filter(
|
.filter(
|
||||||
|
@ -155,9 +156,11 @@ async function repairUnexpiredStories(): Promise<void> {
|
||||||
.map(story => ({
|
.map(story => ({
|
||||||
...story,
|
...story,
|
||||||
expirationStartTimestamp: Math.min(story.timestamp, Date.now()),
|
expirationStartTimestamp: Math.min(story.timestamp, Date.now()),
|
||||||
expireTimer: Math.min(
|
expireTimer: DurationInSeconds.fromMillis(
|
||||||
Math.floor((story.timestamp + durations.DAY - Date.now()) / 1000),
|
Math.min(
|
||||||
DAY_AS_SECONDS
|
Math.floor(story.timestamp + durations.DAY - Date.now()),
|
||||||
|
durations.DAY
|
||||||
|
)
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import * as log from '../../logging/log';
|
||||||
import { calling } from '../../services/calling';
|
import { calling } from '../../services/calling';
|
||||||
import { getOwn } from '../../util/getOwn';
|
import { getOwn } from '../../util/getOwn';
|
||||||
import { assertDev, strictAssert } from '../../util/assert';
|
import { assertDev, strictAssert } from '../../util/assert';
|
||||||
|
import type { DurationInSeconds } from '../../util/durations';
|
||||||
import * as universalExpireTimer from '../../util/universalExpireTimer';
|
import * as universalExpireTimer from '../../util/universalExpireTimer';
|
||||||
import type {
|
import type {
|
||||||
ShowSendAnywayDialogActionType,
|
ShowSendAnywayDialogActionType,
|
||||||
|
@ -178,7 +179,7 @@ export type ConversationType = {
|
||||||
accessControlMembers?: number;
|
accessControlMembers?: number;
|
||||||
announcementsOnly?: boolean;
|
announcementsOnly?: boolean;
|
||||||
announcementsOnlyReady?: boolean;
|
announcementsOnlyReady?: boolean;
|
||||||
expireTimer?: number;
|
expireTimer?: DurationInSeconds;
|
||||||
memberships?: Array<{
|
memberships?: Array<{
|
||||||
uuid: UUIDStringType;
|
uuid: UUIDStringType;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
@ -293,7 +294,7 @@ export type PreJoinConversationType = {
|
||||||
type ComposerGroupCreationState = {
|
type ComposerGroupCreationState = {
|
||||||
groupAvatar: undefined | Uint8Array;
|
groupAvatar: undefined | Uint8Array;
|
||||||
groupName: string;
|
groupName: string;
|
||||||
groupExpireTimer: number;
|
groupExpireTimer: DurationInSeconds;
|
||||||
maximumGroupSizeModalState: OneTimeModalState;
|
maximumGroupSizeModalState: OneTimeModalState;
|
||||||
recommendedGroupSizeModalState: OneTimeModalState;
|
recommendedGroupSizeModalState: OneTimeModalState;
|
||||||
selectedConversationIds: Array<string>;
|
selectedConversationIds: Array<string>;
|
||||||
|
@ -712,7 +713,7 @@ type SetComposeGroupNameActionType = {
|
||||||
};
|
};
|
||||||
type SetComposeGroupExpireTimerActionType = {
|
type SetComposeGroupExpireTimerActionType = {
|
||||||
type: 'SET_COMPOSE_GROUP_EXPIRE_TIMER';
|
type: 'SET_COMPOSE_GROUP_EXPIRE_TIMER';
|
||||||
payload: { groupExpireTimer: number };
|
payload: { groupExpireTimer: DurationInSeconds };
|
||||||
};
|
};
|
||||||
type SetComposeSearchTermActionType = {
|
type SetComposeSearchTermActionType = {
|
||||||
type: 'SET_COMPOSE_SEARCH_TERM';
|
type: 'SET_COMPOSE_SEARCH_TERM';
|
||||||
|
@ -1907,7 +1908,7 @@ function setComposeGroupName(groupName: string): SetComposeGroupNameActionType {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setComposeGroupExpireTimer(
|
function setComposeGroupExpireTimer(
|
||||||
groupExpireTimer: number
|
groupExpireTimer: DurationInSeconds
|
||||||
): SetComposeGroupExpireTimerActionType {
|
): SetComposeGroupExpireTimerActionType {
|
||||||
return {
|
return {
|
||||||
type: 'SET_COMPOSE_GROUP_EXPIRE_TIMER',
|
type: 'SET_COMPOSE_GROUP_EXPIRE_TIMER',
|
||||||
|
@ -3509,7 +3510,7 @@ export function reducer(
|
||||||
let maximumGroupSizeModalState: OneTimeModalState;
|
let maximumGroupSizeModalState: OneTimeModalState;
|
||||||
let groupName: string;
|
let groupName: string;
|
||||||
let groupAvatar: undefined | Uint8Array;
|
let groupAvatar: undefined | Uint8Array;
|
||||||
let groupExpireTimer: number;
|
let groupExpireTimer: DurationInSeconds;
|
||||||
let userAvatarData = getDefaultAvatars(true);
|
let userAvatarData = getDefaultAvatars(true);
|
||||||
|
|
||||||
switch (state.composer?.step) {
|
switch (state.composer?.step) {
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { markViewed } from '../../services/MessageUpdater';
|
||||||
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
|
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
|
||||||
import { replaceIndex } from '../../util/replaceIndex';
|
import { replaceIndex } from '../../util/replaceIndex';
|
||||||
import { showToast } from '../../util/showToast';
|
import { showToast } from '../../util/showToast';
|
||||||
|
import type { DurationInSeconds } from '../../util/durations';
|
||||||
import { hasFailed, isDownloaded, isDownloading } from '../../types/Attachment';
|
import { hasFailed, isDownloaded, isDownloading } from '../../types/Attachment';
|
||||||
import {
|
import {
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
|
@ -79,7 +80,7 @@ export type StoryDataType = {
|
||||||
| 'type'
|
| 'type'
|
||||||
> & {
|
> & {
|
||||||
// don't want the fields to be optional as in MessageAttributesType
|
// don't want the fields to be optional as in MessageAttributesType
|
||||||
expireTimer: number | undefined;
|
expireTimer: DurationInSeconds | undefined;
|
||||||
expirationStartTimestamp: number | undefined;
|
expirationStartTimestamp: number | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ import type { UUIDStringType } from '../../types/UUID';
|
||||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||||
import { isSignalConnection } from '../../util/getSignalConnections';
|
import { isSignalConnection } from '../../util/getSignalConnections';
|
||||||
import { sortByTitle } from '../../util/sortByTitle';
|
import { sortByTitle } from '../../util/sortByTitle';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
import {
|
import {
|
||||||
isDirectConversation,
|
isDirectConversation,
|
||||||
isGroupV1,
|
isGroupV1,
|
||||||
|
@ -634,7 +635,7 @@ const getGroupCreationComposerState = createSelector(
|
||||||
): {
|
): {
|
||||||
groupName: string;
|
groupName: string;
|
||||||
groupAvatar: undefined | Uint8Array;
|
groupAvatar: undefined | Uint8Array;
|
||||||
groupExpireTimer: number;
|
groupExpireTimer: DurationInSeconds;
|
||||||
selectedConversationIds: Array<string>;
|
selectedConversationIds: Array<string>;
|
||||||
} => {
|
} => {
|
||||||
switch (composerState?.step) {
|
switch (composerState?.step) {
|
||||||
|
@ -649,7 +650,7 @@ const getGroupCreationComposerState = createSelector(
|
||||||
return {
|
return {
|
||||||
groupName: '',
|
groupName: '',
|
||||||
groupAvatar: undefined,
|
groupAvatar: undefined,
|
||||||
groupExpireTimer: 0,
|
groupExpireTimer: DurationInSeconds.ZERO,
|
||||||
selectedConversationIds: [],
|
selectedConversationIds: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -668,7 +669,7 @@ export const getComposeGroupName = createSelector(
|
||||||
|
|
||||||
export const getComposeGroupExpireTimer = createSelector(
|
export const getComposeGroupExpireTimer = createSelector(
|
||||||
getGroupCreationComposerState,
|
getGroupCreationComposerState,
|
||||||
(composerState): number => composerState.groupExpireTimer
|
(composerState): DurationInSeconds => composerState.groupExpireTimer
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getComposeSelectedContacts = createSelector(
|
export const getComposeSelectedContacts = createSelector(
|
||||||
|
|
|
@ -17,6 +17,7 @@ import type { UUIDStringType } from '../../types/UUID';
|
||||||
import { DEFAULT_CONVERSATION_COLOR } from '../../types/Colors';
|
import { DEFAULT_CONVERSATION_COLOR } from '../../types/Colors';
|
||||||
import { getPreferredReactionEmoji as getPreferredReactionEmojiFromStoredValue } from '../../reactions/preferredReactionEmoji';
|
import { getPreferredReactionEmoji as getPreferredReactionEmojiFromStoredValue } from '../../reactions/preferredReactionEmoji';
|
||||||
import { isBeta } from '../../util/version';
|
import { isBeta } from '../../util/version';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
import { getUserNumber, getUserACI } from './user';
|
import { getUserNumber, getUserACI } from './user';
|
||||||
|
|
||||||
const DEFAULT_PREFERRED_LEFT_PANE_WIDTH = 320;
|
const DEFAULT_PREFERRED_LEFT_PANE_WIDTH = 320;
|
||||||
|
@ -42,7 +43,8 @@ export const getPinnedConversationIds = createSelector(
|
||||||
|
|
||||||
export const getUniversalExpireTimer = createSelector(
|
export const getUniversalExpireTimer = createSelector(
|
||||||
getItems,
|
getItems,
|
||||||
(state: ItemsStateType): number => state[UNIVERSAL_EXPIRE_TIMER_ITEM] || 0
|
(state: ItemsStateType): DurationInSeconds =>
|
||||||
|
DurationInSeconds.fromSeconds(state[UNIVERSAL_EXPIRE_TIMER_ITEM] || 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
const isRemoteConfigFlagEnabled = (
|
const isRemoteConfigFlagEnabled = (
|
||||||
|
|
|
@ -93,7 +93,7 @@ import {
|
||||||
} from '../../messages/MessageSendState';
|
} from '../../messages/MessageSendState';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
|
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
|
||||||
import { DAY, HOUR, SECOND } from '../../util/durations';
|
import { DAY, HOUR, DurationInSeconds } from '../../util/durations';
|
||||||
import { getStoryReplyText } from '../../util/getStoryReplyText';
|
import { getStoryReplyText } from '../../util/getStoryReplyText';
|
||||||
import { isIncoming, isOutgoing, isStory } from '../../messages/helpers';
|
import { isIncoming, isOutgoing, isStory } from '../../messages/helpers';
|
||||||
import { calculateExpirationTimestamp } from '../../util/expirationTimer';
|
import { calculateExpirationTimestamp } from '../../util/expirationTimer';
|
||||||
|
@ -628,7 +628,9 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||||
}: GetPropsForMessageOptions
|
}: GetPropsForMessageOptions
|
||||||
): ShallowPropsType => {
|
): ShallowPropsType => {
|
||||||
const { expireTimer, expirationStartTimestamp, conversationId } = message;
|
const { expireTimer, expirationStartTimestamp, conversationId } = message;
|
||||||
const expirationLength = expireTimer ? expireTimer * SECOND : undefined;
|
const expirationLength = expireTimer
|
||||||
|
? DurationInSeconds.toMillis(expireTimer)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const conversation = getConversation(message, conversationSelector);
|
const conversation = getConversation(message, conversationSelector);
|
||||||
const isGroup = conversation.type === 'group';
|
const isGroup = conversation.type === 'group';
|
||||||
|
@ -1107,10 +1109,26 @@ function getPropsForTimerNotification(
|
||||||
const sourceId = sourceUuid || source;
|
const sourceId = sourceUuid || source;
|
||||||
const formattedContact = conversationSelector(sourceId);
|
const formattedContact = conversationSelector(sourceId);
|
||||||
|
|
||||||
|
// Pacify typescript
|
||||||
|
type MaybeExpireTimerType =
|
||||||
|
| { disabled: true }
|
||||||
|
| {
|
||||||
|
disabled: false;
|
||||||
|
expireTimer: DurationInSeconds;
|
||||||
|
};
|
||||||
|
|
||||||
|
const maybeExpireTimer: MaybeExpireTimerType = disabled
|
||||||
|
? {
|
||||||
|
disabled: true,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
disabled: false,
|
||||||
|
expireTimer,
|
||||||
|
};
|
||||||
|
|
||||||
const basicProps = {
|
const basicProps = {
|
||||||
...formattedContact,
|
...formattedContact,
|
||||||
disabled,
|
...maybeExpireTimer,
|
||||||
expireTimer,
|
|
||||||
type: 'fromOther' as const,
|
type: 'fromOther' as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
getPreferredBadgeSelector,
|
getPreferredBadgeSelector,
|
||||||
} from '../selectors/badges';
|
} from '../selectors/badges';
|
||||||
import { assertDev } from '../../util/assert';
|
import { assertDev } from '../../util/assert';
|
||||||
|
import type { DurationInSeconds } from '../../util/durations';
|
||||||
import { SignalService as Proto } from '../../protobuf';
|
import { SignalService as Proto } from '../../protobuf';
|
||||||
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
|
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
|
||||||
import type { SmartChooseGroupMembersModalPropsType } from './ChooseGroupMembersModal';
|
import type { SmartChooseGroupMembersModalPropsType } from './ChooseGroupMembersModal';
|
||||||
|
@ -39,7 +40,7 @@ export type SmartConversationDetailsProps = {
|
||||||
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
loadRecentMediaItems: (limit: number) => void;
|
loadRecentMediaItems: (limit: number) => void;
|
||||||
setDisappearingMessages: (seconds: number) => void;
|
setDisappearingMessages: (seconds: DurationInSeconds) => void;
|
||||||
showAllMedia: () => void;
|
showAllMedia: () => void;
|
||||||
showChatColorEditor: () => void;
|
showChatColorEditor: () => void;
|
||||||
showGroupLinkManagement: () => void;
|
showGroupLinkManagement: () => void;
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { mapDispatchToProps } from '../actions';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { strictAssert } from '../../util/assert';
|
import { strictAssert } from '../../util/assert';
|
||||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||||
|
import type { DurationInSeconds } from '../../util/durations';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -37,7 +38,7 @@ export type OwnProps = {
|
||||||
onOutgoingAudioCallInConversation: () => void;
|
onOutgoingAudioCallInConversation: () => void;
|
||||||
onOutgoingVideoCallInConversation: () => void;
|
onOutgoingVideoCallInConversation: () => void;
|
||||||
onSearchInConversation: () => void;
|
onSearchInConversation: () => void;
|
||||||
onSetDisappearingMessages: (seconds: number) => void;
|
onSetDisappearingMessages: (seconds: DurationInSeconds) => void;
|
||||||
onSetMuteNotifications: (seconds: number) => void;
|
onSetMuteNotifications: (seconds: number) => void;
|
||||||
onSetPin: (value: boolean) => void;
|
onSetPin: (value: boolean) => void;
|
||||||
onShowAllMedia: () => void;
|
onShowAllMedia: () => void;
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import { ComposerStep } from '../../state/ducks/conversationsEnums';
|
import { ComposerStep } from '../../state/ducks/conversationsEnums';
|
||||||
import { OneTimeModalState } from '../../groups/toggleSelectedContactForGroupAddition';
|
import { OneTimeModalState } from '../../groups/toggleSelectedContactForGroupAddition';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
|
|
||||||
export const defaultStartDirectConversationComposerState = {
|
export const defaultStartDirectConversationComposerState = {
|
||||||
step: ComposerStep.StartDirectConversation as const,
|
step: ComposerStep.StartDirectConversation as const,
|
||||||
|
@ -16,7 +17,7 @@ export const defaultChooseGroupMembersComposerState = {
|
||||||
uuidFetchState: {},
|
uuidFetchState: {},
|
||||||
groupAvatar: undefined,
|
groupAvatar: undefined,
|
||||||
groupName: '',
|
groupName: '',
|
||||||
groupExpireTimer: 0,
|
groupExpireTimer: DurationInSeconds.ZERO,
|
||||||
maximumGroupSizeModalState: OneTimeModalState.NeverShown,
|
maximumGroupSizeModalState: OneTimeModalState.NeverShown,
|
||||||
recommendedGroupSizeModalState: OneTimeModalState.NeverShown,
|
recommendedGroupSizeModalState: OneTimeModalState.NeverShown,
|
||||||
selectedConversationIds: [],
|
selectedConversationIds: [],
|
||||||
|
@ -28,7 +29,7 @@ export const defaultSetGroupMetadataComposerState = {
|
||||||
isEditingAvatar: false,
|
isEditingAvatar: false,
|
||||||
groupAvatar: undefined,
|
groupAvatar: undefined,
|
||||||
groupName: '',
|
groupName: '',
|
||||||
groupExpireTimer: 0,
|
groupExpireTimer: DurationInSeconds.ZERO,
|
||||||
maximumGroupSizeModalState: OneTimeModalState.NeverShown,
|
maximumGroupSizeModalState: OneTimeModalState.NeverShown,
|
||||||
recommendedGroupSizeModalState: OneTimeModalState.NeverShown,
|
recommendedGroupSizeModalState: OneTimeModalState.NeverShown,
|
||||||
selectedConversationIds: [],
|
selectedConversationIds: [],
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import { setupI18n } from '../../util/setupI18n';
|
import { setupI18n } from '../../util/setupI18n';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import esMessages from '../../../_locales/es/messages.json';
|
import esMessages from '../../../_locales/es/messages.json';
|
||||||
import nbMessages from '../../../_locales/nb/messages.json';
|
import nbMessages from '../../../_locales/nb/messages.json';
|
||||||
|
@ -24,7 +25,7 @@ describe('expiration timer utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('includes 1 hour as seconds', () => {
|
it('includes 1 hour as seconds', () => {
|
||||||
const oneHour = moment.duration(1, 'hour').asSeconds();
|
const oneHour = DurationInSeconds.fromHours(1);
|
||||||
assert.include(DEFAULT_DURATIONS_IN_SECONDS, oneHour);
|
assert.include(DEFAULT_DURATIONS_IN_SECONDS, oneHour);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -37,7 +38,7 @@ describe('expiration timer utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles no duration', () => {
|
it('handles no duration', () => {
|
||||||
assert.strictEqual(format(i18n, 0), 'off');
|
assert.strictEqual(format(i18n, DurationInSeconds.ZERO), 'off');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats durations', () => {
|
it('formats durations', () => {
|
||||||
|
@ -59,22 +60,31 @@ describe('expiration timer utilities', () => {
|
||||||
[moment.duration(3, 'w').asSeconds(), '3 weeks'],
|
[moment.duration(3, 'w').asSeconds(), '3 weeks'],
|
||||||
[moment.duration(52, 'w').asSeconds(), '52 weeks'],
|
[moment.duration(52, 'w').asSeconds(), '52 weeks'],
|
||||||
]).forEach((expected, input) => {
|
]).forEach((expected, input) => {
|
||||||
assert.strictEqual(format(i18n, input), expected);
|
assert.strictEqual(
|
||||||
|
format(i18n, DurationInSeconds.fromSeconds(input)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats other languages successfully', () => {
|
it('formats other languages successfully', () => {
|
||||||
const esI18n = setupI18n('es', esMessages);
|
const esI18n = setupI18n('es', esMessages);
|
||||||
assert.strictEqual(format(esI18n, 120), '2 minutos');
|
assert.strictEqual(
|
||||||
|
format(esI18n, DurationInSeconds.fromSeconds(120)),
|
||||||
|
'2 minutos'
|
||||||
|
);
|
||||||
|
|
||||||
const zhCnI18n = setupI18n('zh-CN', zhCnMessages);
|
const zhCnI18n = setupI18n('zh-CN', zhCnMessages);
|
||||||
assert.strictEqual(format(zhCnI18n, 60), '1 分钟');
|
assert.strictEqual(
|
||||||
|
format(zhCnI18n, DurationInSeconds.fromSeconds(60)),
|
||||||
|
'1 分钟'
|
||||||
|
);
|
||||||
|
|
||||||
// The underlying library supports the "pt" locale, not the "pt_BR" locale. That's
|
// The underlying library supports the "pt" locale, not the "pt_BR" locale. That's
|
||||||
// what we're testing here.
|
// what we're testing here.
|
||||||
const ptBrI18n = setupI18n('pt_BR', ptBrMessages);
|
const ptBrI18n = setupI18n('pt_BR', ptBrMessages);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
format(ptBrI18n, moment.duration(5, 'days').asSeconds()),
|
format(ptBrI18n, DurationInSeconds.fromDays(5)),
|
||||||
'5 dias'
|
'5 dias'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -83,7 +93,7 @@ describe('expiration timer utilities', () => {
|
||||||
[setupI18n('nb', nbMessages), setupI18n('nn', nlMessages)].forEach(
|
[setupI18n('nb', nbMessages), setupI18n('nn', nlMessages)].forEach(
|
||||||
norwegianI18n => {
|
norwegianI18n => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
format(norwegianI18n, moment.duration(6, 'hours').asSeconds()),
|
format(norwegianI18n, DurationInSeconds.fromHours(6)),
|
||||||
'6 timer'
|
'6 timer'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -92,41 +102,76 @@ describe('expiration timer utilities', () => {
|
||||||
|
|
||||||
it('falls back to English if the locale is not supported', () => {
|
it('falls back to English if the locale is not supported', () => {
|
||||||
const badI18n = setupI18n('bogus', {});
|
const badI18n = setupI18n('bogus', {});
|
||||||
assert.strictEqual(format(badI18n, 120), '2 minutes');
|
assert.strictEqual(
|
||||||
|
format(badI18n, DurationInSeconds.fromSeconds(120)),
|
||||||
|
'2 minutes'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles a "mix" of units gracefully', () => {
|
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
|
// 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.
|
// client gives us an unexpected timestamp.
|
||||||
const mix = moment
|
const mix = DurationInSeconds.fromSeconds(
|
||||||
.duration(6, 'days')
|
moment.duration(6, 'days').add(moment.duration(2, 'hours')).asSeconds()
|
||||||
.add(moment.duration(2, 'hours'))
|
);
|
||||||
.asSeconds();
|
|
||||||
assert.strictEqual(format(i18n, mix), '6 days, 2 hours');
|
assert.strictEqual(format(i18n, mix), '6 days, 2 hours');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles negative numbers gracefully', () => {
|
it('handles negative numbers gracefully', () => {
|
||||||
// The proto helps enforce non-negative numbers by specifying a u32, but because
|
// The proto helps enforce non-negative numbers by specifying a u32, but because
|
||||||
// JavaScript lacks such a type, we test it here.
|
// JavaScript lacks such a type, we test it here.
|
||||||
assert.strictEqual(format(i18n, -1), '1 second');
|
assert.strictEqual(
|
||||||
assert.strictEqual(format(i18n, -120), '2 minutes');
|
format(i18n, DurationInSeconds.fromSeconds(-1)),
|
||||||
assert.strictEqual(format(i18n, -0), 'off');
|
'1 second'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
format(i18n, DurationInSeconds.fromSeconds(-120)),
|
||||||
|
'2 minutes'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
format(i18n, DurationInSeconds.fromSeconds(-0)),
|
||||||
|
'off'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles fractional seconds gracefully', () => {
|
it('handles fractional seconds gracefully', () => {
|
||||||
// The proto helps enforce integer numbers by specifying a u32, but this function
|
// The proto helps enforce integer numbers by specifying a u32, but this function
|
||||||
// shouldn't choke if bad data is passed somehow.
|
// shouldn't choke if bad data is passed somehow.
|
||||||
assert.strictEqual(format(i18n, 4.2), '4 seconds');
|
assert.strictEqual(
|
||||||
assert.strictEqual(format(i18n, 4.8), '4 seconds');
|
format(i18n, DurationInSeconds.fromSeconds(4.2)),
|
||||||
assert.strictEqual(format(i18n, 0.2), '1 second');
|
'4 seconds'
|
||||||
assert.strictEqual(format(i18n, 0.8), '1 second');
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
format(i18n, DurationInSeconds.fromSeconds(4.8)),
|
||||||
|
'4 seconds'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
format(i18n, DurationInSeconds.fromSeconds(0.2)),
|
||||||
|
'1 second'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
format(i18n, DurationInSeconds.fromSeconds(0.8)),
|
||||||
|
'1 second'
|
||||||
|
);
|
||||||
|
|
||||||
// If multiple things go wrong and we pass a fractional negative number, we still
|
// If multiple things go wrong and we pass a fractional negative number, we still
|
||||||
// shouldn't explode.
|
// shouldn't explode.
|
||||||
assert.strictEqual(format(i18n, -4.2), '4 seconds');
|
assert.strictEqual(
|
||||||
assert.strictEqual(format(i18n, -4.8), '4 seconds');
|
format(i18n, DurationInSeconds.fromSeconds(-4.2)),
|
||||||
assert.strictEqual(format(i18n, -0.2), '1 second');
|
'4 seconds'
|
||||||
assert.strictEqual(format(i18n, -0.8), '1 second');
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
format(i18n, DurationInSeconds.fromSeconds(-4.8)),
|
||||||
|
'4 seconds'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
format(i18n, DurationInSeconds.fromSeconds(-0.2)),
|
||||||
|
'1 second'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
format(i18n, DurationInSeconds.fromSeconds(-0.8)),
|
||||||
|
'1 second'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,8 +2,12 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as durations from '../../util/durations';
|
import * as durations from '../../util/durations';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
|
|
||||||
export type TestExpireTimer = Readonly<{ value: number; label: string }>;
|
export type TestExpireTimer = Readonly<{
|
||||||
|
value: DurationInSeconds;
|
||||||
|
label: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
export const EXPIRE_TIMERS: ReadonlyArray<TestExpireTimer> = [
|
export const EXPIRE_TIMERS: ReadonlyArray<TestExpireTimer> = [
|
||||||
{ value: 42 * durations.SECOND, label: '42 seconds' },
|
{ value: 42 * durations.SECOND, label: '42 seconds' },
|
||||||
|
@ -11,4 +15,9 @@ export const EXPIRE_TIMERS: ReadonlyArray<TestExpireTimer> = [
|
||||||
{ value: 1 * durations.HOUR, label: '1 hour' },
|
{ value: 1 * durations.HOUR, label: '1 hour' },
|
||||||
{ value: 6 * durations.DAY, label: '6 days' },
|
{ value: 6 * durations.DAY, label: '6 days' },
|
||||||
{ value: 3 * durations.WEEK, label: '3 weeks' },
|
{ value: 3 * durations.WEEK, label: '3 weeks' },
|
||||||
];
|
].map(({ value, label }) => {
|
||||||
|
return {
|
||||||
|
value: DurationInSeconds.fromMillis(value),
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { assert } from 'chai';
|
||||||
import dataInterface from '../../sql/Client';
|
import dataInterface from '../../sql/Client';
|
||||||
import { UUID } from '../../types/UUID';
|
import { UUID } from '../../types/UUID';
|
||||||
import type { UUIDStringType } from '../../types/UUID';
|
import type { UUIDStringType } from '../../types/UUID';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
|
|
||||||
import type { MessageAttributesType } from '../../model-types.d';
|
import type { MessageAttributesType } from '../../model-types.d';
|
||||||
|
|
||||||
|
@ -342,7 +343,7 @@ describe('sql/conversationSummary', () => {
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
conversationId,
|
conversationId,
|
||||||
expirationTimerUpdate: {
|
expirationTimerUpdate: {
|
||||||
expireTimer: 10,
|
expireTimer: DurationInSeconds.fromSeconds(10),
|
||||||
source: 'you',
|
source: 'you',
|
||||||
},
|
},
|
||||||
sent_at: now + 1,
|
sent_at: now + 1,
|
||||||
|
@ -355,7 +356,7 @@ describe('sql/conversationSummary', () => {
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
conversationId,
|
conversationId,
|
||||||
expirationTimerUpdate: {
|
expirationTimerUpdate: {
|
||||||
expireTimer: 10,
|
expireTimer: DurationInSeconds.fromSeconds(10),
|
||||||
fromSync: true,
|
fromSync: true,
|
||||||
},
|
},
|
||||||
sent_at: now + 2,
|
sent_at: now + 2,
|
||||||
|
@ -391,7 +392,7 @@ describe('sql/conversationSummary', () => {
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
conversationId,
|
conversationId,
|
||||||
expirationTimerUpdate: {
|
expirationTimerUpdate: {
|
||||||
expireTimer: 10,
|
expireTimer: DurationInSeconds.fromSeconds(10),
|
||||||
source: 'you',
|
source: 'you',
|
||||||
fromSync: false,
|
fromSync: false,
|
||||||
},
|
},
|
||||||
|
@ -405,7 +406,7 @@ describe('sql/conversationSummary', () => {
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
conversationId,
|
conversationId,
|
||||||
expirationTimerUpdate: {
|
expirationTimerUpdate: {
|
||||||
expireTimer: 10,
|
expireTimer: DurationInSeconds.fromSeconds(10),
|
||||||
fromSync: true,
|
fromSync: true,
|
||||||
},
|
},
|
||||||
sent_at: now + 2,
|
sent_at: now + 2,
|
||||||
|
@ -450,7 +451,7 @@ describe('sql/conversationSummary', () => {
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
conversationId,
|
conversationId,
|
||||||
expirationStartTimestamp: now - 2 * 1000,
|
expirationStartTimestamp: now - 2 * 1000,
|
||||||
expireTimer: 1,
|
expireTimer: DurationInSeconds.fromSeconds(1),
|
||||||
sent_at: now + 2,
|
sent_at: now + 2,
|
||||||
received_at: now + 2,
|
received_at: now + 2,
|
||||||
timestamp: now + 2,
|
timestamp: now + 2,
|
||||||
|
@ -484,7 +485,7 @@ describe('sql/conversationSummary', () => {
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
conversationId,
|
conversationId,
|
||||||
expirationStartTimestamp: now,
|
expirationStartTimestamp: now,
|
||||||
expireTimer: 30,
|
expireTimer: DurationInSeconds.fromSeconds(30),
|
||||||
sent_at: now + 1,
|
sent_at: now + 1,
|
||||||
received_at: now + 1,
|
received_at: now + 1,
|
||||||
timestamp: now + 1,
|
timestamp: now + 1,
|
||||||
|
@ -495,7 +496,7 @@ describe('sql/conversationSummary', () => {
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
conversationId,
|
conversationId,
|
||||||
expirationStartTimestamp: now - 2 * 1000,
|
expirationStartTimestamp: now - 2 * 1000,
|
||||||
expireTimer: 1,
|
expireTimer: DurationInSeconds.fromSeconds(1),
|
||||||
sent_at: now + 2,
|
sent_at: now + 2,
|
||||||
received_at: now + 2,
|
received_at: now + 2,
|
||||||
timestamp: now + 2,
|
timestamp: now + 2,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { UUID } from '../../types/UUID';
|
||||||
import type { UUIDStringType } from '../../types/UUID';
|
import type { UUIDStringType } from '../../types/UUID';
|
||||||
|
|
||||||
import type { ReactionType } from '../../types/Reactions';
|
import type { ReactionType } from '../../types/Reactions';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
import type { MessageAttributesType } from '../../model-types.d';
|
import type { MessageAttributesType } from '../../model-types.d';
|
||||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
|
|
||||||
|
@ -331,7 +332,7 @@ describe('sql/markRead', () => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const readAt = start + 20;
|
const readAt = start + 20;
|
||||||
const conversationId = getUuid();
|
const conversationId = getUuid();
|
||||||
const expireTimer = 15;
|
const expireTimer = DurationInSeconds.fromSeconds(15);
|
||||||
const ourUuid = getUuid();
|
const ourUuid = getUuid();
|
||||||
|
|
||||||
const message1: MessageAttributesType = {
|
const message1: MessageAttributesType = {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import type { ConversationType } from '../../../state/ducks/conversations';
|
||||||
import type { MessageAttributesType } from '../../../model-types.d';
|
import type { MessageAttributesType } from '../../../model-types.d';
|
||||||
import type { StateType as RootStateType } from '../../../state/reducer';
|
import type { StateType as RootStateType } from '../../../state/reducer';
|
||||||
import type { UUIDStringType } from '../../../types/UUID';
|
import type { UUIDStringType } from '../../../types/UUID';
|
||||||
import { DAY } from '../../../util/durations';
|
import { DurationInSeconds } from '../../../util/durations';
|
||||||
import { TEXT_ATTACHMENT, IMAGE_JPEG } from '../../../types/MIME';
|
import { TEXT_ATTACHMENT, IMAGE_JPEG } from '../../../types/MIME';
|
||||||
import { ReadStatus } from '../../../messages/MessageReadStatus';
|
import { ReadStatus } from '../../../messages/MessageReadStatus';
|
||||||
import {
|
import {
|
||||||
|
@ -74,7 +74,7 @@ describe('both/state/ducks/stories', () => {
|
||||||
return {
|
return {
|
||||||
conversationId,
|
conversationId,
|
||||||
expirationStartTimestamp: now,
|
expirationStartTimestamp: now,
|
||||||
expireTimer: 1 * DAY,
|
expireTimer: DurationInSeconds.DAY,
|
||||||
messageId,
|
messageId,
|
||||||
readStatus: ReadStatus.Unread,
|
readStatus: ReadStatus.Unread,
|
||||||
timestamp: now - timestampDelta,
|
timestamp: now - timestampDelta,
|
||||||
|
@ -538,7 +538,7 @@ describe('both/state/ducks/stories', () => {
|
||||||
? ourConversationId
|
? ourConversationId
|
||||||
: groupConversationId,
|
: groupConversationId,
|
||||||
expirationStartTimestamp: now,
|
expirationStartTimestamp: now,
|
||||||
expireTimer: 1 * DAY,
|
expireTimer: DurationInSeconds.DAY,
|
||||||
messageId,
|
messageId,
|
||||||
readStatus: ReadStatus.Unread,
|
readStatus: ReadStatus.Unread,
|
||||||
sendStateByConversationId: {},
|
sendStateByConversationId: {},
|
||||||
|
|
|
@ -5,13 +5,14 @@ import { assert } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { RowType } from '../../../components/ConversationList';
|
import { RowType } from '../../../components/ConversationList';
|
||||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||||
|
import { DurationInSeconds } from '../../../util/durations';
|
||||||
|
|
||||||
import { LeftPaneSetGroupMetadataHelper } from '../../../components/leftPane/LeftPaneSetGroupMetadataHelper';
|
import { LeftPaneSetGroupMetadataHelper } from '../../../components/leftPane/LeftPaneSetGroupMetadataHelper';
|
||||||
|
|
||||||
function getComposeState() {
|
function getComposeState() {
|
||||||
return {
|
return {
|
||||||
groupAvatar: undefined,
|
groupAvatar: undefined,
|
||||||
groupExpireTimer: 0,
|
groupExpireTimer: DurationInSeconds.ZERO,
|
||||||
groupName: '',
|
groupName: '',
|
||||||
hasError: false,
|
hasError: false,
|
||||||
isCreating: false,
|
isCreating: false,
|
||||||
|
|
|
@ -7,23 +7,27 @@ import protobuf from '../protobuf/wrap';
|
||||||
|
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import { normalizeUuid } from '../util/normalizeUuid';
|
import { normalizeUuid } from '../util/normalizeUuid';
|
||||||
|
import { DurationInSeconds } from '../util/durations';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
import Avatar = Proto.ContactDetails.IAvatar;
|
import Avatar = Proto.ContactDetails.IAvatar;
|
||||||
|
|
||||||
const { Reader } = protobuf;
|
const { Reader } = protobuf;
|
||||||
|
|
||||||
type OptionalAvatar = { avatar?: Avatar | null };
|
type OptionalFields = { avatar?: Avatar | null; expireTimer?: number | null };
|
||||||
|
|
||||||
type DecoderBase<Message extends OptionalAvatar> = {
|
type DecoderBase<Message extends OptionalFields> = {
|
||||||
decodeDelimited(reader: protobuf.Reader): Message | undefined;
|
decodeDelimited(reader: protobuf.Reader): Message | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageWithAvatar<Message extends OptionalAvatar> = Omit<
|
type HydratedAvatar = Avatar & { data: Uint8Array };
|
||||||
|
|
||||||
|
type MessageWithAvatar<Message extends OptionalFields> = Omit<
|
||||||
Message,
|
Message,
|
||||||
'avatar'
|
'avatar'
|
||||||
> & {
|
> & {
|
||||||
avatar?: (Avatar & { data: Uint8Array }) | null;
|
avatar?: HydratedAvatar;
|
||||||
|
expireTimer?: DurationInSeconds;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModifiedGroupDetails = MessageWithAvatar<Proto.GroupDetails>;
|
export type ModifiedGroupDetails = MessageWithAvatar<Proto.GroupDetails>;
|
||||||
|
@ -32,7 +36,7 @@ export type ModifiedContactDetails = MessageWithAvatar<Proto.ContactDetails>;
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/brace-style -- Prettier conflicts with ESLint */
|
/* eslint-disable @typescript-eslint/brace-style -- Prettier conflicts with ESLint */
|
||||||
abstract class ParserBase<
|
abstract class ParserBase<
|
||||||
Message extends OptionalAvatar,
|
Message extends OptionalFields,
|
||||||
Decoder extends DecoderBase<Message>,
|
Decoder extends DecoderBase<Message>,
|
||||||
Result
|
Result
|
||||||
> implements Iterable<Result>
|
> implements Iterable<Result>
|
||||||
|
@ -57,28 +61,33 @@ abstract class ParserBase<
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!proto.avatar) {
|
let avatar: HydratedAvatar | undefined;
|
||||||
return {
|
if (proto.avatar) {
|
||||||
...proto,
|
const attachmentLen = proto.avatar.length ?? 0;
|
||||||
avatar: null,
|
const avatarData = this.reader.buf.slice(
|
||||||
|
this.reader.pos,
|
||||||
|
this.reader.pos + attachmentLen
|
||||||
|
);
|
||||||
|
this.reader.skip(attachmentLen);
|
||||||
|
|
||||||
|
avatar = {
|
||||||
|
...proto.avatar,
|
||||||
|
|
||||||
|
data: avatarData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachmentLen = proto.avatar.length ?? 0;
|
let expireTimer: DurationInSeconds | undefined;
|
||||||
const avatarData = this.reader.buf.slice(
|
|
||||||
this.reader.pos,
|
if (proto.expireTimer != null) {
|
||||||
this.reader.pos + attachmentLen
|
expireTimer = DurationInSeconds.fromSeconds(proto.expireTimer);
|
||||||
);
|
}
|
||||||
this.reader.skip(attachmentLen);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...proto,
|
...proto,
|
||||||
|
|
||||||
avatar: {
|
avatar,
|
||||||
...proto.avatar,
|
expireTimer,
|
||||||
|
|
||||||
data: avatarData,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(
|
log.error(
|
||||||
|
@ -118,6 +127,7 @@ export class GroupBuffer extends ParserBase<
|
||||||
if (!proto.members) {
|
if (!proto.members) {
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...proto,
|
...proto,
|
||||||
members: proto.members.map((member, i) => {
|
members: proto.members.map((member, i) => {
|
||||||
|
|
|
@ -45,6 +45,7 @@ import { normalizeUuid } from '../util/normalizeUuid';
|
||||||
import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
||||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||||
import { Zone } from '../util/Zone';
|
import { Zone } from '../util/Zone';
|
||||||
|
import { DurationInSeconds } from '../util/durations';
|
||||||
import { deriveMasterKeyFromGroupV1, bytesToUuid } from '../Crypto';
|
import { deriveMasterKeyFromGroupV1, bytesToUuid } from '../Crypto';
|
||||||
import type { DownloadedAttachmentType } from '../types/Attachment';
|
import type { DownloadedAttachmentType } from '../types/Attachment';
|
||||||
import { Address } from '../types/Address';
|
import { Address } from '../types/Address';
|
||||||
|
@ -2058,7 +2059,7 @@ export default class MessageReceiver
|
||||||
attachments,
|
attachments,
|
||||||
preview,
|
preview,
|
||||||
canReplyToStory: Boolean(msg.allowsReplies),
|
canReplyToStory: Boolean(msg.allowsReplies),
|
||||||
expireTimer: durations.DAY / 1000,
|
expireTimer: DurationInSeconds.DAY,
|
||||||
flags: 0,
|
flags: 0,
|
||||||
groupV2,
|
groupV2,
|
||||||
isStory: true,
|
isStory: true,
|
||||||
|
|
|
@ -65,6 +65,7 @@ import { concat, isEmpty, map } from '../util/iterables';
|
||||||
import type { SendTypesType } from '../util/handleMessageSend';
|
import type { SendTypesType } from '../util/handleMessageSend';
|
||||||
import { shouldSaveProto, sendTypesEnum } from '../util/handleMessageSend';
|
import { shouldSaveProto, sendTypesEnum } from '../util/handleMessageSend';
|
||||||
import { uuidToBytes } from '../util/uuidToBytes';
|
import { uuidToBytes } from '../util/uuidToBytes';
|
||||||
|
import type { DurationInSeconds } from '../util/durations';
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import type { Avatar, EmbeddedContactType } from '../types/EmbeddedContact';
|
import type { Avatar, EmbeddedContactType } from '../types/EmbeddedContact';
|
||||||
|
@ -174,7 +175,7 @@ export type MessageOptionsType = {
|
||||||
attachments?: ReadonlyArray<AttachmentType> | null;
|
attachments?: ReadonlyArray<AttachmentType> | null;
|
||||||
body?: string;
|
body?: string;
|
||||||
contact?: Array<ContactWithHydratedAvatar>;
|
contact?: Array<ContactWithHydratedAvatar>;
|
||||||
expireTimer?: number;
|
expireTimer?: DurationInSeconds;
|
||||||
flags?: number;
|
flags?: number;
|
||||||
group?: {
|
group?: {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -198,7 +199,7 @@ export type GroupSendOptionsType = {
|
||||||
attachments?: Array<AttachmentType>;
|
attachments?: Array<AttachmentType>;
|
||||||
contact?: Array<ContactWithHydratedAvatar>;
|
contact?: Array<ContactWithHydratedAvatar>;
|
||||||
deletedForEveryoneTimestamp?: number;
|
deletedForEveryoneTimestamp?: number;
|
||||||
expireTimer?: number;
|
expireTimer?: DurationInSeconds;
|
||||||
flags?: number;
|
flags?: number;
|
||||||
groupCallUpdate?: GroupCallUpdateType;
|
groupCallUpdate?: GroupCallUpdateType;
|
||||||
groupV1?: GroupV1InfoType;
|
groupV1?: GroupV1InfoType;
|
||||||
|
@ -221,7 +222,7 @@ class Message {
|
||||||
|
|
||||||
contact?: Array<ContactWithHydratedAvatar>;
|
contact?: Array<ContactWithHydratedAvatar>;
|
||||||
|
|
||||||
expireTimer?: number;
|
expireTimer?: DurationInSeconds;
|
||||||
|
|
||||||
flags?: number;
|
flags?: number;
|
||||||
|
|
||||||
|
@ -1358,7 +1359,7 @@ export default class MessageSender {
|
||||||
contact?: Array<ContactWithHydratedAvatar>;
|
contact?: Array<ContactWithHydratedAvatar>;
|
||||||
contentHint: number;
|
contentHint: number;
|
||||||
deletedForEveryoneTimestamp: number | undefined;
|
deletedForEveryoneTimestamp: number | undefined;
|
||||||
expireTimer: number | undefined;
|
expireTimer: DurationInSeconds | undefined;
|
||||||
groupId: string | undefined;
|
groupId: string | undefined;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
messageText: string | undefined;
|
messageText: string | undefined;
|
||||||
|
|
3
ts/textsecure/Types.d.ts
vendored
3
ts/textsecure/Types.d.ts
vendored
|
@ -7,6 +7,7 @@ import type { UUID, UUIDStringType } from '../types/UUID';
|
||||||
import type { TextAttachmentType } from '../types/Attachment';
|
import type { TextAttachmentType } from '../types/Attachment';
|
||||||
import type { GiftBadgeStates } from '../components/conversation/Message';
|
import type { GiftBadgeStates } from '../components/conversation/Message';
|
||||||
import type { MIMEType } from '../types/MIME';
|
import type { MIMEType } from '../types/MIME';
|
||||||
|
import type { DurationInSeconds } from '../util/durations';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IdentityKeyType,
|
IdentityKeyType,
|
||||||
|
@ -207,7 +208,7 @@ export type ProcessedDataMessage = {
|
||||||
group?: ProcessedGroupContext;
|
group?: ProcessedGroupContext;
|
||||||
groupV2?: ProcessedGroupV2Context;
|
groupV2?: ProcessedGroupV2Context;
|
||||||
flags: number;
|
flags: number;
|
||||||
expireTimer: number;
|
expireTimer: DurationInSeconds;
|
||||||
profileKey?: string;
|
profileKey?: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
quote?: ProcessedQuote;
|
quote?: ProcessedQuote;
|
||||||
|
|
|
@ -28,7 +28,7 @@ import type {
|
||||||
import { WarnOnlyError } from './Errors';
|
import { WarnOnlyError } from './Errors';
|
||||||
import { GiftBadgeStates } from '../components/conversation/Message';
|
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||||
import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../types/MIME';
|
import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../types/MIME';
|
||||||
import { SECOND } from '../util/durations';
|
import { SECOND, DurationInSeconds } from '../util/durations';
|
||||||
|
|
||||||
const FLAGS = Proto.DataMessage.Flags;
|
const FLAGS = Proto.DataMessage.Flags;
|
||||||
export const ATTACHMENT_MAX = 32;
|
export const ATTACHMENT_MAX = 32;
|
||||||
|
@ -299,7 +299,7 @@ export function processDataMessage(
|
||||||
group: processGroupContext(message.group),
|
group: processGroupContext(message.group),
|
||||||
groupV2: processGroupV2Context(message.groupV2),
|
groupV2: processGroupV2Context(message.groupV2),
|
||||||
flags: message.flags ?? 0,
|
flags: message.flags ?? 0,
|
||||||
expireTimer: message.expireTimer ?? 0,
|
expireTimer: DurationInSeconds.fromSeconds(message.expireTimer ?? 0),
|
||||||
profileKey:
|
profileKey:
|
||||||
message.profileKey && message.profileKey.length > 0
|
message.profileKey && message.profileKey.length > 0
|
||||||
? Bytes.toBase64(message.profileKey)
|
? Bytes.toBase64(message.profileKey)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
import type { DurationInSeconds } from '../util/durations';
|
||||||
import type { AttachmentType } from './Attachment';
|
import type { AttachmentType } from './Attachment';
|
||||||
import type { EmbeddedContactType } from './EmbeddedContact';
|
import type { EmbeddedContactType } from './EmbeddedContact';
|
||||||
import type { IndexableBoolean, IndexablePresence } from './IndexedDB';
|
import type { IndexableBoolean, IndexablePresence } from './IndexedDB';
|
||||||
|
@ -30,7 +31,7 @@ export type IncomingMessage = Readonly<
|
||||||
body?: string;
|
body?: string;
|
||||||
decrypted_at?: number;
|
decrypted_at?: number;
|
||||||
errors?: Array<Error>;
|
errors?: Array<Error>;
|
||||||
expireTimer?: number;
|
expireTimer?: DurationInSeconds;
|
||||||
messageTimer?: number; // deprecated
|
messageTimer?: number; // deprecated
|
||||||
isViewOnce?: number;
|
isViewOnce?: number;
|
||||||
flags?: number;
|
flags?: number;
|
||||||
|
@ -54,7 +55,7 @@ export type OutgoingMessage = Readonly<
|
||||||
|
|
||||||
// Optional
|
// Optional
|
||||||
body?: string;
|
body?: string;
|
||||||
expireTimer?: number;
|
expireTimer?: DurationInSeconds;
|
||||||
messageTimer?: number; // deprecated
|
messageTimer?: number; // deprecated
|
||||||
isViewOnce?: number;
|
isViewOnce?: number;
|
||||||
synced: boolean;
|
synced: boolean;
|
||||||
|
@ -88,7 +89,7 @@ export type SharedMessageProperties = Readonly<{
|
||||||
export type ExpirationTimerUpdate = Partial<
|
export type ExpirationTimerUpdate = Partial<
|
||||||
Readonly<{
|
Readonly<{
|
||||||
expirationTimerUpdate: Readonly<{
|
expirationTimerUpdate: Readonly<{
|
||||||
expireTimer: number;
|
expireTimer: DurationInSeconds;
|
||||||
fromSync: boolean;
|
fromSync: boolean;
|
||||||
source: string; // PhoneNumber
|
source: string; // PhoneNumber
|
||||||
}>;
|
}>;
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { PhoneNumberDiscoverability } from './phoneNumberDiscoverability';
|
||||||
import { PhoneNumberSharingMode } from './phoneNumberSharingMode';
|
import { PhoneNumberSharingMode } from './phoneNumberSharingMode';
|
||||||
import { assertDev } from './assert';
|
import { assertDev } from './assert';
|
||||||
import * as durations from './durations';
|
import * as durations from './durations';
|
||||||
|
import type { DurationInSeconds } from './durations';
|
||||||
import { isPhoneNumberSharingEnabled } from './isPhoneNumberSharingEnabled';
|
import { isPhoneNumberSharingEnabled } from './isPhoneNumberSharingEnabled';
|
||||||
import {
|
import {
|
||||||
parseE164FromSignalDotMeHash,
|
parseE164FromSignalDotMeHash,
|
||||||
|
@ -66,7 +67,7 @@ export type IPCEventsValuesType = {
|
||||||
spellCheck: boolean;
|
spellCheck: boolean;
|
||||||
systemTraySetting: SystemTraySetting;
|
systemTraySetting: SystemTraySetting;
|
||||||
themeSetting: ThemeType;
|
themeSetting: ThemeType;
|
||||||
universalExpireTimer: number;
|
universalExpireTimer: DurationInSeconds;
|
||||||
zoomFactor: ZoomFactorType;
|
zoomFactor: ZoomFactorType;
|
||||||
storyViewReceiptsEnabled: boolean;
|
storyViewReceiptsEnabled: boolean;
|
||||||
|
|
||||||
|
|
39
ts/util/durations/duration-in-seconds.ts
Normal file
39
ts/util/durations/duration-in-seconds.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as Constants from './constants';
|
||||||
|
|
||||||
|
export type DurationInSeconds = number & {
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
__time_difference_in_seconds: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-namespace, @typescript-eslint/no-redeclare */
|
||||||
|
export namespace DurationInSeconds {
|
||||||
|
export const fromMillis = (ms: number): DurationInSeconds =>
|
||||||
|
(ms / Constants.SECOND) as DurationInSeconds;
|
||||||
|
export const fromSeconds = (seconds: number): DurationInSeconds =>
|
||||||
|
seconds as DurationInSeconds;
|
||||||
|
export const fromMinutes = (m: number): DurationInSeconds =>
|
||||||
|
((m * Constants.MINUTE) / Constants.SECOND) as DurationInSeconds;
|
||||||
|
export const fromHours = (h: number): DurationInSeconds =>
|
||||||
|
((h * Constants.HOUR) / Constants.SECOND) as DurationInSeconds;
|
||||||
|
export const fromDays = (d: number): DurationInSeconds =>
|
||||||
|
((d * Constants.DAY) / Constants.SECOND) as DurationInSeconds;
|
||||||
|
export const fromWeeks = (d: number): DurationInSeconds =>
|
||||||
|
((d * Constants.WEEK) / Constants.SECOND) as DurationInSeconds;
|
||||||
|
export const fromMonths = (d: number): DurationInSeconds =>
|
||||||
|
((d * Constants.MONTH) / Constants.SECOND) as DurationInSeconds;
|
||||||
|
|
||||||
|
export const toSeconds = (d: DurationInSeconds): number => d;
|
||||||
|
export const toMillis = (d: DurationInSeconds): number =>
|
||||||
|
d * Constants.SECOND;
|
||||||
|
export const toHours = (d: DurationInSeconds): number =>
|
||||||
|
(d * Constants.SECOND) / Constants.HOUR;
|
||||||
|
|
||||||
|
export const ZERO = DurationInSeconds.fromSeconds(0);
|
||||||
|
export const HOUR = DurationInSeconds.fromHours(1);
|
||||||
|
export const MINUTE = DurationInSeconds.fromMinutes(1);
|
||||||
|
export const DAY = DurationInSeconds.fromDays(1);
|
||||||
|
}
|
||||||
|
/* eslint-enable @typescript-eslint/no-namespace, @typescript-eslint/no-redeclare */
|
5
ts/util/durations/index.ts
Normal file
5
ts/util/durations/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export * from './constants';
|
||||||
|
export { DurationInSeconds } from './duration-in-seconds';
|
|
@ -1,26 +1,25 @@
|
||||||
// Copyright 2020-2021 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 moment from 'moment';
|
|
||||||
import humanizeDuration from 'humanize-duration';
|
import humanizeDuration from 'humanize-duration';
|
||||||
import type { Unit } from 'humanize-duration';
|
import type { Unit } from 'humanize-duration';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import { SECOND } from './durations';
|
import { SECOND, DurationInSeconds } from './durations';
|
||||||
|
|
||||||
const SECONDS_PER_WEEK = 604800;
|
const SECONDS_PER_WEEK = 604800;
|
||||||
export const DEFAULT_DURATIONS_IN_SECONDS: ReadonlyArray<number> = [
|
export const DEFAULT_DURATIONS_IN_SECONDS: ReadonlyArray<DurationInSeconds> = [
|
||||||
0,
|
DurationInSeconds.ZERO,
|
||||||
moment.duration(4, 'weeks').asSeconds(),
|
DurationInSeconds.fromWeeks(4),
|
||||||
moment.duration(1, 'week').asSeconds(),
|
DurationInSeconds.fromWeeks(1),
|
||||||
moment.duration(1, 'day').asSeconds(),
|
DurationInSeconds.fromDays(1),
|
||||||
moment.duration(8, 'hours').asSeconds(),
|
DurationInSeconds.fromHours(8),
|
||||||
moment.duration(1, 'hour').asSeconds(),
|
DurationInSeconds.fromHours(1),
|
||||||
moment.duration(5, 'minutes').asSeconds(),
|
DurationInSeconds.fromMinutes(5),
|
||||||
moment.duration(30, 'seconds').asSeconds(),
|
DurationInSeconds.fromSeconds(30),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_DURATIONS_SET: ReadonlySet<number> = new Set<number>(
|
export const DEFAULT_DURATIONS_SET: ReadonlySet<DurationInSeconds> = new Set(
|
||||||
DEFAULT_DURATIONS_IN_SECONDS
|
DEFAULT_DURATIONS_IN_SECONDS
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -31,7 +30,7 @@ export type FormatOptions = {
|
||||||
|
|
||||||
export function format(
|
export function format(
|
||||||
i18n: LocalizerType,
|
i18n: LocalizerType,
|
||||||
dirtySeconds?: number,
|
dirtySeconds?: DurationInSeconds,
|
||||||
{ capitalizeOff = false, largest }: FormatOptions = {}
|
{ capitalizeOff = false, largest }: FormatOptions = {}
|
||||||
): string {
|
): string {
|
||||||
let seconds = Math.abs(dirtySeconds || 0);
|
let seconds = Math.abs(dirtySeconds || 0);
|
||||||
|
@ -66,7 +65,7 @@ export function format(
|
||||||
const defaultUnits: Array<Unit> =
|
const defaultUnits: Array<Unit> =
|
||||||
seconds % SECONDS_PER_WEEK === 0 ? ['w'] : ['d', 'h', 'm', 's'];
|
seconds % SECONDS_PER_WEEK === 0 ? ['w'] : ['d', 'h', 'm', 's'];
|
||||||
|
|
||||||
return humanizeDuration(seconds * 1000, {
|
return humanizeDuration(seconds * SECOND, {
|
||||||
// if we have an explict `largest` specified,
|
// if we have an explict `largest` specified,
|
||||||
// allow it to pick from all the units
|
// allow it to pick from all the units
|
||||||
units: largest ? allUnits : defaultUnits,
|
units: largest ? allUnits : defaultUnits,
|
||||||
|
@ -82,10 +81,10 @@ export function calculateExpirationTimestamp({
|
||||||
expireTimer,
|
expireTimer,
|
||||||
expirationStartTimestamp,
|
expirationStartTimestamp,
|
||||||
}: {
|
}: {
|
||||||
expireTimer: number | undefined;
|
expireTimer: DurationInSeconds | undefined;
|
||||||
expirationStartTimestamp: number | undefined | null;
|
expirationStartTimestamp: number | undefined | null;
|
||||||
}): number | undefined {
|
}): number | undefined {
|
||||||
return isNumber(expirationStartTimestamp) && isNumber(expireTimer)
|
return isNumber(expirationStartTimestamp) && isNumber(expireTimer)
|
||||||
? expirationStartTimestamp + expireTimer * SECOND
|
? expirationStartTimestamp + DurationInSeconds.toMillis(expireTimer)
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { DAY } from './durations';
|
|
||||||
import { getMessageById } from '../messages/getMessageById';
|
import { getMessageById } from '../messages/getMessageById';
|
||||||
import { isNotNil } from './isNotNil';
|
import { isNotNil } from './isNotNil';
|
||||||
|
import { DurationInSeconds } from './durations';
|
||||||
import { markViewed } from '../services/MessageUpdater';
|
import { markViewed } from '../services/MessageUpdater';
|
||||||
import { storageServiceUploadJob } from '../services/storage';
|
import { storageServiceUploadJob } from '../services/storage';
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ export async function markOnboardingStoryAsRead(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
message.set({
|
message.set({
|
||||||
expireTimer: DAY,
|
expireTimer: DurationInSeconds.DAY,
|
||||||
});
|
});
|
||||||
|
|
||||||
message.set(markViewed(message.attributes, storyReadDate));
|
message.set(markViewed(message.attributes, storyReadDate));
|
||||||
|
|
|
@ -7,7 +7,6 @@ import type { SendStateByConversationId } from '../messages/MessageSendState';
|
||||||
import type { UUIDStringType } from '../types/UUID';
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import dataInterface from '../sql/Client';
|
import dataInterface from '../sql/Client';
|
||||||
import { DAY, SECOND } from './durations';
|
|
||||||
import { MY_STORY_ID, StorySendMode } from '../types/Stories';
|
import { MY_STORY_ID, StorySendMode } from '../types/Stories';
|
||||||
import { getStoriesBlocked } from './stories';
|
import { getStoriesBlocked } from './stories';
|
||||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
|
@ -24,6 +23,7 @@ import { incrementMessageCounter } from './incrementMessageCounter';
|
||||||
import { isGroupV2 } from './whatTypeOfConversation';
|
import { isGroupV2 } from './whatTypeOfConversation';
|
||||||
import { isNotNil } from './isNotNil';
|
import { isNotNil } from './isNotNil';
|
||||||
import { collect } from './iterables';
|
import { collect } from './iterables';
|
||||||
|
import { DurationInSeconds } from './durations';
|
||||||
|
|
||||||
export async function sendStoryMessage(
|
export async function sendStoryMessage(
|
||||||
listIds: Array<string>,
|
listIds: Array<string>,
|
||||||
|
@ -158,7 +158,7 @@ export async function sendStoryMessage(
|
||||||
return window.Signal.Migrations.upgradeMessageSchema({
|
return window.Signal.Migrations.upgradeMessageSchema({
|
||||||
attachments,
|
attachments,
|
||||||
conversationId: ourConversation.id,
|
conversationId: ourConversation.id,
|
||||||
expireTimer: DAY / SECOND,
|
expireTimer: DurationInSeconds.DAY,
|
||||||
expirationStartTimestamp: Date.now(),
|
expirationStartTimestamp: Date.now(),
|
||||||
id: UUID.generate().toString(),
|
id: UUID.generate().toString(),
|
||||||
readStatus: ReadStatus.Read,
|
readStatus: ReadStatus.Read,
|
||||||
|
@ -262,7 +262,7 @@ export async function sendStoryMessage(
|
||||||
attachments,
|
attachments,
|
||||||
canReplyToStory: true,
|
canReplyToStory: true,
|
||||||
conversationId: group.id,
|
conversationId: group.id,
|
||||||
expireTimer: DAY / SECOND,
|
expireTimer: DurationInSeconds.DAY,
|
||||||
expirationStartTimestamp: Date.now(),
|
expirationStartTimestamp: Date.now(),
|
||||||
id: UUID.generate().toString(),
|
id: UUID.generate().toString(),
|
||||||
readStatus: ReadStatus.Read,
|
readStatus: ReadStatus.Read,
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { DurationInSeconds } from './durations';
|
||||||
|
|
||||||
export const ITEM_NAME = 'universalExpireTimer';
|
export const ITEM_NAME = 'universalExpireTimer';
|
||||||
|
|
||||||
export function get(): number {
|
export function get(): DurationInSeconds {
|
||||||
return window.storage.get(ITEM_NAME) || 0;
|
return DurationInSeconds.fromSeconds(window.storage.get(ITEM_NAME) || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function set(newValue: number | undefined): Promise<void> {
|
export function set(newValue: DurationInSeconds | undefined): Promise<void> {
|
||||||
return window.storage.put(ITEM_NAME, newValue || 0);
|
return window.storage.put(ITEM_NAME, newValue || DurationInSeconds.ZERO);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {
|
||||||
isGroupV1,
|
isGroupV1,
|
||||||
} from '../util/whatTypeOfConversation';
|
} from '../util/whatTypeOfConversation';
|
||||||
import { findAndFormatContact } from '../util/findAndFormatContact';
|
import { findAndFormatContact } from '../util/findAndFormatContact';
|
||||||
|
import type { DurationInSeconds } from '../util/durations';
|
||||||
import { getPreferredBadgeSelector } from '../state/selectors/badges';
|
import { getPreferredBadgeSelector } from '../state/selectors/badges';
|
||||||
import {
|
import {
|
||||||
canReply,
|
canReply,
|
||||||
|
@ -347,7 +348,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
const conversationHeaderProps = {
|
const conversationHeaderProps = {
|
||||||
id: this.model.id,
|
id: this.model.id,
|
||||||
|
|
||||||
onSetDisappearingMessages: (seconds: number) =>
|
onSetDisappearingMessages: (seconds: DurationInSeconds) =>
|
||||||
this.setDisappearingMessages(seconds),
|
this.setDisappearingMessages(seconds),
|
||||||
onDeleteMessages: () => this.destroyMessages(),
|
onDeleteMessages: () => this.destroyMessages(),
|
||||||
onSearchInConversation: () => {
|
onSearchInConversation: () => {
|
||||||
|
@ -2260,7 +2261,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setDisappearingMessages(seconds: number): Promise<void> {
|
async setDisappearingMessages(seconds: DurationInSeconds): Promise<void> {
|
||||||
const { model }: { model: ConversationModel } = this;
|
const { model }: { model: ConversationModel } = this;
|
||||||
|
|
||||||
const valueToSet = seconds > 0 ? seconds : undefined;
|
const valueToSet = seconds > 0 ? seconds : undefined;
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
shouldMinimizeToSystemTray,
|
shouldMinimizeToSystemTray,
|
||||||
} from '../../types/SystemTraySetting';
|
} from '../../types/SystemTraySetting';
|
||||||
import { awaitObject } from '../../util/awaitObject';
|
import { awaitObject } from '../../util/awaitObject';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
import { createSetting, createCallback } from '../../util/preload';
|
import { createSetting, createCallback } from '../../util/preload';
|
||||||
import { startInteractionMode } from '../startInteractionMode';
|
import { startInteractionMode } from '../startInteractionMode';
|
||||||
|
|
||||||
|
@ -215,6 +216,10 @@ const renderPreferences = async () => {
|
||||||
const { hasMinimizeToAndStartInSystemTray, hasMinimizeToSystemTray } =
|
const { hasMinimizeToAndStartInSystemTray, hasMinimizeToSystemTray } =
|
||||||
getSystemTraySettingValues(systemTraySetting);
|
getSystemTraySettingValues(systemTraySetting);
|
||||||
|
|
||||||
|
const onUniversalExpireTimerChange = reRender(
|
||||||
|
settingUniversalExpireTimer.setValue
|
||||||
|
);
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
// Settings
|
// Settings
|
||||||
availableCameras,
|
availableCameras,
|
||||||
|
@ -250,7 +255,7 @@ const renderPreferences = async () => {
|
||||||
selectedMicrophone,
|
selectedMicrophone,
|
||||||
selectedSpeaker,
|
selectedSpeaker,
|
||||||
themeSetting,
|
themeSetting,
|
||||||
universalExpireTimer,
|
universalExpireTimer: DurationInSeconds.fromSeconds(universalExpireTimer),
|
||||||
whoCanFindMe,
|
whoCanFindMe,
|
||||||
whoCanSeeMe,
|
whoCanSeeMe,
|
||||||
zoomFactor,
|
zoomFactor,
|
||||||
|
@ -347,9 +352,11 @@ const renderPreferences = async () => {
|
||||||
onSelectedSpeakerChange: reRender(settingAudioOutput.setValue),
|
onSelectedSpeakerChange: reRender(settingAudioOutput.setValue),
|
||||||
onSpellCheckChange: reRender(settingSpellCheck.setValue),
|
onSpellCheckChange: reRender(settingSpellCheck.setValue),
|
||||||
onThemeChange: reRender(settingTheme.setValue),
|
onThemeChange: reRender(settingTheme.setValue),
|
||||||
onUniversalExpireTimerChange: reRender(
|
onUniversalExpireTimerChange: (newValue: number): Promise<void> => {
|
||||||
settingUniversalExpireTimer.setValue
|
return onUniversalExpireTimerChange(
|
||||||
),
|
DurationInSeconds.fromSeconds(newValue)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
// Zoom factor change doesn't require immediate rerender since it will:
|
// Zoom factor change doesn't require immediate rerender since it will:
|
||||||
// 1. Update the zoom factor in the main window
|
// 1. Update the zoom factor in the main window
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue