From 3a1df01c9e3496e514315af46eed09179b602e06 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Fri, 22 Apr 2022 11:35:14 -0700 Subject: [PATCH] New 'unseenStatus' field for certain secondary message types --- ts/MessageSeenStatus.ts | 25 ++ ts/background.ts | 26 +- .../conversation/Timeline.stories.tsx | 10 +- ts/components/conversation/Timeline.tsx | 24 +- ts/model-types.d.ts | 5 +- ts/models/conversations.ts | 83 +++--- ts/models/messages.ts | 8 +- ts/sql/Interface.ts | 4 +- ts/sql/Server.ts | 77 +++++- ts/sql/migrations/56-add-unseen-to-message.ts | 88 ++++++ ts/sql/migrations/index.ts | 2 + ts/state/ducks/conversations.ts | 26 +- ts/state/selectors/conversations.ts | 18 +- ts/test-both/util/timelineUtil_test.ts | 2 +- ts/test-electron/sql/timelineFetches_test.ts | 18 +- .../state/ducks/conversations_test.ts | 18 +- ts/test-node/sql_migrations_test.ts | 254 ++++++++++++++++++ ts/util/getMessageIdForLogging.ts | 13 - ts/util/idForLogging.ts | 31 +++ ts/util/markConversationRead.ts | 10 +- ts/util/queueAttachmentDownloads.ts | 2 +- ts/util/timelineUtil.ts | 4 +- ts/views/conversation_view.ts | 5 +- 23 files changed, 610 insertions(+), 143 deletions(-) create mode 100644 ts/MessageSeenStatus.ts create mode 100644 ts/sql/migrations/56-add-unseen-to-message.ts delete mode 100644 ts/util/getMessageIdForLogging.ts create mode 100644 ts/util/idForLogging.ts diff --git a/ts/MessageSeenStatus.ts b/ts/MessageSeenStatus.ts new file mode 100644 index 000000000..40e43d088 --- /dev/null +++ b/ts/MessageSeenStatus.ts @@ -0,0 +1,25 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +/** + * `SeenStatus` represents either the idea that a message doesn't need to track its seen + * status, or the standard unseen/seen status pair. + * + * Unseen is a lot like unread - except that unseen messages only affect the placement + * of the last seen indicator and the count it shows. Unread messages will affect the + * left pane badging for conversations, as well as the overall badge count on the app. + */ +export enum SeenStatus { + NotApplicable = 0, + Unseen = 1, + Seen = 2, +} + +const STATUS_NUMBERS: Record = { + [SeenStatus.NotApplicable]: 0, + [SeenStatus.Unseen]: 1, + [SeenStatus.Seen]: 2, +}; + +export const maxSeenStatus = (a: SeenStatus, b: SeenStatus): SeenStatus => + STATUS_NUMBERS[a] > STATUS_NUMBERS[b] ? a : b; diff --git a/ts/background.ts b/ts/background.ts index 20a431c24..c287c22d1 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -143,6 +143,7 @@ import { ReactionSource } from './reactions/ReactionSource'; import { singleProtoJobQueue } from './jobs/singleProtoJobQueue'; import { getInitialState } from './state/getInitialState'; import { conversationJobQueue } from './jobs/conversationJobQueue'; +import { SeenStatus } from './MessageSeenStatus'; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; @@ -3052,22 +3053,24 @@ export async function startApp(): Promise { } return new window.Whisper.Message({ - source: window.textsecure.storage.user.getNumber(), - sourceUuid: window.textsecure.storage.user.getUuid()?.toString(), - sourceDevice: data.device, - sent_at: timestamp, - serverTimestamp: data.serverTimestamp, - received_at: data.receivedAtCounter, - received_at_ms: data.receivedAtDate, conversationId: descriptor.id, - timestamp, - type: 'outgoing', - sendStateByConversationId, - unidentifiedDeliveries, expirationStartTimestamp: Math.min( data.expirationStartTimestamp || timestamp, now ), + readStatus: ReadStatus.Read, + received_at_ms: data.receivedAtDate, + received_at: data.receivedAtCounter, + seenStatus: SeenStatus.NotApplicable, + sendStateByConversationId, + sent_at: timestamp, + serverTimestamp: data.serverTimestamp, + source: window.textsecure.storage.user.getNumber(), + sourceDevice: data.device, + sourceUuid: window.textsecure.storage.user.getUuid()?.toString(), + timestamp, + type: 'outgoing', + unidentifiedDeliveries, } as Partial as WhatIsThis); } @@ -3316,6 +3319,7 @@ export async function startApp(): Promise { unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived, type: data.message.isStory ? 'story' : 'incoming', readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, timestamp: data.timestamp, } as Partial as WhatIsThis); } diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index 4d000b9dc..8e600afc0 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -540,9 +540,9 @@ const useProps = (overrideProps: Partial = {}): PropsType => ({ items: overrideProps.items || Object.keys(items), scrollToIndex: overrideProps.scrollToIndex, scrollToIndexCounter: 0, - totalUnread: number('totalUnread', overrideProps.totalUnread || 0), - oldestUnreadIndex: - number('oldestUnreadIndex', overrideProps.oldestUnreadIndex || 0) || + totalUnseen: number('totalUnseen', overrideProps.totalUnseen || 0), + oldestUnseenIndex: + number('oldestUnseenIndex', overrideProps.oldestUnseenIndex || 0) || undefined, invitedContactsForNewlyCreatedGroup: overrideProps.invitedContactsForNewlyCreatedGroup || [], @@ -608,8 +608,8 @@ story.add('Empty (just hero)', () => { story.add('Last Seen', () => { const props = useProps({ - oldestUnreadIndex: 13, - totalUnread: 2, + oldestUnseenIndex: 13, + totalUnseen: 2, }); return ; diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index 9c0073315..fa49e0680 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -88,10 +88,10 @@ export type PropsDataType = { messageLoadingState?: TimelineMessageLoadingState; isNearBottom?: boolean; items: ReadonlyArray; - oldestUnreadIndex?: number; + oldestUnseenIndex?: number; scrollToIndex?: number; scrollToIndexCounter: number; - totalUnread: number; + totalUnseen: number; }; type PropsHousekeepingType = { @@ -342,7 +342,7 @@ export class Timeline extends React.Component< items, loadNewestMessages, messageLoadingState, - oldestUnreadIndex, + oldestUnseenIndex, selectMessage, } = this.props; const { newestBottomVisibleMessageId } = this.state; @@ -358,15 +358,15 @@ export class Timeline extends React.Component< if ( newestBottomVisibleMessageId && - isNumber(oldestUnreadIndex) && + isNumber(oldestUnseenIndex) && items.findIndex(item => item === newestBottomVisibleMessageId) < - oldestUnreadIndex + oldestUnseenIndex ) { if (setFocus) { - const messageId = items[oldestUnreadIndex]; + const messageId = items[oldestUnseenIndex]; selectMessage(messageId, id); } else { - this.scrollToItemIndex(oldestUnreadIndex); + this.scrollToItemIndex(oldestUnseenIndex); } } else if (haveNewest) { this.scrollToBottom(setFocus); @@ -790,7 +790,7 @@ export class Timeline extends React.Component< isSomeoneTyping, items, messageLoadingState, - oldestUnreadIndex, + oldestUnseenIndex, onBlock, onBlockAndReportSpam, onDelete, @@ -804,7 +804,7 @@ export class Timeline extends React.Component< reviewMessageRequestNameCollision, showContactModal, theme, - totalUnread, + totalUnseen, unblurAvatar, unreadCount, updateSharedGroups, @@ -898,17 +898,17 @@ export class Timeline extends React.Component< } let unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement; - if (oldestUnreadIndex === itemIndex) { + if (oldestUnseenIndex === itemIndex) { unreadIndicatorPlacement = UnreadIndicatorPlacement.JustAbove; messageNodes.push( ); - } else if (oldestUnreadIndex === nextItemIndex) { + } else if (oldestUnseenIndex === nextItemIndex) { unreadIndicatorPlacement = UnreadIndicatorPlacement.JustBelow; } diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index e59ed4bd7..ac8ccb964 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -35,6 +35,7 @@ import { ReactionSource } from './reactions/ReactionSource'; import AccessRequiredEnum = Proto.AccessControl.AccessRequired; import MemberRoleEnum = Proto.Member.Role; +import { SeenStatus } from './MessageSeenStatus'; export type WhatIsThis = any; @@ -219,8 +220,10 @@ export type MessageAttributesType = { sendHQImages?: boolean; - // Should only be present for incoming messages + // Should only be present for incoming messages and errors readStatus?: ReadStatus; + // Used for all kinds of notifications, as well as incoming messages + seenStatus?: SeenStatus; // Should only be present for outgoing messages sendStateByConversationId?: SendStateByConversationId; diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 0ab5b4384..3cd8ecb93 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -117,6 +117,8 @@ import { isMessageUnread } from '../util/isMessageUnread'; import type { SenderKeyTargetType } from '../util/sendToGroup'; import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue'; import { TimelineMessageLoadingState } from '../util/timelineUtil'; +import { SeenStatus } from '../MessageSeenStatus'; +import { getConversationIdForLogging } from '../util/idForLogging'; /* eslint-disable more/no-then */ window.Whisper = window.Whisper || {}; @@ -237,17 +239,7 @@ export class ConversationModel extends window.Backbone } idForLogging(): string { - if (isDirectConversation(this.attributes)) { - const uuid = this.get('uuid'); - const e164 = this.get('e164'); - return `${uuid || e164} (${this.id})`; - } - if (isGroupV2(this.attributes)) { - return `groupv2(${this.get('groupId')})`; - } - - const groupId = this.get('groupId'); - return `group(${groupId})`; + return getConversationIdForLogging(this.attributes); } // This is one of the few times that we want to collapse our uuid/e164 pair down into @@ -1508,8 +1500,8 @@ export class ConversationModel extends window.Backbone return; } - if (scrollToLatestUnread && metrics.oldestUnread) { - this.loadAndScroll(metrics.oldestUnread.id, { + if (scrollToLatestUnread && metrics.oldestUnseen) { + this.loadAndScroll(metrics.oldestUnseen.id, { disableScroll: !setFocus, }); return; @@ -2926,6 +2918,7 @@ export class ConversationModel extends window.Backbone received_at: receivedAtCounter, received_at_ms: receivedAt, readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, // TODO: DESKTOP-722 // this type does not fully implement the interface it is expected to } as unknown as MessageAttributesType; @@ -2968,6 +2961,7 @@ export class ConversationModel extends window.Backbone received_at: receivedAtCounter, received_at_ms: receivedAt, readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, // TODO: DESKTOP-722 // this type does not fully implement the interface it is expected to } as unknown as MessageAttributesType; @@ -3004,7 +2998,8 @@ export class ConversationModel extends window.Backbone received_at: window.Signal.Util.incrementMessageCounter(), received_at_ms: timestamp, key_changed: keyChangedId.toString(), - readStatus: ReadStatus.Unread, + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Unseen, schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY, // TODO: DESKTOP-722 // this type does not fully implement the interface it is expected to @@ -3057,14 +3052,15 @@ export class ConversationModel extends window.Backbone const timestamp = Date.now(); const message = { conversationId: this.id, - type: 'verified-change', - sent_at: lastMessage, - received_at: window.Signal.Util.incrementMessageCounter(), - received_at_ms: timestamp, - verifiedChanged: verifiedChangeId, - verified, local: options.local, readStatus: ReadStatus.Unread, + received_at_ms: timestamp, + received_at: window.Signal.Util.incrementMessageCounter(), + seenStatus: SeenStatus.Unseen, + sent_at: lastMessage, + type: 'verified-change', + verified, + verifiedChanged: verifiedChangeId, // TODO: DESKTOP-722 } as unknown as MessageAttributesType; @@ -3128,6 +3124,7 @@ export class ConversationModel extends window.Backbone receivedAtCounter || window.Signal.Util.incrementMessageCounter(), received_at_ms: timestamp, readStatus: unread ? ReadStatus.Unread : ReadStatus.Read, + seenStatus: unread ? SeenStatus.Unseen : SeenStatus.NotApplicable, callHistoryDetails: detailsToSave, // TODO: DESKTOP-722 } as unknown as MessageAttributesType; @@ -3192,6 +3189,7 @@ export class ConversationModel extends window.Backbone received_at: window.Signal.Util.incrementMessageCounter(), received_at_ms: now, readStatus: ReadStatus.Read, + seenStatus: SeenStatus.NotApplicable, changedId: conversationId || this.id, profileChange, // TODO: DESKTOP-722 @@ -3228,14 +3226,15 @@ export class ConversationModel extends window.Backbone ): Promise { const now = Date.now(); const message: Partial = { - ...extra, - conversationId: this.id, type, sent_at: now, received_at: window.Signal.Util.incrementMessageCounter(), received_at_ms: now, readStatus: ReadStatus.Read, + seenStatus: SeenStatus.NotApplicable, + + ...extra, }; const id = await window.Signal.Data.saveMessage( @@ -3363,6 +3362,8 @@ export class ConversationModel extends window.Backbone await Promise.all( convos.map(convo => { return convo.addNotification('change-number-notification', { + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Unseen, sourceUuid: sourceUuid.toString(), }); }) @@ -4037,6 +4038,8 @@ export class ConversationModel extends window.Backbone received_at_ms: now, expireTimer, recipients, + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.NotApplicable, sticker, bodyRanges: mentions, sendHQImages, @@ -4546,21 +4549,20 @@ export class ConversationModel extends window.Backbone window.Signal.Data.updateConversation(this.attributes); const model = new window.Whisper.Message({ - // Even though this isn't reflected to the user, we want to place the last seen - // indicator above it. We set it to 'unread' to trigger that placement. - readStatus: ReadStatus.Unread, conversationId: this.id, - // No type; 'incoming' messages are specially treated by conversation.markRead() - sent_at: sentAt, - received_at: receivedAt, - received_at_ms: receivedAtMS, - flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, expirationTimerUpdate: { expireTimer, source, fromSync: options.fromSync, fromGroupUpdate: options.fromGroupUpdate, }, + flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, + readStatus: ReadStatus.Unread, + received_at_ms: receivedAtMS, + received_at: receivedAt, + seenStatus: SeenStatus.Unseen, + sent_at: sentAt, + type: 'timer-notification', // TODO: DESKTOP-722 } as unknown as MessageAttributesType); @@ -4589,9 +4591,8 @@ export class ConversationModel extends window.Backbone const model = new window.Whisper.Message({ type: 'message-history-unsynced', - // Even though this isn't reflected to the user, we want to place the last seen - // indicator above it. We set it to 'unread' to trigger that placement. - readStatus: ReadStatus.Unread, + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.NotApplicable, conversationId: this.id, sent_at: timestamp, received_at: window.Signal.Util.incrementMessageCounter(), @@ -4633,12 +4634,14 @@ export class ConversationModel extends window.Backbone window.Signal.Data.updateConversation(this.attributes); const model = new window.Whisper.Message({ - group_update: { left: 'You' }, conversationId: this.id, - type: 'outgoing', - sent_at: now, - received_at: window.Signal.Util.incrementMessageCounter(), + group_update: { left: 'You' }, + readStatus: ReadStatus.Read, received_at_ms: now, + received_at: window.Signal.Util.incrementMessageCounter(), + seenStatus: SeenStatus.NotApplicable, + sent_at: now, + type: 'group', // TODO: DESKTOP-722 } as unknown as MessageAttributesType); @@ -4665,7 +4668,11 @@ export class ConversationModel extends window.Backbone async markRead( newestUnreadAt: number, - options: { readAt?: number; sendReadReceipts: boolean } = { + options: { + readAt?: number; + sendReadReceipts: boolean; + newestSentAt?: number; + } = { sendReadReceipts: true, } ): Promise { diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 649310a2d..81b3caaee 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -141,7 +141,7 @@ import { } from '../messages/helpers'; import type { ReplacementValuesType } from '../types/I18N'; import { viewOnceOpenJobQueue } from '../jobs/viewOnceOpenJobQueue'; -import { getMessageIdForLogging } from '../util/getMessageIdForLogging'; +import { getMessageIdForLogging } from '../util/idForLogging'; import { hasAttachmentDownloads } from '../util/hasAttachmentDownloads'; import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads'; import { findStoryMessage } from '../util/findStoryMessage'; @@ -152,6 +152,7 @@ import { getMessageById } from '../messages/getMessageById'; import { shouldDownloadStory } from '../util/shouldDownloadStory'; import { shouldShowStoriesView } from '../state/selectors/stories'; import type { ContactWithHydratedAvatar } from '../textsecure/SendMessage'; +import { SeenStatus } from '../MessageSeenStatus'; /* eslint-disable camelcase */ /* eslint-disable more/no-then */ @@ -2760,7 +2761,10 @@ export class MessageModel extends window.Backbone.Model { newReadStatus = ReadStatus.Read; } - message.set('readStatus', newReadStatus); + message.set({ + readStatus: newReadStatus, + seenStatus: SeenStatus.Seen, + }); changed = true; this.pendingMarkRead = Math.min( diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 38f6cba9b..98e51487a 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -50,8 +50,8 @@ export type MessageMetricsType = { export type ConversationMetricsType = { oldest?: MessageMetricsType; newest?: MessageMetricsType; - oldestUnread?: MessageMetricsType; - totalUnread: number; + oldestUnseen?: MessageMetricsType; + totalUnseen: number; }; export type ConversationType = ConversationAttributesType; export type EmojiType = { diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 22ba8ff81..6e5b5ca07 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -110,6 +110,7 @@ import type { UnprocessedType, UnprocessedUpdateType, } from './Interface'; +import { SeenStatus } from '../MessageSeenStatus'; type ConversationRow = Readonly<{ json: string; @@ -1737,6 +1738,20 @@ function saveMessageSync( expireTimer, expirationStartTimestamp, } = data; + let { seenStatus } = data; + + if (readStatus === ReadStatus.Unread && seenStatus !== SeenStatus.Unseen) { + log.warn( + `saveMessage: Message ${id}/${type} is unread but had seenStatus=${seenStatus}. Forcing to UnseenStatus.Unseen.` + ); + + // eslint-disable-next-line no-param-reassign + data = { + ...data, + seenStatus: SeenStatus.Unseen, + }; + seenStatus = SeenStatus.Unseen; + } const payload = { id, @@ -1762,6 +1777,7 @@ function saveMessageSync( storyId: storyId || null, type: type || null, readStatus: readStatus ?? null, + seenStatus: seenStatus ?? SeenStatus.NotApplicable, }; if (id && !forceSave) { @@ -1791,7 +1807,8 @@ function saveMessageSync( sourceDevice = $sourceDevice, storyId = $storyId, type = $type, - readStatus = $readStatus + readStatus = $readStatus, + seenStatus = $seenStatus WHERE id = $id; ` ).run(payload); @@ -1834,7 +1851,8 @@ function saveMessageSync( sourceDevice, storyId, type, - readStatus + readStatus, + seenStatus ) values ( $id, $json, @@ -1858,7 +1876,8 @@ function saveMessageSync( $sourceDevice, $storyId, $type, - $readStatus + $readStatus, + $seenStatus ); ` ).run({ @@ -2110,16 +2129,21 @@ async function getUnreadByConversationAndMarkRead({ UPDATE messages SET readStatus = ${ReadStatus.Read}, + seenStatus = ${SeenStatus.Seen}, json = json_patch(json, $jsonPatch) WHERE - readStatus = ${ReadStatus.Unread} AND conversationId = $conversationId AND + seenStatus = ${SeenStatus.Unseen} AND + isStory = 0 AND (${_storyIdPredicate(storyId, isGroup)}) AND received_at <= $newestUnreadAt; ` ).run({ conversationId, - jsonPatch: JSON.stringify({ readStatus: ReadStatus.Read }), + jsonPatch: JSON.stringify({ + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Seen, + }), newestUnreadAt, storyId: storyId || null, }); @@ -2644,7 +2668,7 @@ async function getLastConversationMessage({ return jsonToObject(row.json); } -function getOldestUnreadMessageForConversation( +function getOldestUnseenMessageForConversation( conversationId: string, storyId?: UUIDStringType, isGroup?: boolean @@ -2655,7 +2679,7 @@ function getOldestUnreadMessageForConversation( ` SELECT * FROM messages WHERE conversationId = $conversationId AND - readStatus = ${ReadStatus.Unread} AND + seenStatus = ${SeenStatus.Unseen} AND isStory IS 0 AND (${_storyIdPredicate(storyId, isGroup)}) ORDER BY received_at ASC, sent_at ASC @@ -2709,6 +2733,35 @@ function getTotalUnreadForConversationSync( return row['count(id)']; } +function getTotalUnseenForConversationSync( + conversationId: string, + storyId?: UUIDStringType, + isGroup?: boolean +): number { + const db = getInstance(); + const row = db + .prepare( + ` + SELECT count(id) + FROM messages + WHERE + conversationId = $conversationId AND + seenStatus = ${SeenStatus.Unseen} AND + isStory IS 0 AND + (${_storyIdPredicate(storyId, isGroup)}) + ` + ) + .get({ + conversationId, + storyId: storyId || null, + }); + + if (!row) { + throw new Error('getTotalUnseenForConversationSync: Unable to get count'); + } + + return row['count(id)']; +} async function getMessageMetricsForConversation( conversationId: string, @@ -2732,12 +2785,12 @@ function getMessageMetricsForConversationSync( storyId, isGroup ); - const oldestUnread = getOldestUnreadMessageForConversation( + const oldestUnseen = getOldestUnseenMessageForConversation( conversationId, storyId, isGroup ); - const totalUnread = getTotalUnreadForConversationSync( + const totalUnseen = getTotalUnseenForConversationSync( conversationId, storyId, isGroup @@ -2746,10 +2799,10 @@ function getMessageMetricsForConversationSync( return { oldest: oldest ? pick(oldest, ['received_at', 'sent_at', 'id']) : undefined, newest: newest ? pick(newest, ['received_at', 'sent_at', 'id']) : undefined, - oldestUnread: oldestUnread - ? pick(oldestUnread, ['received_at', 'sent_at', 'id']) + oldestUnseen: oldestUnseen + ? pick(oldestUnseen, ['received_at', 'sent_at', 'id']) : undefined, - totalUnread, + totalUnseen, }; } diff --git a/ts/sql/migrations/56-add-unseen-to-message.ts b/ts/sql/migrations/56-add-unseen-to-message.ts new file mode 100644 index 000000000..4a6927e38 --- /dev/null +++ b/ts/sql/migrations/56-add-unseen-to-message.ts @@ -0,0 +1,88 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Database } from 'better-sqlite3'; +import { ReadStatus } from '../../messages/MessageReadStatus'; +import { SeenStatus } from '../../MessageSeenStatus'; + +import type { LoggerType } from '../../types/Logging'; + +export default function updateToSchemaVersion56( + currentVersion: number, + db: Database, + logger: LoggerType +): void { + if (currentVersion >= 56) { + return; + } + + db.transaction(() => { + db.exec( + ` + --- Add column to messages table + + ALTER TABLE messages ADD COLUMN seenStatus NUMBER default 0; + + --- Add index to make searching on this field easy + + CREATE INDEX messages_unseen_no_story ON messages + (conversationId, seenStatus, isStory, received_at, sent_at) + WHERE + seenStatus IS NOT NULL; + + CREATE INDEX messages_unseen_with_story ON messages + (conversationId, seenStatus, isStory, storyId, received_at, sent_at) + WHERE + seenStatus IS NOT NULL; + + --- Update seenStatus to UnseenStatus.Unseen for certain messages + --- (NULL included because 'timer-notification' in 1:1 convos had type = NULL) + + UPDATE messages + SET + seenStatus = ${SeenStatus.Unseen} + WHERE + readStatus = ${ReadStatus.Unread} AND + ( + type IS NULL + OR + type IN ( + 'call-history', + 'change-number-notification', + 'chat-session-refreshed', + 'delivery-issue', + 'group', + 'incoming', + 'keychange', + 'timer-notification', + 'verified-change' + ) + ); + + --- Set readStatus to ReadStatus.Read for all other message types + + UPDATE messages + SET + readStatus = ${ReadStatus.Read} + WHERE + readStatus = ${ReadStatus.Unread} AND + type IS NOT NULL AND + type NOT IN ( + 'call-history', + 'change-number-notification', + 'chat-session-refreshed', + 'delivery-issue', + 'group', + 'incoming', + 'keychange', + 'timer-notification', + 'verified-change' + ); + ` + ); + + db.pragma('user_version = 56'); + })(); + + logger.info('updateToSchemaVersion56: success!'); +} diff --git a/ts/sql/migrations/index.ts b/ts/sql/migrations/index.ts index 862e3eee1..4f9196299 100644 --- a/ts/sql/migrations/index.ts +++ b/ts/sql/migrations/index.ts @@ -31,6 +31,7 @@ import updateToSchemaVersion52 from './52-optimize-stories'; import updateToSchemaVersion53 from './53-gv2-banned-members'; import updateToSchemaVersion54 from './54-unprocessed-received-at-counter'; import updateToSchemaVersion55 from './55-report-message-aci'; +import updateToSchemaVersion56 from './56-add-unseen-to-message'; function updateToSchemaVersion1( currentVersion: number, @@ -1925,6 +1926,7 @@ export const SCHEMA_VERSIONS = [ updateToSchemaVersion53, updateToSchemaVersion54, updateToSchemaVersion55, + updateToSchemaVersion56, ]; export function updateSchema(db: Database, logger: LoggerType): void { diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 8b66be748..139933775 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -234,8 +234,8 @@ type MessagePointerType = { type MessageMetricsType = { newest?: MessagePointerType; oldest?: MessagePointerType; - oldestUnread?: MessagePointerType; - totalUnread: number; + oldestUnseen?: MessagePointerType; + totalUnseen: number; }; export type MessageLookupType = { @@ -2673,7 +2673,7 @@ export function reducer( let metrics; if (messageIds.length === 0) { metrics = { - totalUnread: 0, + totalUnseen: 0, }; } else { metrics = { @@ -2791,7 +2791,7 @@ export function reducer( return state; } - let { newest, oldest, oldestUnread, totalUnread } = + let { newest, oldest, oldestUnseen, totalUnseen } = existingConversation.metrics; if (messages.length < 1) { @@ -2853,7 +2853,7 @@ export function reducer( const newMessageIds = difference(newIds, existingConversation.messageIds); const { isNearBottom } = existingConversation; - if ((!isNearBottom || !isActive) && !oldestUnread) { + if ((!isNearBottom || !isActive) && !oldestUnseen) { const oldestId = newMessageIds.find(messageId => { const message = lookup[messageId]; @@ -2861,7 +2861,7 @@ export function reducer( }); if (oldestId) { - oldestUnread = pick(lookup[oldestId], [ + oldestUnseen = pick(lookup[oldestId], [ 'id', 'received_at', 'sent_at', @@ -2869,14 +2869,14 @@ export function reducer( } } - // If this is a new incoming message, we'll increment our totalUnread count - if (isNewMessage && !isJustSent && oldestUnread) { + // If this is a new incoming message, we'll increment our totalUnseen count + if (isNewMessage && !isJustSent && oldestUnseen) { const newUnread: number = newMessageIds.reduce((sum, messageId) => { const message = lookup[messageId]; return sum + (message && isMessageUnread(message) ? 1 : 0); }, 0); - totalUnread = (totalUnread || 0) + newUnread; + totalUnseen = (totalUnseen || 0) + newUnread; } return { @@ -2896,8 +2896,8 @@ export function reducer( ...existingConversation.metrics, newest, oldest, - totalUnread, - oldestUnread, + totalUnseen, + oldestUnseen, }, }, }, @@ -2926,8 +2926,8 @@ export function reducer( ...existingConversation, metrics: { ...existingConversation.metrics, - oldestUnread: undefined, - totalUnread: 0, + oldestUnseen: undefined, + totalUnseen: 0, }, }, }, diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index d5177a783..a77c642e9 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -839,7 +839,7 @@ export function _conversationMessagesSelector( const lastId = messageIds.length === 0 ? undefined : messageIds[messageIds.length - 1]; - const { oldestUnread } = metrics; + const { oldestUnseen } = metrics; const haveNewest = !metrics.newest || !lastId || lastId === metrics.newest.id; const haveOldest = @@ -847,13 +847,13 @@ export function _conversationMessagesSelector( const items = messageIds; - const oldestUnreadIndex = oldestUnread - ? messageIds.findIndex(id => id === oldestUnread.id) + const oldestUnseenIndex = oldestUnseen + ? messageIds.findIndex(id => id === oldestUnseen.id) : undefined; const scrollToIndex = scrollToMessageId ? messageIds.findIndex(id => id === scrollToMessageId) : undefined; - const { totalUnread } = metrics; + const { totalUnseen } = metrics; return { haveNewest, @@ -861,14 +861,14 @@ export function _conversationMessagesSelector( isNearBottom, items, messageLoadingState, - oldestUnreadIndex: - isNumber(oldestUnreadIndex) && oldestUnreadIndex >= 0 - ? oldestUnreadIndex + oldestUnseenIndex: + isNumber(oldestUnseenIndex) && oldestUnseenIndex >= 0 + ? oldestUnseenIndex : undefined, scrollToIndex: isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined, scrollToIndexCounter: scrollToMessageCounter, - totalUnread, + totalUnseen, }; } @@ -901,7 +901,7 @@ export const getConversationMessagesSelector = createSelector( haveOldest: false, messageLoadingState: TimelineMessageLoadingState.DoingInitialLoad, scrollToIndexCounter: 0, - totalUnread: 0, + totalUnseen: 0, items: [], }; } diff --git a/ts/test-both/util/timelineUtil_test.ts b/ts/test-both/util/timelineUtil_test.ts index 43b3c2d74..4c1984abc 100644 --- a/ts/test-both/util/timelineUtil_test.ts +++ b/ts/test-both/util/timelineUtil_test.ts @@ -352,7 +352,7 @@ describe(' utilities', () => { const props = { ...defaultProps, items: fakeItems(10), - oldestUnreadIndex: 3, + oldestUnseenIndex: 3, }; assert.strictEqual( diff --git a/ts/test-electron/sql/timelineFetches_test.ts b/ts/test-electron/sql/timelineFetches_test.ts index 1319731ae..496dd6e7a 100644 --- a/ts/test-electron/sql/timelineFetches_test.ts +++ b/ts/test-electron/sql/timelineFetches_test.ts @@ -692,9 +692,9 @@ describe('sql/timelineFetches', () => { received_at: target - 8, timestamp: target - 8, }; - const oldestUnread: MessageAttributesType = { + const oldestUnseen: MessageAttributesType = { id: getUuid(), - body: 'oldestUnread', + body: 'oldestUnseen', type: 'incoming', conversationId, sent_at: target - 7, @@ -748,7 +748,7 @@ describe('sql/timelineFetches', () => { story, oldestInStory, oldest, - oldestUnread, + oldestUnseen, oldestStoryUnread, anotherUnread, newestInStory, @@ -769,11 +769,11 @@ describe('sql/timelineFetches', () => { ); assert.strictEqual(metricsInTimeline?.newest?.id, newest.id, 'newest'); assert.strictEqual( - metricsInTimeline?.oldestUnread?.id, - oldestUnread.id, - 'oldestUnread' + metricsInTimeline?.oldestUnseen?.id, + oldestUnseen.id, + 'oldestUnseen' ); - assert.strictEqual(metricsInTimeline?.totalUnread, 3, 'totalUnread'); + assert.strictEqual(metricsInTimeline?.totalUnseen, 3, 'totalUnseen'); const metricsInStory = await getMessageMetricsForConversation( conversationId, @@ -790,11 +790,11 @@ describe('sql/timelineFetches', () => { 'newestInStory' ); assert.strictEqual( - metricsInStory?.oldestUnread?.id, + metricsInStory?.oldestUnseen?.id, oldestStoryUnread.id, 'oldestStoryUnread' ); - assert.strictEqual(metricsInStory?.totalUnread, 1, 'totalUnread'); + assert.strictEqual(metricsInStory?.totalUnseen, 1, 'totalUnseen'); }); }); }); diff --git a/ts/test-electron/state/ducks/conversations_test.ts b/ts/test-electron/state/ducks/conversations_test.ts index 3345c2383..5f0e7a0e7 100644 --- a/ts/test-electron/state/ducks/conversations_test.ts +++ b/ts/test-electron/state/ducks/conversations_test.ts @@ -332,7 +332,7 @@ describe('both/state/ducks/conversations', () => { return { messageIds: [], metrics: { - totalUnread: 0, + totalUnseen: 0, }, scrollToMessageCounter: 0, }; @@ -1008,7 +1008,7 @@ describe('both/state/ducks/conversations', () => { ...getDefaultConversationMessage(), messageIds: [messageIdThree, messageIdTwo, messageId], metrics: { - totalUnread: 0, + totalUnseen: 0, }, }, }, @@ -1028,7 +1028,7 @@ describe('both/state/ducks/conversations', () => { ...getDefaultConversationMessage(), messageIds: [messageIdThree, messageIdTwo, messageId], metrics: { - totalUnread: 0, + totalUnseen: 0, newest: { id: messageId, received_at: time, @@ -1058,7 +1058,7 @@ describe('both/state/ducks/conversations', () => { ...getDefaultConversationMessage(), messageIds: [], metrics: { - totalUnread: 0, + totalUnseen: 0, newest: { id: messageId, received_at: time, @@ -1082,7 +1082,7 @@ describe('both/state/ducks/conversations', () => { messageIds: [], metrics: { newest: undefined, - totalUnread: 0, + totalUnseen: 0, }, }, }, @@ -1118,7 +1118,7 @@ describe('both/state/ducks/conversations', () => { ...getDefaultConversationMessage(), messageIds: [messageId, messageIdTwo, messageIdThree], metrics: { - totalUnread: 0, + totalUnseen: 0, }, }, }, @@ -1138,7 +1138,7 @@ describe('both/state/ducks/conversations', () => { ...getDefaultConversationMessage(), messageIds: [messageId, messageIdTwo, messageIdThree], metrics: { - totalUnread: 0, + totalUnseen: 0, oldest: { id: messageId, received_at: time, @@ -1168,7 +1168,7 @@ describe('both/state/ducks/conversations', () => { ...getDefaultConversationMessage(), messageIds: [], metrics: { - totalUnread: 0, + totalUnseen: 0, oldest: { id: messageId, received_at: time, @@ -1192,7 +1192,7 @@ describe('both/state/ducks/conversations', () => { messageIds: [], metrics: { oldest: undefined, - totalUnread: 0, + totalUnseen: 0, }, }, }, diff --git a/ts/test-node/sql_migrations_test.ts b/ts/test-node/sql_migrations_test.ts index 29ec49ae3..20169e9a0 100644 --- a/ts/test-node/sql_migrations_test.ts +++ b/ts/test-node/sql_migrations_test.ts @@ -13,6 +13,8 @@ import { insertJobSync, _storyIdPredicate, } from '../sql/Server'; +import { ReadStatus } from '../messages/MessageReadStatus'; +import { SeenStatus } from '../MessageSeenStatus'; const OUR_UUID = generateGuid(); @@ -1772,4 +1774,256 @@ describe('SQL migrations test', () => { ]); }); }); + + describe('updateToSchemaVersion56', () => { + it('updates unseenStatus for previously-unread messages', () => { + const MESSAGE_ID_1 = generateGuid(); + const MESSAGE_ID_2 = generateGuid(); + const MESSAGE_ID_3 = generateGuid(); + const MESSAGE_ID_4 = generateGuid(); + const MESSAGE_ID_5 = generateGuid(); + const MESSAGE_ID_6 = generateGuid(); + const MESSAGE_ID_7 = generateGuid(); + const MESSAGE_ID_8 = generateGuid(); + const MESSAGE_ID_9 = generateGuid(); + const MESSAGE_ID_10 = generateGuid(); + const MESSAGE_ID_11 = generateGuid(); + const CONVERSATION_ID = generateGuid(); + + updateToVersion(55); + + db.exec( + ` + INSERT INTO messages + (id, conversationId, type, readStatus) + VALUES + ('${MESSAGE_ID_1}', '${CONVERSATION_ID}', 'call-history', ${ReadStatus.Unread}), + ('${MESSAGE_ID_2}', '${CONVERSATION_ID}', 'change-number-notification', ${ReadStatus.Unread}), + ('${MESSAGE_ID_3}', '${CONVERSATION_ID}', 'chat-session-refreshed', ${ReadStatus.Unread}), + ('${MESSAGE_ID_4}', '${CONVERSATION_ID}', 'delivery-issue', ${ReadStatus.Unread}), + ('${MESSAGE_ID_5}', '${CONVERSATION_ID}', 'group', ${ReadStatus.Unread}), + ('${MESSAGE_ID_6}', '${CONVERSATION_ID}', 'incoming', ${ReadStatus.Unread}), + ('${MESSAGE_ID_7}', '${CONVERSATION_ID}', 'keychange', ${ReadStatus.Unread}), + ('${MESSAGE_ID_8}', '${CONVERSATION_ID}', 'timer-notification', ${ReadStatus.Unread}), + ('${MESSAGE_ID_9}', '${CONVERSATION_ID}', 'verified-change', ${ReadStatus.Unread}), + ('${MESSAGE_ID_10}', '${CONVERSATION_ID}', NULL, ${ReadStatus.Unread}), + ('${MESSAGE_ID_11}', '${CONVERSATION_ID}', 'other', ${ReadStatus.Unread}); + ` + ); + + assert.strictEqual( + db.prepare('SELECT COUNT(*) FROM messages;').pluck().get(), + 11, + 'starting total' + ); + assert.strictEqual( + db + .prepare( + `SELECT COUNT(*) FROM messages WHERE readStatus = ${ReadStatus.Unread};` + ) + .pluck() + .get(), + 11, + 'starting unread count' + ); + + updateToVersion(56); + + assert.strictEqual( + db.prepare('SELECT COUNT(*) FROM messages;').pluck().get(), + 11, + 'ending total' + ); + assert.strictEqual( + db + .prepare( + `SELECT COUNT(*) FROM messages WHERE readStatus = ${ReadStatus.Unread};` + ) + .pluck() + .get(), + 10, + 'ending unread count' + ); + assert.strictEqual( + db + .prepare( + `SELECT COUNT(*) FROM messages WHERE seenStatus = ${SeenStatus.Unseen};` + ) + .pluck() + .get(), + 10, + 'ending unseen count' + ); + + assert.strictEqual( + db + .prepare( + "SELECT readStatus FROM messages WHERE type = 'other' LIMIT 1;" + ) + .pluck() + .get(), + ReadStatus.Read, + "checking read status for lone 'other' message" + ); + }); + + it('creates usable index for getOldestUnseenMessageForConversation', () => { + updateToVersion(56); + + const first = db + .prepare( + ` + EXPLAIN QUERY PLAN + SELECT * FROM messages WHERE + conversationId = 'id-conversation-4' AND + seenStatus = ${SeenStatus.Unseen} AND + isStory IS 0 AND + NULL IS NULL + ORDER BY received_at ASC, sent_at ASC + LIMIT 1; + ` + ) + .all() + .map(({ detail }) => detail) + .join('\n'); + + assert.include(first, 'USING INDEX messages_unseen_no_story', 'first'); + assert.notInclude(first, 'TEMP B-TREE', 'first'); + assert.notInclude(first, 'SCAN', 'first'); + + const second = db + .prepare( + ` + EXPLAIN QUERY PLAN + SELECT * FROM messages WHERE + conversationId = 'id-conversation-4' AND + seenStatus = ${SeenStatus.Unseen} AND + isStory IS 0 AND + storyId IS 'id-story-4' + ORDER BY received_at ASC, sent_at ASC + LIMIT 1; + ` + ) + .all() + .map(({ detail }) => detail) + .join('\n'); + + assert.include( + second, + 'USING INDEX messages_unseen_with_story', + 'second' + ); + assert.notInclude(second, 'TEMP B-TREE', 'second'); + assert.notInclude(second, 'SCAN', 'second'); + }); + + it('creates usable index for getUnreadByConversationAndMarkRead', () => { + updateToVersion(56); + + const first = db + .prepare( + ` + EXPLAIN QUERY PLAN + UPDATE messages + SET + readStatus = ${ReadStatus.Read}, + seenStatus = ${SeenStatus.Seen}, + json = json_patch(json, '{ something: "one" }') + WHERE + conversationId = 'id-conversation-4' AND + seenStatus = ${SeenStatus.Unseen} AND + isStory = 0 AND + NULL IS NULL AND + received_at <= 2343233; + ` + ) + .all() + .map(({ detail }) => detail) + .join('\n'); + + assert.include(first, 'USING INDEX messages_unseen_no_story', 'first'); + assert.notInclude(first, 'TEMP B-TREE', 'first'); + assert.notInclude(first, 'SCAN', 'first'); + + const second = db + .prepare( + ` + EXPLAIN QUERY PLAN + UPDATE messages + SET + readStatus = ${ReadStatus.Read}, + seenStatus = ${SeenStatus.Seen}, + json = json_patch(json, '{ something: "one" }') + WHERE + conversationId = 'id-conversation-4' AND + seenStatus = ${SeenStatus.Unseen} AND + isStory = 0 AND + storyId IS 'id-story-4' AND + received_at <= 2343233; + ` + ) + .all() + .map(({ detail }) => detail) + .join('\n'); + + assert.include( + second, + 'USING INDEX messages_unseen_with_story', + 'second' + ); + assert.notInclude(second, 'TEMP B-TREE', 'second'); + assert.notInclude(second, 'SCAN', 'second'); + }); + + it('creates usable index for getTotalUnseenForConversationSync', () => { + updateToVersion(56); + + const first = db + .prepare( + ` + EXPLAIN QUERY PLAN + SELECT count(id) + FROM messages + WHERE + conversationId = 'id-conversation-4' AND + seenStatus = ${SeenStatus.Unseen} AND + isStory IS 0 AND + NULL IS NULL; + ` + ) + .all() + .map(({ detail }) => detail) + .join('\n'); + + // Weird, but we don't included received_at so it doesn't really matter + assert.include(first, 'USING INDEX messages_unseen_with_story', 'first'); + assert.notInclude(first, 'TEMP B-TREE', 'first'); + assert.notInclude(first, 'SCAN', 'first'); + + const second = db + .prepare( + ` + EXPLAIN QUERY PLAN + SELECT count(id) + FROM messages + WHERE + conversationId = 'id-conversation-4' AND + seenStatus = ${SeenStatus.Unseen} AND + isStory IS 0 AND + storyId IS 'id-story-4'; + ` + ) + .all() + .map(({ detail }) => detail) + .join('\n'); + + assert.include( + second, + 'USING INDEX messages_unseen_with_story', + 'second' + ); + assert.notInclude(second, 'TEMP B-TREE', 'second'); + assert.notInclude(second, 'SCAN', 'second'); + }); + }); }); diff --git a/ts/util/getMessageIdForLogging.ts b/ts/util/getMessageIdForLogging.ts deleted file mode 100644 index 2b1abc67a..000000000 --- a/ts/util/getMessageIdForLogging.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2020-2022 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type { MessageAttributesType } from '../model-types.d'; -import { getSource, getSourceDevice, getSourceUuid } from '../messages/helpers'; - -export function getMessageIdForLogging(message: MessageAttributesType): string { - const account = getSourceUuid(message) || getSource(message); - const device = getSourceDevice(message); - const timestamp = message.sent_at; - - return `${account}.${device} ${timestamp}`; -} diff --git a/ts/util/idForLogging.ts b/ts/util/idForLogging.ts new file mode 100644 index 000000000..d03d7fbe2 --- /dev/null +++ b/ts/util/idForLogging.ts @@ -0,0 +1,31 @@ +// Copyright 2020-2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { + ConversationAttributesType, + MessageAttributesType, +} from '../model-types.d'; +import { getSource, getSourceDevice, getSourceUuid } from '../messages/helpers'; +import { isDirectConversation, isGroupV2 } from './whatTypeOfConversation'; + +export function getMessageIdForLogging(message: MessageAttributesType): string { + const account = getSourceUuid(message) || getSource(message); + const device = getSourceDevice(message); + const timestamp = message.sent_at; + + return `${account}.${device} ${timestamp}`; +} + +export function getConversationIdForLogging( + conversation: ConversationAttributesType +): string { + if (isDirectConversation(conversation)) { + const { uuid, e164, id } = conversation; + return `${uuid || e164} (${id})`; + } + if (isGroupV2(conversation)) { + return `groupv2(${conversation.groupId})`; + } + + return `group(${conversation.groupId})`; +} diff --git a/ts/util/markConversationRead.ts b/ts/util/markConversationRead.ts index b23c6135e..498f5c0c8 100644 --- a/ts/util/markConversationRead.ts +++ b/ts/util/markConversationRead.ts @@ -8,11 +8,16 @@ import { readSyncJobQueue } from '../jobs/readSyncJobQueue'; import { notificationService } from '../services/notifications'; import { isGroup } from './whatTypeOfConversation'; import * as log from '../logging/log'; +import { getConversationIdForLogging } from './idForLogging'; export async function markConversationRead( conversationAttrs: ConversationAttributesType, newestUnreadAt: number, - options: { readAt?: number; sendReadReceipts: boolean } = { + options: { + readAt?: number; + sendReadReceipts: boolean; + newestSentAt?: number; + } = { sendReadReceipts: true, } ): Promise { @@ -32,7 +37,8 @@ export async function markConversationRead( ]); log.info('markConversationRead', { - conversationId, + conversationId: getConversationIdForLogging(conversationAttrs), + newestSentAt: options.newestSentAt, newestUnreadAt, unreadMessages: unreadMessages.length, unreadReactions: unreadReactions.length, diff --git a/ts/util/queueAttachmentDownloads.ts b/ts/util/queueAttachmentDownloads.ts index 403d6df34..87fb5f955 100644 --- a/ts/util/queueAttachmentDownloads.ts +++ b/ts/util/queueAttachmentDownloads.ts @@ -13,7 +13,7 @@ import type { import * as AttachmentDownloads from '../messageModifiers/AttachmentDownloads'; import * as log from '../logging/log'; import { isLongMessage } from '../types/MIME'; -import { getMessageIdForLogging } from './getMessageIdForLogging'; +import { getMessageIdForLogging } from './idForLogging'; import { copyStickerToAttachments, savePackMetadata, diff --git a/ts/util/timelineUtil.ts b/ts/util/timelineUtil.ts index 2d85e8b0d..87bea03d2 100644 --- a/ts/util/timelineUtil.ts +++ b/ts/util/timelineUtil.ts @@ -134,7 +134,7 @@ type ScrollAnchorBeforeUpdateProps = Readonly< | 'isSomeoneTyping' | 'items' | 'messageLoadingState' - | 'oldestUnreadIndex' + | 'oldestUnseenIndex' | 'scrollToIndex' | 'scrollToIndexCounter' > @@ -169,7 +169,7 @@ export function getScrollAnchorBeforeUpdate( if (props.isIncomingMessageRequest) { return ScrollAnchor.ChangeNothing; } - if (isNumber(props.oldestUnreadIndex)) { + if (isNumber(props.oldestUnseenIndex)) { return ScrollAnchor.ScrollToUnreadIndicator; } return ScrollAnchor.ScrollToBottom; diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index 903f8cfe8..e63a34668 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -493,7 +493,10 @@ export class ConversationView extends window.Backbone.View { throw new Error(`markMessageRead: failed to load message ${messageId}`); } - await this.model.markRead(message.get('received_at')); + await this.model.markRead(message.get('received_at'), { + newestSentAt: message.get('sent_at'), + sendReadReceipts: true, + }); }; const createMessageRequestResponseHandler =