diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index 1663ed358..083ef92d6 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -227,6 +227,7 @@ export type MessageAttributesType = { sendHQImages?: boolean; // Should only be present for incoming messages and errors + readAt?: number; readStatus?: ReadStatus; // Used for all kinds of notifications, as well as incoming messages seenStatus?: SeenStatus; diff --git a/ts/services/MessageUpdater.ts b/ts/services/MessageUpdater.ts index e2d984b24..23a3063b8 100644 --- a/ts/services/MessageUpdater.ts +++ b/ts/services/MessageUpdater.ts @@ -17,6 +17,7 @@ function markReadOrViewed( const nextMessageAttributes: MessageAttributesType = { ...messageAttrs, + readAt: timestamp, readStatus: newReadStatus, seenStatus: SeenStatus.Seen, }; diff --git a/ts/services/storyLoader.ts b/ts/services/storyLoader.ts index 297c9ec2b..117b88eeb 100644 --- a/ts/services/storyLoader.ts +++ b/ts/services/storyLoader.ts @@ -44,6 +44,7 @@ export function getStoryDataFromMessageAttributes( 'conversationId', 'deletedForEveryone', 'reactions', + 'readAt', 'readStatus', 'sendStateByConversationId', 'source', diff --git a/ts/state/ducks/stories.ts b/ts/state/ducks/stories.ts index 8acc30aea..fdee34273 100644 --- a/ts/state/ducks/stories.ts +++ b/ts/state/ducks/stories.ts @@ -64,6 +64,7 @@ export type StoryDataType = { | 'conversationId' | 'deletedForEveryone' | 'reactions' + | 'readAt' | 'readStatus' | 'sendStateByConversationId' | 'source' @@ -140,7 +141,10 @@ type LoadStoryRepliesActionType = { type MarkStoryReadActionType = { type: typeof MARK_STORY_READ; - payload: string; + payload: { + messageId: string; + readAt: number; + }; }; type QueueStoryDownloadActionType = { @@ -456,7 +460,10 @@ function markStoryRead( dispatch({ type: MARK_STORY_READ, - payload: messageId, + payload: { + messageId, + readAt: storyReadDate, + }, }); }; } @@ -1157,6 +1164,7 @@ export function reducer( 'expireTimer', 'messageId', 'reactions', + 'readAt', 'readStatus', 'sendStateByConversationId', 'source', @@ -1228,12 +1236,15 @@ export function reducer( } if (action.type === MARK_STORY_READ) { + const { messageId, readAt } = action.payload; + return { ...state, stories: state.stories.map(story => { - if (story.messageId === action.payload) { + if (story.messageId === messageId) { return { ...story, + readAt, readStatus: ReadStatus.Viewed, }; } diff --git a/ts/state/selectors/stories.ts b/ts/state/selectors/stories.ts index e4da71fc4..016043a0f 100644 --- a/ts/state/selectors/stories.ts +++ b/ts/state/selectors/stories.ts @@ -73,6 +73,10 @@ function sortByRecencyAndUnread( return -1; } + if (storyA.storyView.readAt && storyB.storyView.readAt) { + return storyA.storyView.readAt > storyB.storyView.readAt ? -1 : 1; + } + return storyA.storyView.timestamp > storyB.storyView.timestamp ? -1 : 1; } @@ -145,10 +149,19 @@ export function getStoryView( 'title', ]); - const { attachment, timestamp, expirationStartTimestamp, expireTimer } = pick( - story, - ['attachment', 'timestamp', 'expirationStartTimestamp', 'expireTimer'] - ); + const { + attachment, + expirationStartTimestamp, + expireTimer, + readAt, + timestamp, + } = pick(story, [ + 'attachment', + 'expirationStartTimestamp', + 'expireTimer', + 'readAt', + 'timestamp', + ]); const { sendStateByConversationId } = story; let sendState: Array | undefined; @@ -182,6 +195,7 @@ export function getStoryView( isHidden: Boolean(sender.hideStory), isUnread: story.readStatus === ReadStatus.Unread, messageId: story.messageId, + readAt, sender, sendState, timestamp, diff --git a/ts/types/Stories.ts b/ts/types/Stories.ts index f9acf652b..b2d633841 100644 --- a/ts/types/Stories.ts +++ b/ts/types/Stories.ts @@ -73,6 +73,7 @@ export type StoryViewType = { isHidden?: boolean; isUnread?: boolean; messageId: string; + readAt?: number; sender: Pick< ConversationType, | 'acceptedMessageRequest'