New 'unseenStatus' field for certain secondary message types
This commit is contained in:
parent
ed9f54d7d6
commit
3a1df01c9e
|
@ -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, number> = {
|
||||
[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;
|
|
@ -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<void> {
|
|||
}
|
||||
|
||||
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<MessageAttributesType> as WhatIsThis);
|
||||
}
|
||||
|
||||
|
@ -3316,6 +3319,7 @@ export async function startApp(): Promise<void> {
|
|||
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
||||
type: data.message.isStory ? 'story' : 'incoming',
|
||||
readStatus: ReadStatus.Unread,
|
||||
seenStatus: SeenStatus.Unseen,
|
||||
timestamp: data.timestamp,
|
||||
} as Partial<MessageAttributesType> as WhatIsThis);
|
||||
}
|
||||
|
|
|
@ -540,9 +540,9 @@ const useProps = (overrideProps: Partial<PropsType> = {}): 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 <Timeline {...props} />;
|
||||
|
|
|
@ -88,10 +88,10 @@ export type PropsDataType = {
|
|||
messageLoadingState?: TimelineMessageLoadingState;
|
||||
isNearBottom?: boolean;
|
||||
items: ReadonlyArray<string>;
|
||||
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(
|
||||
<LastSeenIndicator
|
||||
key="last seen indicator"
|
||||
count={totalUnread}
|
||||
count={totalUnseen}
|
||||
i18n={i18n}
|
||||
ref={this.lastSeenIndicatorRef}
|
||||
/>
|
||||
);
|
||||
} else if (oldestUnreadIndex === nextItemIndex) {
|
||||
} else if (oldestUnseenIndex === nextItemIndex) {
|
||||
unreadIndicatorPlacement = UnreadIndicatorPlacement.JustBelow;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<string> {
|
||||
const now = Date.now();
|
||||
const message: Partial<MessageAttributesType> = {
|
||||
...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<void> {
|
||||
|
|
|
@ -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<MessageAttributesType> {
|
|||
newReadStatus = ReadStatus.Read;
|
||||
}
|
||||
|
||||
message.set('readStatus', newReadStatus);
|
||||
message.set({
|
||||
readStatus: newReadStatus,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
});
|
||||
changed = true;
|
||||
|
||||
this.pendingMarkRead = Math.min(
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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<Query>(
|
||||
`
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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!');
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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: [],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -352,7 +352,7 @@ describe('<Timeline> utilities', () => {
|
|||
const props = {
|
||||
...defaultProps,
|
||||
items: fakeItems(10),
|
||||
oldestUnreadIndex: 3,
|
||||
oldestUnseenIndex: 3,
|
||||
};
|
||||
|
||||
assert.strictEqual(
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}`;
|
||||
}
|
|
@ -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})`;
|
||||
}
|
|
@ -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<boolean> {
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -493,7 +493,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
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 =
|
||||
|
|
Loading…
Reference in New Issue