Added the time remaining for disappearing messages and stories
This commit is contained in:
parent
134265496b
commit
383a0fd17f
|
@ -4855,6 +4855,10 @@
|
||||||
"message": "Viewed by",
|
"message": "Viewed by",
|
||||||
"description": "In the message details screen, shown above contacts who have viewed this message"
|
"description": "In the message details screen, shown above contacts who have viewed this message"
|
||||||
},
|
},
|
||||||
|
"MessageDetail--disappears-in": {
|
||||||
|
"message": "Disappears in",
|
||||||
|
"description": "In the message details screen, shown as a label of how long it will be before the message disappears"
|
||||||
|
},
|
||||||
"ProfileEditor--about": {
|
"ProfileEditor--about": {
|
||||||
"message": "About",
|
"message": "About",
|
||||||
"description": "Default text for about field"
|
"description": "Default text for about field"
|
||||||
|
@ -5575,6 +5579,16 @@
|
||||||
"message": "File size $size$",
|
"message": "File size $size$",
|
||||||
"description": "File size description"
|
"description": "File size description"
|
||||||
},
|
},
|
||||||
|
"StoryDetailsModal__disappears-in": {
|
||||||
|
"message": "Disappears in $countdown$",
|
||||||
|
"description": "File size description",
|
||||||
|
"placeholders": {
|
||||||
|
"countdown": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "2 weeks, 3 days"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"StoryDetailsModal__copy-timestamp": {
|
"StoryDetailsModal__copy-timestamp": {
|
||||||
"message": "Copy timestamp",
|
"message": "Copy timestamp",
|
||||||
"description": "Context menu item to help debugging"
|
"description": "Context menu item to help debugging"
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { ThemeType } from '../types/Util';
|
||||||
import { Time } from './Time';
|
import { Time } from './Time';
|
||||||
import { formatDateTimeLong } from '../util/timestamp';
|
import { formatDateTimeLong } from '../util/timestamp';
|
||||||
import { groupBy } from '../util/mapUtil';
|
import { groupBy } from '../util/mapUtil';
|
||||||
|
import { format as formatRelativeTime } from '../util/expirationTimer';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
|
@ -25,6 +26,7 @@ export type PropsType = {
|
||||||
sender: StoryViewType['sender'];
|
sender: StoryViewType['sender'];
|
||||||
sendState?: Array<StorySendStateType>;
|
sendState?: Array<StorySendStateType>;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
expirationTimestamp: number | undefined;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -66,6 +68,7 @@ export const StoryDetailsModal = ({
|
||||||
sendState,
|
sendState,
|
||||||
size,
|
size,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
expirationTimestamp,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const contactsBySendStatus = sendState
|
const contactsBySendStatus = sendState
|
||||||
? groupBy(sendState, contact => contact.status)
|
? groupBy(sendState, contact => contact.status)
|
||||||
|
@ -181,6 +184,10 @@ export const StoryDetailsModal = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeRemaining = expirationTimestamp
|
||||||
|
? expirationTimestamp - Date.now()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
hasXButton
|
hasXButton
|
||||||
|
@ -235,6 +242,21 @@ export const StoryDetailsModal = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{timeRemaining && timeRemaining > 0 && (
|
||||||
|
<div>
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id="StoryDetailsModal__disappears-in"
|
||||||
|
components={[
|
||||||
|
<span className="StoryDetailsModal__debugger__button__text">
|
||||||
|
{formatRelativeTime(i18n, timeRemaining / 1000, {
|
||||||
|
largest: 2,
|
||||||
|
})}
|
||||||
|
</span>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -55,6 +55,7 @@ SomeonesStory.args = {
|
||||||
messageId: '123',
|
messageId: '123',
|
||||||
sender: getDefaultConversation(),
|
sender: getDefaultConversation(),
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
expirationTimestamp: undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
SomeonesStory.story = {
|
SomeonesStory.story = {
|
||||||
|
|
|
@ -748,6 +748,7 @@ export const StoryViewer = ({
|
||||||
sendState={sendState}
|
sendState={sendState}
|
||||||
size={attachment?.size}
|
size={attachment?.size}
|
||||||
timestamp={timestamp}
|
timestamp={timestamp}
|
||||||
|
expirationTimestamp={story.expirationTimestamp}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasStoryViewsNRepliesModal && (
|
{hasStoryViewsNRepliesModal && (
|
||||||
|
|
|
@ -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 { format as formatRelativeTime } from '../../util/expirationTimer';
|
||||||
|
|
||||||
export type Contact = Pick<
|
export type Contact = Pick<
|
||||||
ConversationType,
|
ConversationType,
|
||||||
|
@ -65,7 +66,13 @@ export type PropsData = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
} & Pick<MessagePropsType, 'getPreferredBadge' | 'interactionMode'>;
|
} & Pick<
|
||||||
|
MessagePropsType,
|
||||||
|
| 'getPreferredBadge'
|
||||||
|
| 'interactionMode'
|
||||||
|
| 'expirationLength'
|
||||||
|
| 'expirationTimestamp'
|
||||||
|
>;
|
||||||
|
|
||||||
export type PropsBackboneActions = Pick<
|
export type PropsBackboneActions = Pick<
|
||||||
MessagePropsType,
|
MessagePropsType,
|
||||||
|
@ -280,6 +287,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
contactNameColor,
|
contactNameColor,
|
||||||
displayTapToViewMessage,
|
displayTapToViewMessage,
|
||||||
doubleCheckMissingQuoteReference,
|
doubleCheckMissingQuoteReference,
|
||||||
|
expirationTimestamp,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
i18n,
|
i18n,
|
||||||
interactionMode,
|
interactionMode,
|
||||||
|
@ -307,6 +315,10 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
viewStory,
|
viewStory,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const timeRemaining = expirationTimestamp
|
||||||
|
? expirationTimestamp - Date.now()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||||
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
|
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
|
||||||
|
@ -431,6 +443,18 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : null}
|
) : null}
|
||||||
|
{timeRemaining && timeRemaining > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td className="module-message-detail__label">
|
||||||
|
{i18n('MessageDetail--disappears-in')}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{formatRelativeTime(i18n, timeRemaining / 1000, {
|
||||||
|
largest: 2,
|
||||||
|
})}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{this.renderContacts()}
|
{this.renderContacts()}
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
// Copyright 2020-2022 Signal Messenger, LLC
|
// Copyright 2020-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { isEmpty, isEqual, mapValues, maxBy, noop, omit, union } from 'lodash';
|
import {
|
||||||
|
isEmpty,
|
||||||
|
isEqual,
|
||||||
|
isNumber,
|
||||||
|
mapValues,
|
||||||
|
maxBy,
|
||||||
|
noop,
|
||||||
|
omit,
|
||||||
|
union,
|
||||||
|
} from 'lodash';
|
||||||
import type {
|
import type {
|
||||||
CustomError,
|
CustomError,
|
||||||
GroupV1Update,
|
GroupV1Update,
|
||||||
|
@ -163,12 +172,19 @@ 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';
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
|
|
||||||
type PropsForMessageDetail = Pick<
|
type PropsForMessageDetail = Pick<
|
||||||
SmartMessageDetailPropsType,
|
SmartMessageDetailPropsType,
|
||||||
'sentAt' | 'receivedAt' | 'message' | 'errors' | 'contacts'
|
| 'sentAt'
|
||||||
|
| 'receivedAt'
|
||||||
|
| 'message'
|
||||||
|
| 'errors'
|
||||||
|
| 'contacts'
|
||||||
|
| 'expirationLength'
|
||||||
|
| 'expirationTimestamp'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
declare const _: typeof window._;
|
declare const _: typeof window._;
|
||||||
|
@ -465,9 +481,21 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const expireTimer = this.get('expireTimer');
|
||||||
|
const expirationStartTimestamp = this.get('expirationStartTimestamp');
|
||||||
|
const expirationLength = isNumber(expireTimer)
|
||||||
|
? expireTimer * SECOND
|
||||||
|
: undefined;
|
||||||
|
const expirationTimestamp = expirationTimer.calculateExpirationTimestamp({
|
||||||
|
expireTimer,
|
||||||
|
expirationStartTimestamp,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sentAt: this.get('sent_at'),
|
sentAt: this.get('sent_at'),
|
||||||
receivedAt: this.getReceivedAt(),
|
receivedAt: this.getReceivedAt(),
|
||||||
|
expirationLength,
|
||||||
|
expirationTimestamp,
|
||||||
message: getPropsForMessage(this.attributes, {
|
message: getPropsForMessage(this.attributes, {
|
||||||
conversationSelector: findAndFormatContact,
|
conversationSelector: findAndFormatContact,
|
||||||
ourConversationId,
|
ourConversationId,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import dataInterface from '../sql/Client';
|
||||||
import { getAttachmentsForMessage } from '../state/selectors/message';
|
import { getAttachmentsForMessage } from '../state/selectors/message';
|
||||||
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';
|
||||||
|
|
||||||
let storyData: Array<MessageAttributesType> | undefined;
|
let storyData: Array<MessageAttributesType> | undefined;
|
||||||
|
|
||||||
|
@ -51,6 +52,8 @@ export function getStoryDataFromMessageAttributes(
|
||||||
'timestamp',
|
'timestamp',
|
||||||
'type',
|
'type',
|
||||||
]),
|
]),
|
||||||
|
expireTimer: message.expireTimer,
|
||||||
|
expirationStartTimestamp: dropNull(message.expirationStartTimestamp),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,11 @@ export type StoryDataType = {
|
||||||
| 'storyDistributionListId'
|
| 'storyDistributionListId'
|
||||||
| 'timestamp'
|
| 'timestamp'
|
||||||
| 'type'
|
| 'type'
|
||||||
>;
|
> & {
|
||||||
|
// don't want the fields to be optional as in MessageAttributesType
|
||||||
|
expireTimer: number | undefined;
|
||||||
|
expirationStartTimestamp: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export type SelectedStoryDataType = {
|
export type SelectedStoryDataType = {
|
||||||
currentIndex: number;
|
currentIndex: number;
|
||||||
|
@ -1149,6 +1153,8 @@ export function reducer(
|
||||||
'canReplyToStory',
|
'canReplyToStory',
|
||||||
'conversationId',
|
'conversationId',
|
||||||
'deletedForEveryone',
|
'deletedForEveryone',
|
||||||
|
'expirationStartTimestamp',
|
||||||
|
'expireTimer',
|
||||||
'messageId',
|
'messageId',
|
||||||
'reactions',
|
'reactions',
|
||||||
'readStatus',
|
'readStatus',
|
||||||
|
|
|
@ -92,9 +92,10 @@ 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 } from '../../util/durations';
|
import { DAY, HOUR, SECOND } 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';
|
||||||
|
|
||||||
export { isIncoming, isOutgoing, isStory };
|
export { isIncoming, isOutgoing, isStory };
|
||||||
|
|
||||||
|
@ -625,11 +626,7 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||||
}: GetPropsForMessageOptions
|
}: GetPropsForMessageOptions
|
||||||
): ShallowPropsType => {
|
): ShallowPropsType => {
|
||||||
const { expireTimer, expirationStartTimestamp, conversationId } = message;
|
const { expireTimer, expirationStartTimestamp, conversationId } = message;
|
||||||
const expirationLength = expireTimer ? expireTimer * 1000 : undefined;
|
const expirationLength = expireTimer ? expireTimer * SECOND : undefined;
|
||||||
const expirationTimestamp =
|
|
||||||
expirationStartTimestamp && expirationLength
|
|
||||||
? expirationStartTimestamp + expirationLength
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const conversation = getConversation(message, conversationSelector);
|
const conversation = getConversation(message, conversationSelector);
|
||||||
const isGroup = conversation.type === 'group';
|
const isGroup = conversation.type === 'group';
|
||||||
|
@ -673,7 +670,10 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||||
direction: isIncoming(message) ? 'incoming' : 'outgoing',
|
direction: isIncoming(message) ? 'incoming' : 'outgoing',
|
||||||
displayLimit: message.displayLimit,
|
displayLimit: message.displayLimit,
|
||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp: calculateExpirationTimestamp({
|
||||||
|
expireTimer,
|
||||||
|
expirationStartTimestamp,
|
||||||
|
}),
|
||||||
giftBadge: message.giftBadge,
|
giftBadge: message.giftBadge,
|
||||||
id: message.id,
|
id: message.id,
|
||||||
isBlocked: conversation.isBlocked || false,
|
isBlocked: conversation.isBlocked || false,
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
} from './conversations';
|
} from './conversations';
|
||||||
import { getDistributionListSelector } from './storyDistributionLists';
|
import { getDistributionListSelector } from './storyDistributionLists';
|
||||||
import { getStoriesEnabled } from './items';
|
import { getStoriesEnabled } from './items';
|
||||||
|
import { calculateExpirationTimestamp } from '../../util/expirationTimer';
|
||||||
|
|
||||||
export const getStoriesState = (state: StateType): StoriesStateType =>
|
export const getStoriesState = (state: StateType): StoriesStateType =>
|
||||||
state.stories;
|
state.stories;
|
||||||
|
@ -142,7 +143,10 @@ export function getStoryView(
|
||||||
'title',
|
'title',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { attachment, timestamp } = pick(story, ['attachment', 'timestamp']);
|
const { attachment, timestamp, expirationStartTimestamp, expireTimer } = pick(
|
||||||
|
story,
|
||||||
|
['attachment', 'timestamp', 'expirationStartTimestamp', 'expireTimer']
|
||||||
|
);
|
||||||
|
|
||||||
const { sendStateByConversationId } = story;
|
const { sendStateByConversationId } = story;
|
||||||
let sendState: Array<StorySendStateType> | undefined;
|
let sendState: Array<StorySendStateType> | undefined;
|
||||||
|
@ -179,6 +183,10 @@ export function getStoryView(
|
||||||
sender,
|
sender,
|
||||||
sendState,
|
sendState,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
expirationTimestamp: calculateExpirationTimestamp({
|
||||||
|
expireTimer,
|
||||||
|
expirationStartTimestamp,
|
||||||
|
}),
|
||||||
views,
|
views,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@ export function getFakeStoryView(
|
||||||
messageId: UUID.generate().toString(),
|
messageId: UUID.generate().toString(),
|
||||||
sender,
|
sender,
|
||||||
timestamp: timestamp || Date.now() - 2 * durations.MINUTE,
|
timestamp: timestamp || Date.now() - 2 * durations.MINUTE,
|
||||||
|
expirationTimestamp: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
} from '../../../state/ducks/stories';
|
} from '../../../state/ducks/stories';
|
||||||
import { noopAction } from '../../../state/ducks/noop';
|
import { noopAction } from '../../../state/ducks/noop';
|
||||||
import { reducer as rootReducer } from '../../../state/reducer';
|
import { reducer as rootReducer } from '../../../state/reducer';
|
||||||
|
import { dropNull } from '../../../util/dropNull';
|
||||||
|
|
||||||
describe('both/state/ducks/stories', () => {
|
describe('both/state/ducks/stories', () => {
|
||||||
const getEmptyRootState = () => ({
|
const getEmptyRootState = () => ({
|
||||||
|
@ -119,6 +120,10 @@ describe('both/state/ducks/stories', () => {
|
||||||
...messageAttributes,
|
...messageAttributes,
|
||||||
attachment: messageAttributes.attachments[0],
|
attachment: messageAttributes.attachments[0],
|
||||||
messageId: messageAttributes.id,
|
messageId: messageAttributes.id,
|
||||||
|
expireTimer: messageAttributes.expireTimer,
|
||||||
|
expirationStartTimestamp: dropNull(
|
||||||
|
messageAttributes.expirationStartTimestamp
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -150,6 +155,10 @@ describe('both/state/ducks/stories', () => {
|
||||||
...messageAttributes,
|
...messageAttributes,
|
||||||
messageId: storyId,
|
messageId: storyId,
|
||||||
attachment,
|
attachment,
|
||||||
|
expireTimer: messageAttributes.expireTimer,
|
||||||
|
expirationStartTimestamp: dropNull(
|
||||||
|
messageAttributes.expirationStartTimestamp
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -191,6 +200,10 @@ describe('both/state/ducks/stories', () => {
|
||||||
...messageAttributes,
|
...messageAttributes,
|
||||||
attachment: messageAttributes.attachments[0],
|
attachment: messageAttributes.attachments[0],
|
||||||
messageId: messageAttributes.id,
|
messageId: messageAttributes.id,
|
||||||
|
expireTimer: messageAttributes.expireTimer,
|
||||||
|
expirationStartTimestamp: dropNull(
|
||||||
|
messageAttributes.expirationStartTimestamp
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -90,6 +90,7 @@ export type StoryViewType = {
|
||||||
>;
|
>;
|
||||||
sendState?: Array<StorySendStateType>;
|
sendState?: Array<StorySendStateType>;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
expirationTimestamp: number | undefined;
|
||||||
views?: number;
|
views?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
|
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import humanizeDuration from 'humanize-duration';
|
import humanizeDuration from 'humanize-duration';
|
||||||
|
import type { Unit } from 'humanize-duration';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import { SECOND } 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<number> = [
|
||||||
|
@ -23,12 +26,13 @@ export const DEFAULT_DURATIONS_SET: ReadonlySet<number> = new Set<number>(
|
||||||
|
|
||||||
export type FormatOptions = {
|
export type FormatOptions = {
|
||||||
capitalizeOff?: boolean;
|
capitalizeOff?: boolean;
|
||||||
|
largest?: number; // how many units to show (the largest n)
|
||||||
};
|
};
|
||||||
|
|
||||||
export function format(
|
export function format(
|
||||||
i18n: LocalizerType,
|
i18n: LocalizerType,
|
||||||
dirtySeconds?: number,
|
dirtySeconds?: number,
|
||||||
{ capitalizeOff = false }: FormatOptions = {}
|
{ capitalizeOff = false, largest }: FormatOptions = {}
|
||||||
): string {
|
): string {
|
||||||
let seconds = Math.abs(dirtySeconds || 0);
|
let seconds = Math.abs(dirtySeconds || 0);
|
||||||
if (!seconds) {
|
if (!seconds) {
|
||||||
|
@ -49,9 +53,31 @@ export function format(
|
||||||
fallbacks.push('en');
|
fallbacks.push('en');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allUnits: Array<Unit> = ['y', 'mo', 'w', 'd', 'h', 'm', 's'];
|
||||||
|
|
||||||
|
const defaultUnits: Array<Unit> =
|
||||||
|
seconds % SECONDS_PER_WEEK === 0 ? ['w'] : ['d', 'h', 'm', 's'];
|
||||||
|
|
||||||
return humanizeDuration(seconds * 1000, {
|
return humanizeDuration(seconds * 1000, {
|
||||||
units: seconds % SECONDS_PER_WEEK === 0 ? ['w'] : ['d', 'h', 'm', 's'],
|
// if we have an explict `largest` specified,
|
||||||
|
// allow it to pick from all the units
|
||||||
|
units: largest ? allUnits : defaultUnits,
|
||||||
|
largest,
|
||||||
language: locale,
|
language: locale,
|
||||||
...(fallbacks.length ? { fallbacks } : {}),
|
...(fallbacks.length ? { fallbacks } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normally we would not have undefineds all over,
|
||||||
|
// but most use-cases start out with undefineds
|
||||||
|
export function calculateExpirationTimestamp({
|
||||||
|
expireTimer,
|
||||||
|
expirationStartTimestamp,
|
||||||
|
}: {
|
||||||
|
expireTimer: number | undefined;
|
||||||
|
expirationStartTimestamp: number | undefined | null;
|
||||||
|
}): number | undefined {
|
||||||
|
return isNumber(expirationStartTimestamp) && isNumber(expireTimer)
|
||||||
|
? expirationStartTimestamp + expireTimer * SECOND
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue