Use DurationInSeconds for expireTimer

This commit is contained in:
Fedor Indutny 2022-11-16 12:18:02 -08:00 committed by GitHub
parent cf57c7aaf0
commit 6be69a7ba8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 411 additions and 216 deletions

View file

@ -7,6 +7,7 @@ import { ConfirmationDialog } from './ConfirmationDialog';
import { Select } from './Select';
import type { LocalizerType } from '../types/Util';
import type { Theme } from '../util/theme';
import { DurationInSeconds } from '../util/durations';
const CSS_MODULE = 'module-disappearing-time-dialog';
@ -15,14 +16,14 @@ const DEFAULT_VALUE = 60;
export type PropsType = Readonly<{
i18n: LocalizerType;
theme?: Theme;
initialValue?: number;
onSubmit: (value: number) => void;
initialValue?: DurationInSeconds;
onSubmit: (value: DurationInSeconds) => void;
onClose: () => void;
}>;
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],
['minutes', 60],
['hours', 60 * 60],
@ -50,14 +51,14 @@ export function DisappearingTimeDialog(props: PropsType): JSX.Element {
let initialUnit = 'seconds';
let initialUnitValue = 1;
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;
}
initialUnit = unit;
initialUnitValue = Math.floor(initialValue / ms);
initialUnitValue = Math.floor(initialValue / sec);
}
const [unitValue, setUnitValue] = useState(initialUnitValue);
@ -84,7 +85,11 @@ export function DisappearingTimeDialog(props: PropsType): JSX.Element {
text: i18n('DisappearingTimeDialog__set'),
style: 'affirmative',
action() {
onSubmit(unitValue * (UNIT_TO_MS.get(unit) || 1));
onSubmit(
DurationInSeconds.fromSeconds(
unitValue * (UNIT_TO_SEC.get(unit) ?? 1)
)
);
},
},
]}

View file

@ -5,6 +5,7 @@ import React, { useState } from 'react';
import { DisappearingTimerSelect } from './DisappearingTimerSelect';
import { setupI18n } from '../util/setupI18n';
import { DurationInSeconds } from '../util/durations';
import enMessages from '../../_locales/en/messages.json';
export default {
@ -23,7 +24,7 @@ const TimerSelectWrap: React.FC<Props> = ({ initialValue }) => {
return (
<DisappearingTimerSelect
i18n={i18n}
value={value}
value={DurationInSeconds.fromSeconds(value)}
onChange={newValue => setValue(newValue)}
/>
);

View file

@ -7,6 +7,7 @@ import classNames from 'classnames';
import type { LocalizerType } from '../types/Util';
import * as expirationTimer from '../util/expirationTimer';
import { DurationInSeconds } from '../util/durations';
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
import { Select } from './Select';
@ -16,17 +17,17 @@ const CSS_MODULE = 'module-disappearing-timer-select';
export type Props = {
i18n: LocalizerType;
value?: number;
onChange(value: number): void;
value?: DurationInSeconds;
onChange(value: DurationInSeconds): void;
};
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);
let expirationTimerOptions: ReadonlyArray<{
readonly value: number;
readonly value: DurationInSeconds;
readonly text: string;
}> = expirationTimer.DEFAULT_DURATIONS_IN_SECONDS.map(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);
const onSelectChange = (newValue: string) => {
const intValue = parseInt(newValue, 10);
const intValue = DurationInSeconds.fromSeconds(parseInt(newValue, 10));
if (intValue === -1) {
setIsModalOpen(true);
} else {
@ -54,7 +55,7 @@ export const DisappearingTimerSelect: React.FC<Props> = (props: Props) => {
expirationTimerOptions = [
...expirationTimerOptions,
{
value: -1,
value: DurationInSeconds.fromSeconds(-1),
text: i18n(
isCustomTimeSelected
? 'selectedCustomDisappearingTimeOption'

View file

@ -13,6 +13,7 @@ import { CrashReportDialog } from './CrashReportDialog';
import type { ConversationType } from '../state/ducks/conversations';
import { MessageSearchResult } from './conversationList/MessageSearchResult';
import { setupI18n } from '../util/setupI18n';
import { DurationInSeconds } from '../util/durations';
import enMessages from '../../_locales/en/messages.json';
import { ThemeType } from '../types/Util';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
@ -953,7 +954,7 @@ export const GroupMetadataNoTimer = (): JSX.Element => (
mode: LeftPaneMode.SetGroupMetadata,
groupAvatar: undefined,
groupName: 'Group 1',
groupExpireTimer: 0,
groupExpireTimer: DurationInSeconds.ZERO,
hasError: false,
isCreating: false,
isEditingAvatar: false,
@ -975,7 +976,7 @@ export const GroupMetadataRegularTimer = (): JSX.Element => (
mode: LeftPaneMode.SetGroupMetadata,
groupAvatar: undefined,
groupName: 'Group 1',
groupExpireTimer: 24 * 3600,
groupExpireTimer: DurationInSeconds.DAY,
hasError: false,
isCreating: false,
isEditingAvatar: false,
@ -997,7 +998,7 @@ export const GroupMetadataCustomTimer = (): JSX.Element => (
mode: LeftPaneMode.SetGroupMetadata,
groupAvatar: undefined,
groupName: 'Group 1',
groupExpireTimer: 7 * 3600,
groupExpireTimer: DurationInSeconds.fromHours(7),
hasError: false,
isCreating: false,
isEditingAvatar: false,

View file

@ -28,6 +28,7 @@ import { ScrollBehavior } from '../types/Util';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import { usePrevious } from '../hooks/usePrevious';
import { missingCaseError } from '../util/missingCaseError';
import type { DurationInSeconds } from '../util/durations';
import type { WidthBreakpoint } from './_util';
import { getConversationListWidthBreakpoint } from './_util';
import * as KeyboardLayout from '../services/keyboardLayout';
@ -106,7 +107,7 @@ export type PropsType = {
savePreferredLeftPaneWidth: (_: number) => void;
searchInConversation: (conversationId: string) => unknown;
setComposeGroupAvatar: (_: undefined | Uint8Array) => void;
setComposeGroupExpireTimer: (_: number) => void;
setComposeGroupExpireTimer: (_: DurationInSeconds) => void;
setComposeGroupName: (_: string) => void;
setComposeSearchTerm: (composeSearchTerm: string) => void;
showArchivedConversations: () => void;

View file

@ -12,6 +12,7 @@ import { DEFAULT_CONVERSATION_COLOR } from '../types/Colors';
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
import { objectMap } from '../util/objectMap';
import { DurationInSeconds } from '../util/durations';
const i18n = setupI18n('en', enMessages);
@ -107,7 +108,7 @@ const getDefaultArgs = (): PropsDataType => ({
selectedSpeaker: availableSpeakers[1],
shouldShowStoriesSettings: true,
themeSetting: 'system',
universalExpireTimer: 3600,
universalExpireTimer: DurationInSeconds.HOUR,
whoCanFindMe: PhoneNumberDiscoverability.Discoverable,
whoCanSeeMe: PhoneNumberSharingMode.Everybody,
zoomFactor: 1,
@ -186,7 +187,7 @@ BlockedMany.args = {
export const CustomUniversalExpireTimer = Template.bind({});
CustomUniversalExpireTimer.args = {
universalExpireTimer: 9000,
universalExpireTimer: DurationInSeconds.fromSeconds(9000),
};
CustomUniversalExpireTimer.story = {
name: 'Custom universalExpireTimer',

View file

@ -37,6 +37,7 @@ import {
DEFAULT_DURATIONS_SET,
format as formatExpirationTimer,
} from '../util/expirationTimer';
import { DurationInSeconds } from '../util/durations';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { useUniqueId } from '../hooks/useUniqueId';
import { useTheme } from '../hooks/useTheme';
@ -76,7 +77,7 @@ export type PropsDataType = {
selectedMicrophone?: AudioDevice;
selectedSpeaker?: AudioDevice;
themeSetting: ThemeSettingType;
universalExpireTimer: number;
universalExpireTimer: DurationInSeconds;
whoCanFindMe: PhoneNumberDiscoverability;
whoCanSeeMe: PhoneNumberSharingMode;
zoomFactor: ZoomFactorType;
@ -280,7 +281,7 @@ export const Preferences = ({
setGlobalDefaultConversationColor,
shouldShowStoriesSettings,
themeSetting,
universalExpireTimer = 0,
universalExpireTimer = DurationInSeconds.ZERO,
whoCanFindMe,
whoCanSeeMe,
zoomFactor,
@ -954,7 +955,7 @@ export const Preferences = ({
{
value: isCustomDisappearingMessageValue
? universalExpireTimer
: -1,
: DurationInSeconds.fromSeconds(-1),
text: isCustomDisappearingMessageValue
? formatExpirationTimer(i18n, universalExpireTimer)
: i18n('selectedCustomDisappearingTimeOption'),

View file

@ -13,9 +13,10 @@ import { Intl } from './Intl';
import { Modal } from './Modal';
import { SendStatus } from '../messages/MessageSendState';
import { Theme } from '../util/theme';
import { formatDateTimeLong } from '../util/timestamp';
import { DurationInSeconds } from '../util/durations';
import { ThemeType } from '../types/Util';
import { Time } from './Time';
import { formatDateTimeLong } from '../util/timestamp';
import { groupBy } from '../util/mapUtil';
import { format as formatRelativeTime } from '../util/expirationTimer';
@ -189,7 +190,7 @@ export const StoryDetailsModal = ({
}
const timeRemaining = expirationTimestamp
? expirationTimestamp - Date.now()
? DurationInSeconds.fromMillis(expirationTimestamp - Date.now())
: undefined;
return (
@ -254,7 +255,7 @@ export const StoryDetailsModal = ({
id="StoryDetailsModal__disappears-in"
components={[
<span className="StoryDetailsModal__debugger__button__text">
{formatRelativeTime(i18n, timeRemaining / 1000, {
{formatRelativeTime(i18n, timeRemaining, {
largest: 2,
})}
</span>,

View file

@ -9,6 +9,7 @@ import { action } from '@storybook/addon-actions';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
import { getRandomColor } from '../../test-both/helpers/getRandomColor';
import { setupI18n } from '../../util/setupI18n';
import { DurationInSeconds } from '../../util/durations';
import enMessages from '../../../_locales/en/messages.json';
import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext';
import {
@ -152,7 +153,7 @@ export const PrivateConvo = (): JSX.Element => {
phoneNumber: '(202) 555-0005',
type: 'direct',
id: '7',
expireTimer: 10,
expireTimer: DurationInSeconds.fromSeconds(10),
acceptedMessageRequest: true,
},
},
@ -165,7 +166,7 @@ export const PrivateConvo = (): JSX.Element => {
phoneNumber: '(202) 555-0005',
type: 'direct',
id: '8',
expireTimer: 300,
expireTimer: DurationInSeconds.fromSeconds(300),
acceptedMessageRequest: true,
isVerified: true,
canChangeTimer: true,
@ -231,7 +232,7 @@ export const Group = (): JSX.Element => {
phoneNumber: '',
id: '11',
type: 'group',
expireTimer: 10,
expireTimer: DurationInSeconds.fromSeconds(10),
acceptedMessageRequest: true,
outgoingCallButtonStyle: OutgoingCallButtonStyle.JustVideo,
},
@ -247,7 +248,7 @@ export const Group = (): JSX.Element => {
id: '12',
type: 'group',
left: true,
expireTimer: 10,
expireTimer: DurationInSeconds.fromSeconds(10),
acceptedMessageRequest: true,
outgoingCallButtonStyle: OutgoingCallButtonStyle.JustVideo,
},
@ -262,7 +263,7 @@ export const Group = (): JSX.Element => {
phoneNumber: '',
id: '13',
type: 'group',
expireTimer: 10,
expireTimer: DurationInSeconds.fromSeconds(10),
acceptedMessageRequest: true,
outgoingCallButtonStyle: OutgoingCallButtonStyle.Join,
},
@ -277,7 +278,7 @@ export const Group = (): JSX.Element => {
phoneNumber: '',
id: '14',
type: 'group',
expireTimer: 10,
expireTimer: DurationInSeconds.fromSeconds(10),
acceptedMessageRequest: true,
outgoingCallButtonStyle: OutgoingCallButtonStyle.JustVideo,
muteExpiresAt: Infinity,

View file

@ -28,6 +28,7 @@ import * as expirationTimer from '../../util/expirationTimer';
import { missingCaseError } from '../../util/missingCaseError';
import { isInSystemContacts } from '../../util/isInSystemContacts';
import { isConversationMuted } from '../../util/isConversationMuted';
import { DurationInSeconds } from '../../util/durations';
import {
useStartCallShortcuts,
useKeyboardShortcuts,
@ -79,7 +80,7 @@ export type PropsDataType = {
export type PropsActionsType = {
onSetMuteNotifications: (seconds: number) => void;
onSetDisappearingMessages: (seconds: number) => void;
onSetDisappearingMessages: (seconds: DurationInSeconds) => void;
onDeleteMessages: () => void;
onSearchInConversation: () => void;
onOutgoingAudioCallInConversation: () => void;
@ -406,8 +407,8 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
const expireDurations: ReadonlyArray<ReactNode> = [
...expirationTimer.DEFAULT_DURATIONS_IN_SECONDS,
-1,
].map((seconds: number) => {
DurationInSeconds.fromSeconds(-1),
].map(seconds => {
let text: string;
if (seconds === -1) {

View file

@ -24,6 +24,7 @@ import { SendStatus } from '../../messages/MessageSendState';
import { WidthBreakpoint } from '../_util';
import * as log from '../../logging/log';
import { formatDateTimeLong } from '../../util/timestamp';
import { DurationInSeconds } from '../../util/durations';
import { format as formatRelativeTime } from '../../util/expirationTimer';
export type Contact = Pick<
@ -302,7 +303,7 @@ export class MessageDetail extends React.Component<Props> {
} = this.props;
const timeRemaining = expirationTimestamp
? expirationTimestamp - Date.now()
? DurationInSeconds.fromMillis(expirationTimestamp - Date.now())
: undefined;
return (
@ -422,7 +423,7 @@ export class MessageDetail extends React.Component<Props> {
{i18n('MessageDetail--disappears-in')}
</td>
<td>
{formatRelativeTime(i18n, timeRemaining / 1000, {
{formatRelativeTime(i18n, timeRemaining, {
largest: 2,
})}
</td>

View file

@ -2,13 +2,13 @@
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as moment from 'moment';
import { times } from 'lodash';
import { v4 as uuid } from 'uuid';
import { text, boolean, number } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { setupI18n } from '../../util/setupI18n';
import { DurationInSeconds } from '../../util/durations';
import enMessages from '../../../_locales/en/messages.json';
import type { PropsType } from './Timeline';
import { Timeline } from './Timeline';
@ -135,7 +135,7 @@ const items: Record<string, TimelineItemType> = {
type: 'timerNotification',
data: {
disabled: false,
expireTimer: moment.duration(2, 'hours').asSeconds(),
expireTimer: DurationInSeconds.fromHours(2),
title: "It's Me",
type: 'fromMe',
},
@ -145,7 +145,7 @@ const items: Record<string, TimelineItemType> = {
type: 'timerNotification',
data: {
disabled: false,
expireTimer: moment.duration(2, 'hours').asSeconds(),
expireTimer: DurationInSeconds.fromHours(2),
title: '(202) 555-0000',
type: 'fromOther',
},

View file

@ -7,6 +7,7 @@ import { action } from '@storybook/addon-actions';
import { EmojiPicker } from '../emoji/EmojiPicker';
import { setupI18n } from '../../util/setupI18n';
import { DurationInSeconds } from '../../util/durations';
import enMessages from '../../../_locales/en/messages.json';
import type { PropsType as TimelineItemProps } from './TimelineItem';
import { TimelineItem } from './TimelineItem';
@ -43,7 +44,10 @@ const renderContact = (conversationId: string) => (
);
const renderUniversalTimerNotification = () => (
<UniversalTimerNotification i18n={i18n} expireTimer={3600} />
<UniversalTimerNotification
i18n={i18n}
expireTimer={DurationInSeconds.HOUR}
/>
);
const getDefaultProps = () => ({
@ -138,7 +142,7 @@ export const Notification = (): JSX.Element => {
type: 'timerNotification',
data: {
phoneNumber: '(202) 555-0000',
expireTimer: 60,
expireTimer: DurationInSeconds.MINUTE,
...getDefaultConversation(),
type: 'fromOther',
},

View file

@ -2,10 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as moment from 'moment';
import { boolean, number, select, text } from '@storybook/addon-knobs';
import { setupI18n } from '../../util/setupI18n';
import { DurationInSeconds } from '../../util/durations';
import enMessages from '../../../_locales/en/messages.json';
import type { Props } from './TimerNotification';
import { TimerNotification } from './TimerNotification';
@ -34,16 +34,19 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
}
: {
disabled: false,
expireTimer: number(
'expireTimer',
('expireTimer' in overrideProps ? overrideProps.expireTimer : 0) || 0
expireTimer: DurationInSeconds.fromMillis(
number(
'expireTimer',
('expireTimer' in overrideProps ? overrideProps.expireTimer : 0) ||
0
)
),
}),
});
export const SetByOther = (): JSX.Element => {
const props = createProps({
expireTimer: moment.duration(1, 'hour').asSeconds(),
expireTimer: DurationInSeconds.fromHours(1),
type: 'fromOther',
title: 'Mr. Fire',
});
@ -61,7 +64,7 @@ export const SetByOtherWithALongName = (): JSX.Element => {
const longName = '🦴🧩📴'.repeat(50);
const props = createProps({
expireTimer: moment.duration(1, 'hour').asSeconds(),
expireTimer: DurationInSeconds.fromHours(1),
type: 'fromOther',
title: longName,
});
@ -81,7 +84,7 @@ SetByOtherWithALongName.story = {
export const SetByYou = (): JSX.Element => {
const props = createProps({
expireTimer: moment.duration(1, 'hour').asSeconds(),
expireTimer: DurationInSeconds.fromHours(1),
type: 'fromMe',
title: 'Mr. Fire',
});
@ -97,7 +100,7 @@ export const SetByYou = (): JSX.Element => {
export const SetBySync = (): JSX.Element => {
const props = createProps({
expireTimer: moment.duration(1, 'hour').asSeconds(),
expireTimer: DurationInSeconds.fromHours(1),
type: 'fromSync',
title: 'Mr. Fire',
});

View file

@ -9,6 +9,7 @@ import { SystemMessage } from './SystemMessage';
import { Intl } from '../Intl';
import type { LocalizerType } from '../../types/Util';
import * as expirationTimer from '../../util/expirationTimer';
import type { DurationInSeconds } from '../../util/durations';
import * as log from '../../logging/log';
export type TimerNotificationType =
@ -27,7 +28,7 @@ export type PropsData = {
| { disabled: true }
| {
disabled: false;
expireTimer: number;
expireTimer: DurationInSeconds;
}
);

View file

@ -18,34 +18,34 @@ const i18n = setupI18n('en', enMessages);
export const Seconds = (): JSX.Element => (
<UniversalTimerNotification
i18n={i18n}
expireTimer={EXPIRE_TIMERS[0].value / 1000}
expireTimer={EXPIRE_TIMERS[0].value}
/>
);
export const Minutes = (): JSX.Element => (
<UniversalTimerNotification
i18n={i18n}
expireTimer={EXPIRE_TIMERS[1].value / 1000}
expireTimer={EXPIRE_TIMERS[1].value}
/>
);
export const Hours = (): JSX.Element => (
<UniversalTimerNotification
i18n={i18n}
expireTimer={EXPIRE_TIMERS[2].value / 1000}
expireTimer={EXPIRE_TIMERS[2].value}
/>
);
export const Days = (): JSX.Element => (
<UniversalTimerNotification
i18n={i18n}
expireTimer={EXPIRE_TIMERS[3].value / 1000}
expireTimer={EXPIRE_TIMERS[3].value}
/>
);
export const Weeks = (): JSX.Element => (
<UniversalTimerNotification
i18n={i18n}
expireTimer={EXPIRE_TIMERS[4].value / 1000}
expireTimer={EXPIRE_TIMERS[4].value}
/>
);

View file

@ -6,10 +6,11 @@ import React from 'react';
import { SystemMessage } from './SystemMessage';
import type { LocalizerType } from '../../types/Util';
import * as expirationTimer from '../../util/expirationTimer';
import type { DurationInSeconds } from '../../util/durations';
export type Props = {
i18n: LocalizerType;
expireTimer: number;
expireTimer: DurationInSeconds;
};
export const UniversalTimerNotification: React.FC<Props> = props => {

View file

@ -16,6 +16,7 @@ import type { ConversationType } from '../../../state/ducks/conversations';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
import { makeFakeLookupConversationWithoutUuid } from '../../../test-both/helpers/fakeLookupConversationWithoutUuid';
import { ThemeType } from '../../../types/Util';
import { DurationInSeconds } from '../../../util/durations';
const i18n = setupI18n('en', enMessages);
@ -35,7 +36,10 @@ const conversation: ConversationType = getDefaultConversation({
const allCandidateContacts = times(10, () => getDefaultConversation());
const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
const createProps = (
hasGroupLink = false,
expireTimer?: DurationInSeconds
): Props => ({
addMembers: async () => {
action('addMembers');
},
@ -194,7 +198,7 @@ export const GroupEditable = (): 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 />;
};

View file

@ -20,6 +20,7 @@ import type { LocalizerType, ThemeType } from '../../../types/Util';
import type { MediaItemType } from '../../../types/MediaItem';
import type { BadgeType } from '../../../badges/types';
import { missingCaseError } from '../../../util/missingCaseError';
import { DurationInSeconds } from '../../../util/durations';
import { DisappearingTimerSelect } from '../../DisappearingTimerSelect';
@ -79,7 +80,7 @@ export type StateProps = {
memberships: Array<GroupV2Membership>;
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
setDisappearingMessages: (seconds: number) => void;
setDisappearingMessages: (seconds: DurationInSeconds) => void;
showAllMedia: () => void;
showChatColorEditor: () => void;
showGroupLinkManagement: () => void;
@ -410,7 +411,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
right={
<DisappearingTimerSelect
i18n={i18n}
value={conversation.expireTimer || 0}
value={conversation.expireTimer || DurationInSeconds.ZERO}
onChange={setDisappearingMessages}
/>
}

View file

@ -10,6 +10,7 @@ import type {
ReplaceAvatarActionType,
SaveAvatarToDiskActionType,
} from '../../types/Avatar';
import type { DurationInSeconds } from '../../util/durations';
import type { ShowConversationType } from '../../state/ducks/conversations';
export enum FindDirection {
@ -73,7 +74,7 @@ export abstract class LeftPaneHelper<T> {
i18n: LocalizerType;
removeSelectedContact: (_: string) => unknown;
setComposeGroupAvatar: (_: undefined | Uint8Array) => unknown;
setComposeGroupExpireTimer: (_: number) => void;
setComposeGroupExpireTimer: (_: DurationInSeconds) => void;
setComposeGroupName: (_: string) => unknown;
toggleComposeEditingAvatar: () => unknown;
}>

View file

@ -10,6 +10,7 @@ import { RowType } from '../ConversationList';
import type { ContactListItemConversationType } from '../conversationList/ContactListItem';
import { DisappearingTimerSelect } from '../DisappearingTimerSelect';
import type { LocalizerType } from '../../types/Util';
import type { DurationInSeconds } from '../../util/durations';
import { Alert } from '../Alert';
import { AvatarEditor } from '../AvatarEditor';
import { AvatarPreview } from '../AvatarPreview';
@ -28,7 +29,7 @@ import { AvatarColors } from '../../types/Colors';
export type LeftPaneSetGroupMetadataPropsType = {
groupAvatar: undefined | Uint8Array;
groupName: string;
groupExpireTimer: number;
groupExpireTimer: DurationInSeconds;
hasError: boolean;
isCreating: boolean;
isEditingAvatar: boolean;
@ -41,7 +42,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
private readonly groupName: string;
private readonly groupExpireTimer: number;
private readonly groupExpireTimer: DurationInSeconds;
private readonly hasError: boolean;
@ -128,7 +129,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
createGroup: () => unknown;
i18n: LocalizerType;
setComposeGroupAvatar: (_: undefined | Uint8Array) => unknown;
setComposeGroupExpireTimer: (_: number) => void;
setComposeGroupExpireTimer: (_: DurationInSeconds) => void;
setComposeGroupName: (_: string) => unknown;
toggleComposeEditingAvatar: () => unknown;
}>): ReactChild {

View file

@ -23,7 +23,7 @@ import dataInterface from './sql/Client';
import { toWebSafeBase64, fromWebSafeBase64 } from './util/webSafeBase64';
import { assertDev, strictAssert } from './util/assert';
import { isMoreRecentThan } from './util/timestamp';
import * as durations from './util/durations';
import { MINUTE, DurationInSeconds } from './util/durations';
import { normalizeUuid } from './util/normalizeUuid';
import { dropNull } from './util/dropNull';
import type {
@ -854,7 +854,7 @@ export function buildDisappearingMessagesTimerChange({
expireTimer,
group,
}: {
expireTimer: number;
expireTimer: DurationInSeconds;
group: ConversationAttributesType;
}): Proto.GroupChange.Actions {
const actions = new Proto.GroupChange.Actions();
@ -1458,7 +1458,7 @@ export async function modifyGroupV2({
}
const startTime = Date.now();
const timeoutTime = startTime + durations.MINUTE;
const timeoutTime = startTime + MINUTE;
const MAX_ATTEMPTS = 5;
@ -1725,7 +1725,7 @@ export async function createGroupV2(
options: Readonly<{
name: string;
avatar: undefined | Uint8Array;
expireTimer: undefined | number;
expireTimer: undefined | DurationInSeconds;
conversationIds: Array<string>;
avatars?: Array<AvatarDataType>;
refreshedCredentials?: boolean;
@ -2904,7 +2904,7 @@ type MaybeUpdatePropsType = Readonly<{
groupChange?: WrappedGroupChangeType;
}>;
const FIVE_MINUTES = 5 * durations.MINUTE;
const FIVE_MINUTES = 5 * MINUTE;
export async function waitThenMaybeUpdateGroup(
options: MaybeUpdatePropsType,
@ -4625,7 +4625,7 @@ function extractDiffs({
Boolean(current.expireTimer) &&
old.expireTimer !== current.expireTimer)
) {
const expireTimer = current.expireTimer || 0;
const expireTimer = current.expireTimer || DurationInSeconds.ZERO;
log.info(
`extractDiffs/${logId}: generating change notifcation for new ${expireTimer} timer`
);
@ -4977,9 +4977,9 @@ async function applyGroupChange({
disappearingMessagesTimer &&
disappearingMessagesTimer.content === 'disappearingMessagesDuration'
) {
result.expireTimer = dropNull(
disappearingMessagesTimer.disappearingMessagesDuration
);
const duration = disappearingMessagesTimer.disappearingMessagesDuration;
result.expireTimer =
duration == null ? undefined : DurationInSeconds.fromSeconds(duration);
} else {
log.warn(
`applyGroupChange/${logId}: Clearing group expireTimer due to missing data.`
@ -5335,9 +5335,9 @@ async function applyGroupState({
disappearingMessagesTimer &&
disappearingMessagesTimer.content === 'disappearingMessagesDuration'
) {
result.expireTimer = dropNull(
disappearingMessagesTimer.disappearingMessagesDuration
);
const duration = disappearingMessagesTimer.disappearingMessagesDuration;
result.expireTimer =
duration == null ? undefined : DurationInSeconds.fromSeconds(duration);
} else {
result.expireTimer = undefined;
}

View file

@ -19,6 +19,7 @@ import type {
import { handleMessageSend } from '../../util/handleMessageSend';
import { isConversationAccepted } from '../../util/isConversationAccepted';
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import { DurationInSeconds } from '../../util/durations';
export async function sendDirectExpirationTimerUpdate(
conversation: ConversationModel,
@ -77,7 +78,11 @@ export async function sendDirectExpirationTimerUpdate(
const sendType = 'expirationTimerUpdate';
const flags = Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
const proto = await messaging.getContentMessage({
expireTimer,
// `expireTimer` is already in seconds
expireTimer:
expireTimer === undefined
? undefined
: DurationInSeconds.fromSeconds(expireTimer),
flags,
profileKey,
recipients: conversation.getRecipients(),

View file

@ -36,6 +36,7 @@ import { ourProfileKeyService } from '../../services/ourProfileKey';
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import { isConversationAccepted } from '../../util/isConversationAccepted';
import { sendToGroup } from '../../util/sendToGroup';
import type { DurationInSeconds } from '../../util/durations';
import type { UUIDStringType } from '../../types/UUID';
export async function sendNormalMessage(
@ -466,7 +467,7 @@ async function getMessageSendData({
body: undefined | string;
contact?: Array<ContactWithHydratedAvatar>;
deletedForEveryoneTimestamp: undefined | number;
expireTimer: undefined | number;
expireTimer: undefined | DurationInSeconds;
mentions: undefined | BodyRangesType;
messageTimestamp: number;
preview: Array<LinkPreviewType>;

7
ts/model-types.d.ts vendored
View file

@ -32,6 +32,7 @@ import type { LinkPreviewType } from './types/message/LinkPreviews';
import type { StickerType } from './types/Stickers';
import type { StorySendMode } from './types/Stories';
import type { MIMEType } from './types/MIME';
import type { DurationInSeconds } from './util/durations';
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
import MemberRoleEnum = Proto.Member.Role;
@ -130,7 +131,7 @@ export type MessageAttributesType = {
deletedForEveryoneTimestamp?: number;
errors?: Array<CustomError>;
expirationStartTimestamp?: number | null;
expireTimer?: number;
expireTimer?: DurationInSeconds;
groupMigration?: GroupMigrationType;
group_update?: GroupV1Update;
hasAttachments?: boolean | 0 | 1;
@ -198,7 +199,7 @@ export type MessageAttributesType = {
};
expirationTimerUpdate?: {
expireTimer: number;
expireTimer?: DurationInSeconds;
fromSync?: unknown;
source?: string;
sourceUuid?: string;
@ -381,7 +382,7 @@ export type ConversationAttributesType = {
} | null;
avatars?: Array<AvatarDataType>;
description?: string;
expireTimer?: number;
expireTimer?: DurationInSeconds;
membersV2?: Array<GroupV2MemberType>;
pendingMembersV2?: Array<GroupV2PendingMemberType>;
pendingAdminApprovalV2?: Array<GroupV2PendingAdminApprovalType>;

View file

@ -80,7 +80,7 @@ import { updateConversationsWithUuidLookup } from '../updateConversationsWithUui
import { ReadStatus } from '../messages/MessageReadStatus';
import { SendStatus } from '../messages/MessageSendState';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import * as durations from '../util/durations';
import { MINUTE, DurationInSeconds } from '../util/durations';
import {
concat,
filter,
@ -155,7 +155,7 @@ const {
getNewerMessagesByConversation,
} = window.Signal.Data;
const FIVE_MINUTES = durations.MINUTE * 5;
const FIVE_MINUTES = MINUTE * 5;
const JOB_REPORTING_THRESHOLD_MS = 25;
const SEND_REPORTING_THRESHOLD_MS = 25;
@ -471,7 +471,7 @@ export class ConversationModel extends window.Backbone
}
async updateExpirationTimerInGroupV2(
seconds?: number
seconds?: DurationInSeconds
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
const current = this.get('expireTimer');
@ -485,7 +485,7 @@ export class ConversationModel extends window.Backbone
}
return window.Signal.Groups.buildDisappearingMessagesTimerChange({
expireTimer: seconds || 0,
expireTimer: seconds || DurationInSeconds.ZERO,
group: this.attributes,
});
}
@ -1382,7 +1382,7 @@ export class ConversationModel extends window.Backbone
if (!this.newMessageQueue) {
this.newMessageQueue = new PQueue({
concurrency: 1,
timeout: durations.MINUTE * 30,
timeout: MINUTE * 30,
});
}
@ -3944,7 +3944,7 @@ export class ConversationModel extends window.Backbone
);
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
// with the parent story's expiration.
@ -3952,7 +3952,7 @@ export class ConversationModel extends window.Backbone
const parentStory = await getMessageById(storyId);
expirationStartTimestamp =
parentStory?.expirationStartTimestamp || Date.now();
expireTimer = parentStory?.expireTimer || durations.DAY;
expireTimer = parentStory?.expireTimer || DurationInSeconds.DAY;
} else {
await this.maybeApplyUniversalTimer();
expireTimer = this.get('expireTimer');
@ -4431,7 +4431,7 @@ export class ConversationModel extends window.Backbone
}
async updateExpirationTimer(
providedExpireTimer: number | undefined,
providedExpireTimer: DurationInSeconds | undefined,
{
reason,
receivedAt,
@ -4479,7 +4479,7 @@ export class ConversationModel extends window.Backbone
);
}
let expireTimer: number | undefined = providedExpireTimer;
let expireTimer: DurationInSeconds | undefined = providedExpireTimer;
let source = providedSource;
if (this.get('left')) {
return false;

View file

@ -172,7 +172,7 @@ import { parseBoostBadgeListFromServer } from '../badges/parseBadgesFromServer';
import { GiftBadgeStates } from '../components/conversation/Message';
import { downloadAttachment } from '../util/downloadAttachment';
import type { StickerWithHydratedData } from '../types/Stickers';
import { SECOND } from '../util/durations';
import { DurationInSeconds } from '../util/durations';
import dataInterface from '../sql/Client';
function isSameUuid(
@ -566,7 +566,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const expireTimer = this.get('expireTimer');
const expirationStartTimestamp = this.get('expirationStartTimestamp');
const expirationLength = isNumber(expireTimer)
? expireTimer * SECOND
? DurationInSeconds.toMillis(expireTimer)
: undefined;
const expirationTimestamp = expirationTimer.calculateExpirationTimestamp({
expireTimer,

View file

@ -39,6 +39,7 @@ import {
} from '../util/universalExpireTimer';
import { ourProfileKeyService } from './ourProfileKey';
import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation';
import { DurationInSeconds } from '../util/durations';
import { isValidUuid, UUID, UUIDKind } from '../types/UUID';
import * as preferredReactionEmoji from '../reactions/preferredReactionEmoji';
import { SignalService as Proto } from '../protobuf';
@ -1178,7 +1179,9 @@ export async function mergeAccountRecord(
window.storage.put('preferredReactionEmoji', rawPreferredReactionEmoji);
}
setUniversalExpireTimer(universalExpireTimer || 0);
setUniversalExpireTimer(
DurationInSeconds.fromSeconds(universalExpireTimer || 0)
);
const PHONE_NUMBER_SHARING_MODE_ENUM =
Proto.AccountRecord.PhoneNumberSharingMode;

View file

@ -15,6 +15,7 @@ import type { LinkPreviewType } from '../types/message/LinkPreviews';
import { isNotNil } from '../util/isNotNil';
import { strictAssert } from '../util/assert';
import { dropNull } from '../util/dropNull';
import { DurationInSeconds } from '../util/durations';
import { isGroup } from '../util/whatTypeOfConversation';
import { SIGNAL_ACI } from '../types/SignalConversation';
@ -142,7 +143,7 @@ export function getStoriesForRedux(): Array<StoryDataType> {
async function repairUnexpiredStories(): Promise<void> {
strictAssert(storyData, 'Could not load stories');
const DAY_AS_SECONDS = durations.DAY / 1000;
const DAY_AS_SECONDS = DurationInSeconds.fromDays(1);
const storiesWithExpiry = storyData
.filter(
@ -155,9 +156,11 @@ async function repairUnexpiredStories(): Promise<void> {
.map(story => ({
...story,
expirationStartTimestamp: Math.min(story.timestamp, Date.now()),
expireTimer: Math.min(
Math.floor((story.timestamp + durations.DAY - Date.now()) / 1000),
DAY_AS_SECONDS
expireTimer: DurationInSeconds.fromMillis(
Math.min(
Math.floor(story.timestamp + durations.DAY - Date.now()),
durations.DAY
)
),
}));

View file

@ -20,6 +20,7 @@ import * as log from '../../logging/log';
import { calling } from '../../services/calling';
import { getOwn } from '../../util/getOwn';
import { assertDev, strictAssert } from '../../util/assert';
import type { DurationInSeconds } from '../../util/durations';
import * as universalExpireTimer from '../../util/universalExpireTimer';
import type {
ShowSendAnywayDialogActionType,
@ -178,7 +179,7 @@ export type ConversationType = {
accessControlMembers?: number;
announcementsOnly?: boolean;
announcementsOnlyReady?: boolean;
expireTimer?: number;
expireTimer?: DurationInSeconds;
memberships?: Array<{
uuid: UUIDStringType;
isAdmin: boolean;
@ -293,7 +294,7 @@ export type PreJoinConversationType = {
type ComposerGroupCreationState = {
groupAvatar: undefined | Uint8Array;
groupName: string;
groupExpireTimer: number;
groupExpireTimer: DurationInSeconds;
maximumGroupSizeModalState: OneTimeModalState;
recommendedGroupSizeModalState: OneTimeModalState;
selectedConversationIds: Array<string>;
@ -712,7 +713,7 @@ type SetComposeGroupNameActionType = {
};
type SetComposeGroupExpireTimerActionType = {
type: 'SET_COMPOSE_GROUP_EXPIRE_TIMER';
payload: { groupExpireTimer: number };
payload: { groupExpireTimer: DurationInSeconds };
};
type SetComposeSearchTermActionType = {
type: 'SET_COMPOSE_SEARCH_TERM';
@ -1907,7 +1908,7 @@ function setComposeGroupName(groupName: string): SetComposeGroupNameActionType {
}
function setComposeGroupExpireTimer(
groupExpireTimer: number
groupExpireTimer: DurationInSeconds
): SetComposeGroupExpireTimerActionType {
return {
type: 'SET_COMPOSE_GROUP_EXPIRE_TIMER',
@ -3509,7 +3510,7 @@ export function reducer(
let maximumGroupSizeModalState: OneTimeModalState;
let groupName: string;
let groupAvatar: undefined | Uint8Array;
let groupExpireTimer: number;
let groupExpireTimer: DurationInSeconds;
let userAvatarData = getDefaultAvatars(true);
switch (state.composer?.step) {

View file

@ -36,6 +36,7 @@ import { markViewed } from '../../services/MessageUpdater';
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
import { replaceIndex } from '../../util/replaceIndex';
import { showToast } from '../../util/showToast';
import type { DurationInSeconds } from '../../util/durations';
import { hasFailed, isDownloaded, isDownloading } from '../../types/Attachment';
import {
getConversationSelector,
@ -79,7 +80,7 @@ export type StoryDataType = {
| 'type'
> & {
// don't want the fields to be optional as in MessageAttributesType
expireTimer: number | undefined;
expireTimer: DurationInSeconds | undefined;
expirationStartTimestamp: number | undefined;
};

View file

@ -38,6 +38,7 @@ import type { UUIDStringType } from '../../types/UUID';
import { isInSystemContacts } from '../../util/isInSystemContacts';
import { isSignalConnection } from '../../util/getSignalConnections';
import { sortByTitle } from '../../util/sortByTitle';
import { DurationInSeconds } from '../../util/durations';
import {
isDirectConversation,
isGroupV1,
@ -634,7 +635,7 @@ const getGroupCreationComposerState = createSelector(
): {
groupName: string;
groupAvatar: undefined | Uint8Array;
groupExpireTimer: number;
groupExpireTimer: DurationInSeconds;
selectedConversationIds: Array<string>;
} => {
switch (composerState?.step) {
@ -649,7 +650,7 @@ const getGroupCreationComposerState = createSelector(
return {
groupName: '',
groupAvatar: undefined,
groupExpireTimer: 0,
groupExpireTimer: DurationInSeconds.ZERO,
selectedConversationIds: [],
};
}
@ -668,7 +669,7 @@ export const getComposeGroupName = createSelector(
export const getComposeGroupExpireTimer = createSelector(
getGroupCreationComposerState,
(composerState): number => composerState.groupExpireTimer
(composerState): DurationInSeconds => composerState.groupExpireTimer
);
export const getComposeSelectedContacts = createSelector(

View file

@ -17,6 +17,7 @@ import type { UUIDStringType } from '../../types/UUID';
import { DEFAULT_CONVERSATION_COLOR } from '../../types/Colors';
import { getPreferredReactionEmoji as getPreferredReactionEmojiFromStoredValue } from '../../reactions/preferredReactionEmoji';
import { isBeta } from '../../util/version';
import { DurationInSeconds } from '../../util/durations';
import { getUserNumber, getUserACI } from './user';
const DEFAULT_PREFERRED_LEFT_PANE_WIDTH = 320;
@ -42,7 +43,8 @@ export const getPinnedConversationIds = createSelector(
export const getUniversalExpireTimer = createSelector(
getItems,
(state: ItemsStateType): number => state[UNIVERSAL_EXPIRE_TIMER_ITEM] || 0
(state: ItemsStateType): DurationInSeconds =>
DurationInSeconds.fromSeconds(state[UNIVERSAL_EXPIRE_TIMER_ITEM] || 0)
);
const isRemoteConfigFlagEnabled = (

View file

@ -93,7 +93,7 @@ import {
} from '../../messages/MessageSendState';
import * as log from '../../logging/log';
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 { isIncoming, isOutgoing, isStory } from '../../messages/helpers';
import { calculateExpirationTimestamp } from '../../util/expirationTimer';
@ -628,7 +628,9 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
}: GetPropsForMessageOptions
): ShallowPropsType => {
const { expireTimer, expirationStartTimestamp, conversationId } = message;
const expirationLength = expireTimer ? expireTimer * SECOND : undefined;
const expirationLength = expireTimer
? DurationInSeconds.toMillis(expireTimer)
: undefined;
const conversation = getConversation(message, conversationSelector);
const isGroup = conversation.type === 'group';
@ -1107,10 +1109,26 @@ function getPropsForTimerNotification(
const sourceId = sourceUuid || source;
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 = {
...formattedContact,
disabled,
expireTimer,
...maybeExpireTimer,
type: 'fromOther' as const,
};

View file

@ -24,6 +24,7 @@ import {
getPreferredBadgeSelector,
} from '../selectors/badges';
import { assertDev } from '../../util/assert';
import type { DurationInSeconds } from '../../util/durations';
import { SignalService as Proto } from '../../protobuf';
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
import type { SmartChooseGroupMembersModalPropsType } from './ChooseGroupMembersModal';
@ -39,7 +40,7 @@ export type SmartConversationDetailsProps = {
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
conversationId: string;
loadRecentMediaItems: (limit: number) => void;
setDisappearingMessages: (seconds: number) => void;
setDisappearingMessages: (seconds: DurationInSeconds) => void;
showAllMedia: () => void;
showChatColorEditor: () => void;
showGroupLinkManagement: () => void;

View file

@ -25,6 +25,7 @@ import { mapDispatchToProps } from '../actions';
import { missingCaseError } from '../../util/missingCaseError';
import { strictAssert } from '../../util/assert';
import { isSignalConversation } from '../../util/isSignalConversation';
import type { DurationInSeconds } from '../../util/durations';
export type OwnProps = {
id: string;
@ -37,7 +38,7 @@ export type OwnProps = {
onOutgoingAudioCallInConversation: () => void;
onOutgoingVideoCallInConversation: () => void;
onSearchInConversation: () => void;
onSetDisappearingMessages: (seconds: number) => void;
onSetDisappearingMessages: (seconds: DurationInSeconds) => void;
onSetMuteNotifications: (seconds: number) => void;
onSetPin: (value: boolean) => void;
onShowAllMedia: () => void;

View file

@ -3,6 +3,7 @@
import { ComposerStep } from '../../state/ducks/conversationsEnums';
import { OneTimeModalState } from '../../groups/toggleSelectedContactForGroupAddition';
import { DurationInSeconds } from '../../util/durations';
export const defaultStartDirectConversationComposerState = {
step: ComposerStep.StartDirectConversation as const,
@ -16,7 +17,7 @@ export const defaultChooseGroupMembersComposerState = {
uuidFetchState: {},
groupAvatar: undefined,
groupName: '',
groupExpireTimer: 0,
groupExpireTimer: DurationInSeconds.ZERO,
maximumGroupSizeModalState: OneTimeModalState.NeverShown,
recommendedGroupSizeModalState: OneTimeModalState.NeverShown,
selectedConversationIds: [],
@ -28,7 +29,7 @@ export const defaultSetGroupMetadataComposerState = {
isEditingAvatar: false,
groupAvatar: undefined,
groupName: '',
groupExpireTimer: 0,
groupExpireTimer: DurationInSeconds.ZERO,
maximumGroupSizeModalState: OneTimeModalState.NeverShown,
recommendedGroupSizeModalState: OneTimeModalState.NeverShown,
selectedConversationIds: [],

View file

@ -4,6 +4,7 @@
import { assert } from 'chai';
import * as moment from 'moment';
import { setupI18n } from '../../util/setupI18n';
import { DurationInSeconds } from '../../util/durations';
import enMessages from '../../../_locales/en/messages.json';
import esMessages from '../../../_locales/es/messages.json';
import nbMessages from '../../../_locales/nb/messages.json';
@ -24,7 +25,7 @@ describe('expiration timer utilities', () => {
});
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);
});
});
@ -37,7 +38,7 @@ describe('expiration timer utilities', () => {
});
it('handles no duration', () => {
assert.strictEqual(format(i18n, 0), 'off');
assert.strictEqual(format(i18n, DurationInSeconds.ZERO), 'off');
});
it('formats durations', () => {
@ -59,22 +60,31 @@ describe('expiration timer utilities', () => {
[moment.duration(3, 'w').asSeconds(), '3 weeks'],
[moment.duration(52, 'w').asSeconds(), '52 weeks'],
]).forEach((expected, input) => {
assert.strictEqual(format(i18n, input), expected);
assert.strictEqual(
format(i18n, DurationInSeconds.fromSeconds(input)),
expected
);
});
});
it('formats other languages successfully', () => {
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);
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
// what we're testing here.
const ptBrI18n = setupI18n('pt_BR', ptBrMessages);
assert.strictEqual(
format(ptBrI18n, moment.duration(5, 'days').asSeconds()),
format(ptBrI18n, DurationInSeconds.fromDays(5)),
'5 dias'
);
@ -83,7 +93,7 @@ describe('expiration timer utilities', () => {
[setupI18n('nb', nbMessages), setupI18n('nn', nlMessages)].forEach(
norwegianI18n => {
assert.strictEqual(
format(norwegianI18n, moment.duration(6, 'hours').asSeconds()),
format(norwegianI18n, DurationInSeconds.fromHours(6)),
'6 timer'
);
}
@ -92,41 +102,76 @@ describe('expiration timer utilities', () => {
it('falls back to English if the locale is not supported', () => {
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', () => {
// We don't expect there to be a "mix" of units, but we shouldn't choke if a bad
// client gives us an unexpected timestamp.
const mix = moment
.duration(6, 'days')
.add(moment.duration(2, 'hours'))
.asSeconds();
const mix = DurationInSeconds.fromSeconds(
moment.duration(6, 'days').add(moment.duration(2, 'hours')).asSeconds()
);
assert.strictEqual(format(i18n, mix), '6 days, 2 hours');
});
it('handles negative numbers gracefully', () => {
// The proto helps enforce non-negative numbers by specifying a u32, but because
// JavaScript lacks such a type, we test it here.
assert.strictEqual(format(i18n, -1), '1 second');
assert.strictEqual(format(i18n, -120), '2 minutes');
assert.strictEqual(format(i18n, -0), 'off');
assert.strictEqual(
format(i18n, DurationInSeconds.fromSeconds(-1)),
'1 second'
);
assert.strictEqual(
format(i18n, DurationInSeconds.fromSeconds(-120)),
'2 minutes'
);
assert.strictEqual(
format(i18n, DurationInSeconds.fromSeconds(-0)),
'off'
);
});
it('handles fractional seconds gracefully', () => {
// The proto helps enforce integer numbers by specifying a u32, but this function
// shouldn't choke if bad data is passed somehow.
assert.strictEqual(format(i18n, 4.2), '4 seconds');
assert.strictEqual(format(i18n, 4.8), '4 seconds');
assert.strictEqual(format(i18n, 0.2), '1 second');
assert.strictEqual(format(i18n, 0.8), '1 second');
assert.strictEqual(
format(i18n, DurationInSeconds.fromSeconds(4.2)),
'4 seconds'
);
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
// shouldn't explode.
assert.strictEqual(format(i18n, -4.2), '4 seconds');
assert.strictEqual(format(i18n, -4.8), '4 seconds');
assert.strictEqual(format(i18n, -0.2), '1 second');
assert.strictEqual(format(i18n, -0.8), '1 second');
assert.strictEqual(
format(i18n, DurationInSeconds.fromSeconds(-4.2)),
'4 seconds'
);
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'
);
});
});
});

View file

@ -2,8 +2,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
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> = [
{ 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: 6 * durations.DAY, label: '6 days' },
{ value: 3 * durations.WEEK, label: '3 weeks' },
];
].map(({ value, label }) => {
return {
value: DurationInSeconds.fromMillis(value),
label,
};
});

View file

@ -6,6 +6,7 @@ import { assert } from 'chai';
import dataInterface from '../../sql/Client';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import { DurationInSeconds } from '../../util/durations';
import type { MessageAttributesType } from '../../model-types.d';
@ -342,7 +343,7 @@ describe('sql/conversationSummary', () => {
type: 'outgoing',
conversationId,
expirationTimerUpdate: {
expireTimer: 10,
expireTimer: DurationInSeconds.fromSeconds(10),
source: 'you',
},
sent_at: now + 1,
@ -355,7 +356,7 @@ describe('sql/conversationSummary', () => {
type: 'outgoing',
conversationId,
expirationTimerUpdate: {
expireTimer: 10,
expireTimer: DurationInSeconds.fromSeconds(10),
fromSync: true,
},
sent_at: now + 2,
@ -391,7 +392,7 @@ describe('sql/conversationSummary', () => {
type: 'outgoing',
conversationId,
expirationTimerUpdate: {
expireTimer: 10,
expireTimer: DurationInSeconds.fromSeconds(10),
source: 'you',
fromSync: false,
},
@ -405,7 +406,7 @@ describe('sql/conversationSummary', () => {
type: 'outgoing',
conversationId,
expirationTimerUpdate: {
expireTimer: 10,
expireTimer: DurationInSeconds.fromSeconds(10),
fromSync: true,
},
sent_at: now + 2,
@ -450,7 +451,7 @@ describe('sql/conversationSummary', () => {
type: 'outgoing',
conversationId,
expirationStartTimestamp: now - 2 * 1000,
expireTimer: 1,
expireTimer: DurationInSeconds.fromSeconds(1),
sent_at: now + 2,
received_at: now + 2,
timestamp: now + 2,
@ -484,7 +485,7 @@ describe('sql/conversationSummary', () => {
type: 'outgoing',
conversationId,
expirationStartTimestamp: now,
expireTimer: 30,
expireTimer: DurationInSeconds.fromSeconds(30),
sent_at: now + 1,
received_at: now + 1,
timestamp: now + 1,
@ -495,7 +496,7 @@ describe('sql/conversationSummary', () => {
type: 'outgoing',
conversationId,
expirationStartTimestamp: now - 2 * 1000,
expireTimer: 1,
expireTimer: DurationInSeconds.fromSeconds(1),
sent_at: now + 2,
received_at: now + 2,
timestamp: now + 2,

View file

@ -8,6 +8,7 @@ import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import type { ReactionType } from '../../types/Reactions';
import { DurationInSeconds } from '../../util/durations';
import type { MessageAttributesType } from '../../model-types.d';
import { ReadStatus } from '../../messages/MessageReadStatus';
@ -331,7 +332,7 @@ describe('sql/markRead', () => {
const start = Date.now();
const readAt = start + 20;
const conversationId = getUuid();
const expireTimer = 15;
const expireTimer = DurationInSeconds.fromSeconds(15);
const ourUuid = getUuid();
const message1: MessageAttributesType = {

View file

@ -12,7 +12,7 @@ import type { ConversationType } from '../../../state/ducks/conversations';
import type { MessageAttributesType } from '../../../model-types.d';
import type { StateType as RootStateType } from '../../../state/reducer';
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 { ReadStatus } from '../../../messages/MessageReadStatus';
import {
@ -74,7 +74,7 @@ describe('both/state/ducks/stories', () => {
return {
conversationId,
expirationStartTimestamp: now,
expireTimer: 1 * DAY,
expireTimer: DurationInSeconds.DAY,
messageId,
readStatus: ReadStatus.Unread,
timestamp: now - timestampDelta,
@ -538,7 +538,7 @@ describe('both/state/ducks/stories', () => {
? ourConversationId
: groupConversationId,
expirationStartTimestamp: now,
expireTimer: 1 * DAY,
expireTimer: DurationInSeconds.DAY,
messageId,
readStatus: ReadStatus.Unread,
sendStateByConversationId: {},

View file

@ -5,13 +5,14 @@ import { assert } from 'chai';
import * as sinon from 'sinon';
import { RowType } from '../../../components/ConversationList';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
import { DurationInSeconds } from '../../../util/durations';
import { LeftPaneSetGroupMetadataHelper } from '../../../components/leftPane/LeftPaneSetGroupMetadataHelper';
function getComposeState() {
return {
groupAvatar: undefined,
groupExpireTimer: 0,
groupExpireTimer: DurationInSeconds.ZERO,
groupName: '',
hasError: false,
isCreating: false,

View file

@ -7,23 +7,27 @@ import protobuf from '../protobuf/wrap';
import { SignalService as Proto } from '../protobuf';
import { normalizeUuid } from '../util/normalizeUuid';
import { DurationInSeconds } from '../util/durations';
import * as log from '../logging/log';
import Avatar = Proto.ContactDetails.IAvatar;
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;
};
export type MessageWithAvatar<Message extends OptionalAvatar> = Omit<
type HydratedAvatar = Avatar & { data: Uint8Array };
type MessageWithAvatar<Message extends OptionalFields> = Omit<
Message,
'avatar'
> & {
avatar?: (Avatar & { data: Uint8Array }) | null;
avatar?: HydratedAvatar;
expireTimer?: DurationInSeconds;
};
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 */
abstract class ParserBase<
Message extends OptionalAvatar,
Message extends OptionalFields,
Decoder extends DecoderBase<Message>,
Result
> implements Iterable<Result>
@ -57,28 +61,33 @@ abstract class ParserBase<
return undefined;
}
if (!proto.avatar) {
return {
...proto,
avatar: null,
let avatar: HydratedAvatar | undefined;
if (proto.avatar) {
const attachmentLen = proto.avatar.length ?? 0;
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;
const avatarData = this.reader.buf.slice(
this.reader.pos,
this.reader.pos + attachmentLen
);
this.reader.skip(attachmentLen);
let expireTimer: DurationInSeconds | undefined;
if (proto.expireTimer != null) {
expireTimer = DurationInSeconds.fromSeconds(proto.expireTimer);
}
return {
...proto,
avatar: {
...proto.avatar,
data: avatarData,
},
avatar,
expireTimer,
};
} catch (error) {
log.error(
@ -118,6 +127,7 @@ export class GroupBuffer extends ParserBase<
if (!proto.members) {
return proto;
}
return {
...proto,
members: proto.members.map((member, i) => {

View file

@ -45,6 +45,7 @@ import { normalizeUuid } from '../util/normalizeUuid';
import { parseIntOrThrow } from '../util/parseIntOrThrow';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import { Zone } from '../util/Zone';
import { DurationInSeconds } from '../util/durations';
import { deriveMasterKeyFromGroupV1, bytesToUuid } from '../Crypto';
import type { DownloadedAttachmentType } from '../types/Attachment';
import { Address } from '../types/Address';
@ -2058,7 +2059,7 @@ export default class MessageReceiver
attachments,
preview,
canReplyToStory: Boolean(msg.allowsReplies),
expireTimer: durations.DAY / 1000,
expireTimer: DurationInSeconds.DAY,
flags: 0,
groupV2,
isStory: true,

View file

@ -65,6 +65,7 @@ import { concat, isEmpty, map } from '../util/iterables';
import type { SendTypesType } from '../util/handleMessageSend';
import { shouldSaveProto, sendTypesEnum } from '../util/handleMessageSend';
import { uuidToBytes } from '../util/uuidToBytes';
import type { DurationInSeconds } from '../util/durations';
import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log';
import type { Avatar, EmbeddedContactType } from '../types/EmbeddedContact';
@ -174,7 +175,7 @@ export type MessageOptionsType = {
attachments?: ReadonlyArray<AttachmentType> | null;
body?: string;
contact?: Array<ContactWithHydratedAvatar>;
expireTimer?: number;
expireTimer?: DurationInSeconds;
flags?: number;
group?: {
id: string;
@ -198,7 +199,7 @@ export type GroupSendOptionsType = {
attachments?: Array<AttachmentType>;
contact?: Array<ContactWithHydratedAvatar>;
deletedForEveryoneTimestamp?: number;
expireTimer?: number;
expireTimer?: DurationInSeconds;
flags?: number;
groupCallUpdate?: GroupCallUpdateType;
groupV1?: GroupV1InfoType;
@ -221,7 +222,7 @@ class Message {
contact?: Array<ContactWithHydratedAvatar>;
expireTimer?: number;
expireTimer?: DurationInSeconds;
flags?: number;
@ -1358,7 +1359,7 @@ export default class MessageSender {
contact?: Array<ContactWithHydratedAvatar>;
contentHint: number;
deletedForEveryoneTimestamp: number | undefined;
expireTimer: number | undefined;
expireTimer: DurationInSeconds | undefined;
groupId: string | undefined;
identifier: string;
messageText: string | undefined;

View file

@ -7,6 +7,7 @@ import type { UUID, UUIDStringType } from '../types/UUID';
import type { TextAttachmentType } from '../types/Attachment';
import type { GiftBadgeStates } from '../components/conversation/Message';
import type { MIMEType } from '../types/MIME';
import type { DurationInSeconds } from '../util/durations';
export {
IdentityKeyType,
@ -207,7 +208,7 @@ export type ProcessedDataMessage = {
group?: ProcessedGroupContext;
groupV2?: ProcessedGroupV2Context;
flags: number;
expireTimer: number;
expireTimer: DurationInSeconds;
profileKey?: string;
timestamp: number;
quote?: ProcessedQuote;

View file

@ -28,7 +28,7 @@ import type {
import { WarnOnlyError } from './Errors';
import { GiftBadgeStates } from '../components/conversation/Message';
import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../types/MIME';
import { SECOND } from '../util/durations';
import { SECOND, DurationInSeconds } from '../util/durations';
const FLAGS = Proto.DataMessage.Flags;
export const ATTACHMENT_MAX = 32;
@ -299,7 +299,7 @@ export function processDataMessage(
group: processGroupContext(message.group),
groupV2: processGroupV2Context(message.groupV2),
flags: message.flags ?? 0,
expireTimer: message.expireTimer ?? 0,
expireTimer: DurationInSeconds.fromSeconds(message.expireTimer ?? 0),
profileKey:
message.profileKey && message.profileKey.length > 0
? Bytes.toBase64(message.profileKey)

View file

@ -3,6 +3,7 @@
/* eslint-disable camelcase */
import type { DurationInSeconds } from '../util/durations';
import type { AttachmentType } from './Attachment';
import type { EmbeddedContactType } from './EmbeddedContact';
import type { IndexableBoolean, IndexablePresence } from './IndexedDB';
@ -30,7 +31,7 @@ export type IncomingMessage = Readonly<
body?: string;
decrypted_at?: number;
errors?: Array<Error>;
expireTimer?: number;
expireTimer?: DurationInSeconds;
messageTimer?: number; // deprecated
isViewOnce?: number;
flags?: number;
@ -54,7 +55,7 @@ export type OutgoingMessage = Readonly<
// Optional
body?: string;
expireTimer?: number;
expireTimer?: DurationInSeconds;
messageTimer?: number; // deprecated
isViewOnce?: number;
synced: boolean;
@ -88,7 +89,7 @@ export type SharedMessageProperties = Readonly<{
export type ExpirationTimerUpdate = Partial<
Readonly<{
expirationTimerUpdate: Readonly<{
expireTimer: number;
expireTimer: DurationInSeconds;
fromSync: boolean;
source: string; // PhoneNumber
}>;

View file

@ -34,6 +34,7 @@ import { PhoneNumberDiscoverability } from './phoneNumberDiscoverability';
import { PhoneNumberSharingMode } from './phoneNumberSharingMode';
import { assertDev } from './assert';
import * as durations from './durations';
import type { DurationInSeconds } from './durations';
import { isPhoneNumberSharingEnabled } from './isPhoneNumberSharingEnabled';
import {
parseE164FromSignalDotMeHash,
@ -66,7 +67,7 @@ export type IPCEventsValuesType = {
spellCheck: boolean;
systemTraySetting: SystemTraySetting;
themeSetting: ThemeType;
universalExpireTimer: number;
universalExpireTimer: DurationInSeconds;
zoomFactor: ZoomFactorType;
storyViewReceiptsEnabled: boolean;

View 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 */

View 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';

View file

@ -1,26 +1,25 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as moment from 'moment';
import humanizeDuration from 'humanize-duration';
import type { Unit } from 'humanize-duration';
import { isNumber } from 'lodash';
import type { LocalizerType } from '../types/Util';
import { SECOND } from './durations';
import { SECOND, DurationInSeconds } from './durations';
const SECONDS_PER_WEEK = 604800;
export const DEFAULT_DURATIONS_IN_SECONDS: ReadonlyArray<number> = [
0,
moment.duration(4, 'weeks').asSeconds(),
moment.duration(1, 'week').asSeconds(),
moment.duration(1, 'day').asSeconds(),
moment.duration(8, 'hours').asSeconds(),
moment.duration(1, 'hour').asSeconds(),
moment.duration(5, 'minutes').asSeconds(),
moment.duration(30, 'seconds').asSeconds(),
export const DEFAULT_DURATIONS_IN_SECONDS: ReadonlyArray<DurationInSeconds> = [
DurationInSeconds.ZERO,
DurationInSeconds.fromWeeks(4),
DurationInSeconds.fromWeeks(1),
DurationInSeconds.fromDays(1),
DurationInSeconds.fromHours(8),
DurationInSeconds.fromHours(1),
DurationInSeconds.fromMinutes(5),
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
);
@ -31,7 +30,7 @@ export type FormatOptions = {
export function format(
i18n: LocalizerType,
dirtySeconds?: number,
dirtySeconds?: DurationInSeconds,
{ capitalizeOff = false, largest }: FormatOptions = {}
): string {
let seconds = Math.abs(dirtySeconds || 0);
@ -66,7 +65,7 @@ export function format(
const defaultUnits: Array<Unit> =
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,
// allow it to pick from all the units
units: largest ? allUnits : defaultUnits,
@ -82,10 +81,10 @@ export function calculateExpirationTimestamp({
expireTimer,
expirationStartTimestamp,
}: {
expireTimer: number | undefined;
expireTimer: DurationInSeconds | undefined;
expirationStartTimestamp: number | undefined | null;
}): number | undefined {
return isNumber(expirationStartTimestamp) && isNumber(expireTimer)
? expirationStartTimestamp + expireTimer * SECOND
? expirationStartTimestamp + DurationInSeconds.toMillis(expireTimer)
: undefined;
}

View file

@ -1,9 +1,9 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { DAY } from './durations';
import { getMessageById } from '../messages/getMessageById';
import { isNotNil } from './isNotNil';
import { DurationInSeconds } from './durations';
import { markViewed } from '../services/MessageUpdater';
import { storageServiceUploadJob } from '../services/storage';
@ -29,7 +29,7 @@ export async function markOnboardingStoryAsRead(): Promise<void> {
}
message.set({
expireTimer: DAY,
expireTimer: DurationInSeconds.DAY,
});
message.set(markViewed(message.attributes, storyReadDate));

View file

@ -7,7 +7,6 @@ import type { SendStateByConversationId } from '../messages/MessageSendState';
import type { UUIDStringType } from '../types/UUID';
import * as log from '../logging/log';
import dataInterface from '../sql/Client';
import { DAY, SECOND } from './durations';
import { MY_STORY_ID, StorySendMode } from '../types/Stories';
import { getStoriesBlocked } from './stories';
import { ReadStatus } from '../messages/MessageReadStatus';
@ -24,6 +23,7 @@ import { incrementMessageCounter } from './incrementMessageCounter';
import { isGroupV2 } from './whatTypeOfConversation';
import { isNotNil } from './isNotNil';
import { collect } from './iterables';
import { DurationInSeconds } from './durations';
export async function sendStoryMessage(
listIds: Array<string>,
@ -158,7 +158,7 @@ export async function sendStoryMessage(
return window.Signal.Migrations.upgradeMessageSchema({
attachments,
conversationId: ourConversation.id,
expireTimer: DAY / SECOND,
expireTimer: DurationInSeconds.DAY,
expirationStartTimestamp: Date.now(),
id: UUID.generate().toString(),
readStatus: ReadStatus.Read,
@ -262,7 +262,7 @@ export async function sendStoryMessage(
attachments,
canReplyToStory: true,
conversationId: group.id,
expireTimer: DAY / SECOND,
expireTimer: DurationInSeconds.DAY,
expirationStartTimestamp: Date.now(),
id: UUID.generate().toString(),
readStatus: ReadStatus.Read,

View file

@ -1,12 +1,14 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { DurationInSeconds } from './durations';
export const ITEM_NAME = 'universalExpireTimer';
export function get(): number {
return window.storage.get(ITEM_NAME) || 0;
export function get(): DurationInSeconds {
return DurationInSeconds.fromSeconds(window.storage.get(ITEM_NAME) || 0);
}
export function set(newValue: number | undefined): Promise<void> {
return window.storage.put(ITEM_NAME, newValue || 0);
export function set(newValue: DurationInSeconds | undefined): Promise<void> {
return window.storage.put(ITEM_NAME, newValue || DurationInSeconds.ZERO);
}

View file

@ -35,6 +35,7 @@ import {
isGroupV1,
} from '../util/whatTypeOfConversation';
import { findAndFormatContact } from '../util/findAndFormatContact';
import type { DurationInSeconds } from '../util/durations';
import { getPreferredBadgeSelector } from '../state/selectors/badges';
import {
canReply,
@ -347,7 +348,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
const conversationHeaderProps = {
id: this.model.id,
onSetDisappearingMessages: (seconds: number) =>
onSetDisappearingMessages: (seconds: DurationInSeconds) =>
this.setDisappearingMessages(seconds),
onDeleteMessages: () => this.destroyMessages(),
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 valueToSet = seconds > 0 ? seconds : undefined;

View file

@ -14,6 +14,7 @@ import {
shouldMinimizeToSystemTray,
} from '../../types/SystemTraySetting';
import { awaitObject } from '../../util/awaitObject';
import { DurationInSeconds } from '../../util/durations';
import { createSetting, createCallback } from '../../util/preload';
import { startInteractionMode } from '../startInteractionMode';
@ -215,6 +216,10 @@ const renderPreferences = async () => {
const { hasMinimizeToAndStartInSystemTray, hasMinimizeToSystemTray } =
getSystemTraySettingValues(systemTraySetting);
const onUniversalExpireTimerChange = reRender(
settingUniversalExpireTimer.setValue
);
const props = {
// Settings
availableCameras,
@ -250,7 +255,7 @@ const renderPreferences = async () => {
selectedMicrophone,
selectedSpeaker,
themeSetting,
universalExpireTimer,
universalExpireTimer: DurationInSeconds.fromSeconds(universalExpireTimer),
whoCanFindMe,
whoCanSeeMe,
zoomFactor,
@ -347,9 +352,11 @@ const renderPreferences = async () => {
onSelectedSpeakerChange: reRender(settingAudioOutput.setValue),
onSpellCheckChange: reRender(settingSpellCheck.setValue),
onThemeChange: reRender(settingTheme.setValue),
onUniversalExpireTimerChange: reRender(
settingUniversalExpireTimer.setValue
),
onUniversalExpireTimerChange: (newValue: number): Promise<void> => {
return onUniversalExpireTimerChange(
DurationInSeconds.fromSeconds(newValue)
);
},
// Zoom factor change doesn't require immediate rerender since it will:
// 1. Update the zoom factor in the main window