Skip to content

Commit

Permalink
Added the time remaining for disappearing messages and stories
Browse files Browse the repository at this point in the history
  • Loading branch information
alvaro-signal committed Sep 9, 2022
1 parent 1342654 commit 383a0fd
Show file tree
Hide file tree
Showing 14 changed files with 162 additions and 14 deletions.
14 changes: 14 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4855,6 +4855,10 @@
"message": "Viewed by",
"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": {
"message": "About",
"description": "Default text for about field"
Expand Down Expand Up @@ -5575,6 +5579,16 @@
"message": "File size $size$",
"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": {
"message": "Copy timestamp",
"description": "Context menu item to help debugging"
Expand Down
22 changes: 22 additions & 0 deletions ts/components/StoryDetailsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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';

export type PropsType = {
getPreferredBadge: PreferredBadgeSelectorType;
Expand All @@ -25,6 +26,7 @@ export type PropsType = {
sender: StoryViewType['sender'];
sendState?: Array<StorySendStateType>;
size?: number;
expirationTimestamp: number | undefined;
timestamp: number;
};

Expand Down Expand Up @@ -66,6 +68,7 @@ export const StoryDetailsModal = ({
sendState,
size,
timestamp,
expirationTimestamp,
}: PropsType): JSX.Element => {
const contactsBySendStatus = sendState
? groupBy(sendState, contact => contact.status)
Expand Down Expand Up @@ -181,6 +184,10 @@ export const StoryDetailsModal = ({
);
}

const timeRemaining = expirationTimestamp
? expirationTimestamp - Date.now()
: undefined;

return (
<Modal
hasXButton
Expand Down Expand Up @@ -235,6 +242,21 @@ export const StoryDetailsModal = ({
/>
</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>
}
>
Expand Down
1 change: 1 addition & 0 deletions ts/components/StoryListItem.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ SomeonesStory.args = {
messageId: '123',
sender: getDefaultConversation(),
timestamp: Date.now(),
expirationTimestamp: undefined,
},
};
SomeonesStory.story = {
Expand Down
1 change: 1 addition & 0 deletions ts/components/StoryViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,7 @@ export const StoryViewer = ({
sendState={sendState}
size={attachment?.size}
timestamp={timestamp}
expirationTimestamp={story.expirationTimestamp}
/>
)}
{hasStoryViewsNRepliesModal && (
Expand Down
26 changes: 25 additions & 1 deletion ts/components/conversation/MessageDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { format as formatRelativeTime } from '../../util/expirationTimer';

export type Contact = Pick<
ConversationType,
Expand Down Expand Up @@ -65,7 +66,13 @@ export type PropsData = {
i18n: LocalizerType;
theme: ThemeType;
getPreferredBadge: PreferredBadgeSelectorType;
} & Pick<MessagePropsType, 'getPreferredBadge' | 'interactionMode'>;
} & Pick<
MessagePropsType,
| 'getPreferredBadge'
| 'interactionMode'
| 'expirationLength'
| 'expirationTimestamp'
>;

export type PropsBackboneActions = Pick<
MessagePropsType,
Expand Down Expand Up @@ -280,6 +287,7 @@ export class MessageDetail extends React.Component<Props> {
contactNameColor,
displayTapToViewMessage,
doubleCheckMissingQuoteReference,
expirationTimestamp,
getPreferredBadge,
i18n,
interactionMode,
Expand Down Expand Up @@ -307,6 +315,10 @@ export class MessageDetail extends React.Component<Props> {
viewStory,
} = this.props;

const timeRemaining = expirationTimestamp
? expirationTimestamp - Date.now()
: undefined;

return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
Expand Down Expand Up @@ -431,6 +443,18 @@ export class MessageDetail extends React.Component<Props> {
</td>
</tr>
) : 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>
</table>
{this.renderContacts()}
Expand Down
32 changes: 30 additions & 2 deletions ts/models/messages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
// Copyright 2020-2022 Signal Messenger, LLC
// 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 {
CustomError,
GroupV1Update,
Expand Down Expand Up @@ -163,12 +172,19 @@ 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';

/* eslint-disable more/no-then */

type PropsForMessageDetail = Pick<
SmartMessageDetailPropsType,
'sentAt' | 'receivedAt' | 'message' | 'errors' | 'contacts'
| 'sentAt'
| 'receivedAt'
| 'message'
| 'errors'
| 'contacts'
| 'expirationLength'
| 'expirationTimestamp'
>;

declare const _: typeof window._;
Expand Down Expand Up @@ -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 {
sentAt: this.get('sent_at'),
receivedAt: this.getReceivedAt(),
expirationLength,
expirationTimestamp,
message: getPropsForMessage(this.attributes, {
conversationSelector: findAndFormatContact,
ourConversationId,
Expand Down
3 changes: 3 additions & 0 deletions ts/services/storyLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import dataInterface from '../sql/Client';
import { getAttachmentsForMessage } from '../state/selectors/message';
import { isNotNil } from '../util/isNotNil';
import { strictAssert } from '../util/assert';
import { dropNull } from '../util/dropNull';

let storyData: Array<MessageAttributesType> | undefined;

Expand Down Expand Up @@ -51,6 +52,8 @@ export function getStoryDataFromMessageAttributes(
'timestamp',
'type',
]),
expireTimer: message.expireTimer,
expirationStartTimestamp: dropNull(message.expirationStartTimestamp),
};
}

Expand Down
8 changes: 7 additions & 1 deletion ts/state/ducks/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ export type StoryDataType = {
| 'storyDistributionListId'
| 'timestamp'
| 'type'
>;
> & {
// don't want the fields to be optional as in MessageAttributesType
expireTimer: number | undefined;
expirationStartTimestamp: number | undefined;
};

export type SelectedStoryDataType = {
currentIndex: number;
Expand Down Expand Up @@ -1149,6 +1153,8 @@ export function reducer(
'canReplyToStory',
'conversationId',
'deletedForEveryone',
'expirationStartTimestamp',
'expireTimer',
'messageId',
'reactions',
'readStatus',
Expand Down
14 changes: 7 additions & 7 deletions ts/state/selectors/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,10 @@ import {
} from '../../messages/MessageSendState';
import * as log from '../../logging/log';
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
import { DAY, HOUR } from '../../util/durations';
import { DAY, HOUR, SECOND } from '../../util/durations';
import { getStoryReplyText } from '../../util/getStoryReplyText';
import { isIncoming, isOutgoing, isStory } from '../../messages/helpers';
import { calculateExpirationTimestamp } from '../../util/expirationTimer';

export { isIncoming, isOutgoing, isStory };

Expand Down Expand Up @@ -625,11 +626,7 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
}: GetPropsForMessageOptions
): ShallowPropsType => {
const { expireTimer, expirationStartTimestamp, conversationId } = message;
const expirationLength = expireTimer ? expireTimer * 1000 : undefined;
const expirationTimestamp =
expirationStartTimestamp && expirationLength
? expirationStartTimestamp + expirationLength
: undefined;
const expirationLength = expireTimer ? expireTimer * SECOND : undefined;

const conversation = getConversation(message, conversationSelector);
const isGroup = conversation.type === 'group';
Expand Down Expand Up @@ -673,7 +670,10 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
direction: isIncoming(message) ? 'incoming' : 'outgoing',
displayLimit: message.displayLimit,
expirationLength,
expirationTimestamp,
expirationTimestamp: calculateExpirationTimestamp({
expireTimer,
expirationStartTimestamp,
}),
giftBadge: message.giftBadge,
id: message.id,
isBlocked: conversation.isBlocked || false,
Expand Down
10 changes: 9 additions & 1 deletion ts/state/selectors/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from './conversations';
import { getDistributionListSelector } from './storyDistributionLists';
import { getStoriesEnabled } from './items';
import { calculateExpirationTimestamp } from '../../util/expirationTimer';

export const getStoriesState = (state: StateType): StoriesStateType =>
state.stories;
Expand Down Expand Up @@ -142,7 +143,10 @@ export function getStoryView(
'title',
]);

const { attachment, timestamp } = pick(story, ['attachment', 'timestamp']);
const { attachment, timestamp, expirationStartTimestamp, expireTimer } = pick(
story,
['attachment', 'timestamp', 'expirationStartTimestamp', 'expireTimer']
);

const { sendStateByConversationId } = story;
let sendState: Array<StorySendStateType> | undefined;
Expand Down Expand Up @@ -179,6 +183,10 @@ export function getStoryView(
sender,
sendState,
timestamp,
expirationTimestamp: calculateExpirationTimestamp({
expireTimer,
expirationStartTimestamp,
}),
views,
};
}
Expand Down
1 change: 1 addition & 0 deletions ts/test-both/helpers/getFakeStory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export function getFakeStoryView(
messageId: UUID.generate().toString(),
sender,
timestamp: timestamp || Date.now() - 2 * durations.MINUTE,
expirationTimestamp: undefined,
};
}

Expand Down
13 changes: 13 additions & 0 deletions ts/test-electron/state/ducks/stories_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '../../../state/ducks/stories';
import { noopAction } from '../../../state/ducks/noop';
import { reducer as rootReducer } from '../../../state/reducer';
import { dropNull } from '../../../util/dropNull';

describe('both/state/ducks/stories', () => {
const getEmptyRootState = () => ({
Expand Down Expand Up @@ -119,6 +120,10 @@ describe('both/state/ducks/stories', () => {
...messageAttributes,
attachment: messageAttributes.attachments[0],
messageId: messageAttributes.id,
expireTimer: messageAttributes.expireTimer,
expirationStartTimestamp: dropNull(
messageAttributes.expirationStartTimestamp
),
},
],
},
Expand Down Expand Up @@ -150,6 +155,10 @@ describe('both/state/ducks/stories', () => {
...messageAttributes,
messageId: storyId,
attachment,
expireTimer: messageAttributes.expireTimer,
expirationStartTimestamp: dropNull(
messageAttributes.expirationStartTimestamp
),
},
],
};
Expand Down Expand Up @@ -191,6 +200,10 @@ describe('both/state/ducks/stories', () => {
...messageAttributes,
attachment: messageAttributes.attachments[0],
messageId: messageAttributes.id,
expireTimer: messageAttributes.expireTimer,
expirationStartTimestamp: dropNull(
messageAttributes.expirationStartTimestamp
),
},
],
},
Expand Down
1 change: 1 addition & 0 deletions ts/types/Stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export type StoryViewType = {
>;
sendState?: Array<StorySendStateType>;
timestamp: number;
expirationTimestamp: number | undefined;
views?: number;
};

Expand Down

0 comments on commit 383a0fd

Please sign in to comment.