New 'unseenStatus' field for certain secondary message types
This commit is contained in:
parent
ed9f54d7d6
commit
3a1df01c9e
23 changed files with 610 additions and 143 deletions
25
ts/MessageSeenStatus.ts
Normal file
25
ts/MessageSeenStatus.ts
Normal file
|
@ -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 { singleProtoJobQueue } from './jobs/singleProtoJobQueue';
|
||||||
import { getInitialState } from './state/getInitialState';
|
import { getInitialState } from './state/getInitialState';
|
||||||
import { conversationJobQueue } from './jobs/conversationJobQueue';
|
import { conversationJobQueue } from './jobs/conversationJobQueue';
|
||||||
|
import { SeenStatus } from './MessageSeenStatus';
|
||||||
|
|
||||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||||
|
|
||||||
|
@ -3052,22 +3053,24 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return new window.Whisper.Message({
|
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,
|
conversationId: descriptor.id,
|
||||||
timestamp,
|
|
||||||
type: 'outgoing',
|
|
||||||
sendStateByConversationId,
|
|
||||||
unidentifiedDeliveries,
|
|
||||||
expirationStartTimestamp: Math.min(
|
expirationStartTimestamp: Math.min(
|
||||||
data.expirationStartTimestamp || timestamp,
|
data.expirationStartTimestamp || timestamp,
|
||||||
now
|
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);
|
} as Partial<MessageAttributesType> as WhatIsThis);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3316,6 +3319,7 @@ export async function startApp(): Promise<void> {
|
||||||
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
||||||
type: data.message.isStory ? 'story' : 'incoming',
|
type: data.message.isStory ? 'story' : 'incoming',
|
||||||
readStatus: ReadStatus.Unread,
|
readStatus: ReadStatus.Unread,
|
||||||
|
seenStatus: SeenStatus.Unseen,
|
||||||
timestamp: data.timestamp,
|
timestamp: data.timestamp,
|
||||||
} as Partial<MessageAttributesType> as WhatIsThis);
|
} as Partial<MessageAttributesType> as WhatIsThis);
|
||||||
}
|
}
|
||||||
|
|
|
@ -540,9 +540,9 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
items: overrideProps.items || Object.keys(items),
|
items: overrideProps.items || Object.keys(items),
|
||||||
scrollToIndex: overrideProps.scrollToIndex,
|
scrollToIndex: overrideProps.scrollToIndex,
|
||||||
scrollToIndexCounter: 0,
|
scrollToIndexCounter: 0,
|
||||||
totalUnread: number('totalUnread', overrideProps.totalUnread || 0),
|
totalUnseen: number('totalUnseen', overrideProps.totalUnseen || 0),
|
||||||
oldestUnreadIndex:
|
oldestUnseenIndex:
|
||||||
number('oldestUnreadIndex', overrideProps.oldestUnreadIndex || 0) ||
|
number('oldestUnseenIndex', overrideProps.oldestUnseenIndex || 0) ||
|
||||||
undefined,
|
undefined,
|
||||||
invitedContactsForNewlyCreatedGroup:
|
invitedContactsForNewlyCreatedGroup:
|
||||||
overrideProps.invitedContactsForNewlyCreatedGroup || [],
|
overrideProps.invitedContactsForNewlyCreatedGroup || [],
|
||||||
|
@ -608,8 +608,8 @@ story.add('Empty (just hero)', () => {
|
||||||
|
|
||||||
story.add('Last Seen', () => {
|
story.add('Last Seen', () => {
|
||||||
const props = useProps({
|
const props = useProps({
|
||||||
oldestUnreadIndex: 13,
|
oldestUnseenIndex: 13,
|
||||||
totalUnread: 2,
|
totalUnseen: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <Timeline {...props} />;
|
return <Timeline {...props} />;
|
||||||
|
|
|
@ -88,10 +88,10 @@ export type PropsDataType = {
|
||||||
messageLoadingState?: TimelineMessageLoadingState;
|
messageLoadingState?: TimelineMessageLoadingState;
|
||||||
isNearBottom?: boolean;
|
isNearBottom?: boolean;
|
||||||
items: ReadonlyArray<string>;
|
items: ReadonlyArray<string>;
|
||||||
oldestUnreadIndex?: number;
|
oldestUnseenIndex?: number;
|
||||||
scrollToIndex?: number;
|
scrollToIndex?: number;
|
||||||
scrollToIndexCounter: number;
|
scrollToIndexCounter: number;
|
||||||
totalUnread: number;
|
totalUnseen: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsHousekeepingType = {
|
type PropsHousekeepingType = {
|
||||||
|
@ -342,7 +342,7 @@ export class Timeline extends React.Component<
|
||||||
items,
|
items,
|
||||||
loadNewestMessages,
|
loadNewestMessages,
|
||||||
messageLoadingState,
|
messageLoadingState,
|
||||||
oldestUnreadIndex,
|
oldestUnseenIndex,
|
||||||
selectMessage,
|
selectMessage,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { newestBottomVisibleMessageId } = this.state;
|
const { newestBottomVisibleMessageId } = this.state;
|
||||||
|
@ -358,15 +358,15 @@ export class Timeline extends React.Component<
|
||||||
|
|
||||||
if (
|
if (
|
||||||
newestBottomVisibleMessageId &&
|
newestBottomVisibleMessageId &&
|
||||||
isNumber(oldestUnreadIndex) &&
|
isNumber(oldestUnseenIndex) &&
|
||||||
items.findIndex(item => item === newestBottomVisibleMessageId) <
|
items.findIndex(item => item === newestBottomVisibleMessageId) <
|
||||||
oldestUnreadIndex
|
oldestUnseenIndex
|
||||||
) {
|
) {
|
||||||
if (setFocus) {
|
if (setFocus) {
|
||||||
const messageId = items[oldestUnreadIndex];
|
const messageId = items[oldestUnseenIndex];
|
||||||
selectMessage(messageId, id);
|
selectMessage(messageId, id);
|
||||||
} else {
|
} else {
|
||||||
this.scrollToItemIndex(oldestUnreadIndex);
|
this.scrollToItemIndex(oldestUnseenIndex);
|
||||||
}
|
}
|
||||||
} else if (haveNewest) {
|
} else if (haveNewest) {
|
||||||
this.scrollToBottom(setFocus);
|
this.scrollToBottom(setFocus);
|
||||||
|
@ -790,7 +790,7 @@ export class Timeline extends React.Component<
|
||||||
isSomeoneTyping,
|
isSomeoneTyping,
|
||||||
items,
|
items,
|
||||||
messageLoadingState,
|
messageLoadingState,
|
||||||
oldestUnreadIndex,
|
oldestUnseenIndex,
|
||||||
onBlock,
|
onBlock,
|
||||||
onBlockAndReportSpam,
|
onBlockAndReportSpam,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
@ -804,7 +804,7 @@ export class Timeline extends React.Component<
|
||||||
reviewMessageRequestNameCollision,
|
reviewMessageRequestNameCollision,
|
||||||
showContactModal,
|
showContactModal,
|
||||||
theme,
|
theme,
|
||||||
totalUnread,
|
totalUnseen,
|
||||||
unblurAvatar,
|
unblurAvatar,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
updateSharedGroups,
|
updateSharedGroups,
|
||||||
|
@ -898,17 +898,17 @@ export class Timeline extends React.Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
let unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement;
|
let unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement;
|
||||||
if (oldestUnreadIndex === itemIndex) {
|
if (oldestUnseenIndex === itemIndex) {
|
||||||
unreadIndicatorPlacement = UnreadIndicatorPlacement.JustAbove;
|
unreadIndicatorPlacement = UnreadIndicatorPlacement.JustAbove;
|
||||||
messageNodes.push(
|
messageNodes.push(
|
||||||
<LastSeenIndicator
|
<LastSeenIndicator
|
||||||
key="last seen indicator"
|
key="last seen indicator"
|
||||||
count={totalUnread}
|
count={totalUnseen}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
ref={this.lastSeenIndicatorRef}
|
ref={this.lastSeenIndicatorRef}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (oldestUnreadIndex === nextItemIndex) {
|
} else if (oldestUnseenIndex === nextItemIndex) {
|
||||||
unreadIndicatorPlacement = UnreadIndicatorPlacement.JustBelow;
|
unreadIndicatorPlacement = UnreadIndicatorPlacement.JustBelow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
ts/model-types.d.ts
vendored
5
ts/model-types.d.ts
vendored
|
@ -35,6 +35,7 @@ import { ReactionSource } from './reactions/ReactionSource';
|
||||||
|
|
||||||
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
||||||
import MemberRoleEnum = Proto.Member.Role;
|
import MemberRoleEnum = Proto.Member.Role;
|
||||||
|
import { SeenStatus } from './MessageSeenStatus';
|
||||||
|
|
||||||
export type WhatIsThis = any;
|
export type WhatIsThis = any;
|
||||||
|
|
||||||
|
@ -219,8 +220,10 @@ export type MessageAttributesType = {
|
||||||
|
|
||||||
sendHQImages?: boolean;
|
sendHQImages?: boolean;
|
||||||
|
|
||||||
// Should only be present for incoming messages
|
// Should only be present for incoming messages and errors
|
||||||
readStatus?: ReadStatus;
|
readStatus?: ReadStatus;
|
||||||
|
// Used for all kinds of notifications, as well as incoming messages
|
||||||
|
seenStatus?: SeenStatus;
|
||||||
|
|
||||||
// Should only be present for outgoing messages
|
// Should only be present for outgoing messages
|
||||||
sendStateByConversationId?: SendStateByConversationId;
|
sendStateByConversationId?: SendStateByConversationId;
|
||||||
|
|
|
@ -117,6 +117,8 @@ import { isMessageUnread } from '../util/isMessageUnread';
|
||||||
import type { SenderKeyTargetType } from '../util/sendToGroup';
|
import type { SenderKeyTargetType } from '../util/sendToGroup';
|
||||||
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||||
import { TimelineMessageLoadingState } from '../util/timelineUtil';
|
import { TimelineMessageLoadingState } from '../util/timelineUtil';
|
||||||
|
import { SeenStatus } from '../MessageSeenStatus';
|
||||||
|
import { getConversationIdForLogging } from '../util/idForLogging';
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
@ -237,17 +239,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
idForLogging(): string {
|
idForLogging(): string {
|
||||||
if (isDirectConversation(this.attributes)) {
|
return getConversationIdForLogging(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})`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is one of the few times that we want to collapse our uuid/e164 pair down into
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollToLatestUnread && metrics.oldestUnread) {
|
if (scrollToLatestUnread && metrics.oldestUnseen) {
|
||||||
this.loadAndScroll(metrics.oldestUnread.id, {
|
this.loadAndScroll(metrics.oldestUnseen.id, {
|
||||||
disableScroll: !setFocus,
|
disableScroll: !setFocus,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -2926,6 +2918,7 @@ export class ConversationModel extends window.Backbone
|
||||||
received_at: receivedAtCounter,
|
received_at: receivedAtCounter,
|
||||||
received_at_ms: receivedAt,
|
received_at_ms: receivedAt,
|
||||||
readStatus: ReadStatus.Unread,
|
readStatus: ReadStatus.Unread,
|
||||||
|
seenStatus: SeenStatus.Unseen,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
// this type does not fully implement the interface it is expected to
|
// this type does not fully implement the interface it is expected to
|
||||||
} as unknown as MessageAttributesType;
|
} as unknown as MessageAttributesType;
|
||||||
|
@ -2968,6 +2961,7 @@ export class ConversationModel extends window.Backbone
|
||||||
received_at: receivedAtCounter,
|
received_at: receivedAtCounter,
|
||||||
received_at_ms: receivedAt,
|
received_at_ms: receivedAt,
|
||||||
readStatus: ReadStatus.Unread,
|
readStatus: ReadStatus.Unread,
|
||||||
|
seenStatus: SeenStatus.Unseen,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
// this type does not fully implement the interface it is expected to
|
// this type does not fully implement the interface it is expected to
|
||||||
} as unknown as MessageAttributesType;
|
} as unknown as MessageAttributesType;
|
||||||
|
@ -3004,7 +2998,8 @@ export class ConversationModel extends window.Backbone
|
||||||
received_at: window.Signal.Util.incrementMessageCounter(),
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
received_at_ms: timestamp,
|
received_at_ms: timestamp,
|
||||||
key_changed: keyChangedId.toString(),
|
key_changed: keyChangedId.toString(),
|
||||||
readStatus: ReadStatus.Unread,
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Unseen,
|
||||||
schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY,
|
schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
// this type does not fully implement the interface it is expected to
|
// 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 timestamp = Date.now();
|
||||||
const message = {
|
const message = {
|
||||||
conversationId: this.id,
|
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,
|
local: options.local,
|
||||||
readStatus: ReadStatus.Unread,
|
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
|
// TODO: DESKTOP-722
|
||||||
} as unknown as MessageAttributesType;
|
} as unknown as MessageAttributesType;
|
||||||
|
|
||||||
|
@ -3128,6 +3124,7 @@ export class ConversationModel extends window.Backbone
|
||||||
receivedAtCounter || window.Signal.Util.incrementMessageCounter(),
|
receivedAtCounter || window.Signal.Util.incrementMessageCounter(),
|
||||||
received_at_ms: timestamp,
|
received_at_ms: timestamp,
|
||||||
readStatus: unread ? ReadStatus.Unread : ReadStatus.Read,
|
readStatus: unread ? ReadStatus.Unread : ReadStatus.Read,
|
||||||
|
seenStatus: unread ? SeenStatus.Unseen : SeenStatus.NotApplicable,
|
||||||
callHistoryDetails: detailsToSave,
|
callHistoryDetails: detailsToSave,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
} as unknown as MessageAttributesType;
|
} as unknown as MessageAttributesType;
|
||||||
|
@ -3192,6 +3189,7 @@ export class ConversationModel extends window.Backbone
|
||||||
received_at: window.Signal.Util.incrementMessageCounter(),
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
received_at_ms: now,
|
received_at_ms: now,
|
||||||
readStatus: ReadStatus.Read,
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.NotApplicable,
|
||||||
changedId: conversationId || this.id,
|
changedId: conversationId || this.id,
|
||||||
profileChange,
|
profileChange,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
|
@ -3228,14 +3226,15 @@ export class ConversationModel extends window.Backbone
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const message: Partial<MessageAttributesType> = {
|
const message: Partial<MessageAttributesType> = {
|
||||||
...extra,
|
|
||||||
|
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type,
|
type,
|
||||||
sent_at: now,
|
sent_at: now,
|
||||||
received_at: window.Signal.Util.incrementMessageCounter(),
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
received_at_ms: now,
|
received_at_ms: now,
|
||||||
readStatus: ReadStatus.Read,
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.NotApplicable,
|
||||||
|
|
||||||
|
...extra,
|
||||||
};
|
};
|
||||||
|
|
||||||
const id = await window.Signal.Data.saveMessage(
|
const id = await window.Signal.Data.saveMessage(
|
||||||
|
@ -3363,6 +3362,8 @@ export class ConversationModel extends window.Backbone
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
convos.map(convo => {
|
convos.map(convo => {
|
||||||
return convo.addNotification('change-number-notification', {
|
return convo.addNotification('change-number-notification', {
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Unseen,
|
||||||
sourceUuid: sourceUuid.toString(),
|
sourceUuid: sourceUuid.toString(),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -4037,6 +4038,8 @@ export class ConversationModel extends window.Backbone
|
||||||
received_at_ms: now,
|
received_at_ms: now,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
recipients,
|
recipients,
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.NotApplicable,
|
||||||
sticker,
|
sticker,
|
||||||
bodyRanges: mentions,
|
bodyRanges: mentions,
|
||||||
sendHQImages,
|
sendHQImages,
|
||||||
|
@ -4546,21 +4549,20 @@ export class ConversationModel extends window.Backbone
|
||||||
window.Signal.Data.updateConversation(this.attributes);
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
|
||||||
const model = new window.Whisper.Message({
|
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,
|
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: {
|
expirationTimerUpdate: {
|
||||||
expireTimer,
|
expireTimer,
|
||||||
source,
|
source,
|
||||||
fromSync: options.fromSync,
|
fromSync: options.fromSync,
|
||||||
fromGroupUpdate: options.fromGroupUpdate,
|
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
|
// TODO: DESKTOP-722
|
||||||
} as unknown as MessageAttributesType);
|
} as unknown as MessageAttributesType);
|
||||||
|
|
||||||
|
@ -4589,9 +4591,8 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
const model = new window.Whisper.Message({
|
const model = new window.Whisper.Message({
|
||||||
type: 'message-history-unsynced',
|
type: 'message-history-unsynced',
|
||||||
// Even though this isn't reflected to the user, we want to place the last seen
|
readStatus: ReadStatus.Read,
|
||||||
// indicator above it. We set it to 'unread' to trigger that placement.
|
seenStatus: SeenStatus.NotApplicable,
|
||||||
readStatus: ReadStatus.Unread,
|
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
sent_at: timestamp,
|
sent_at: timestamp,
|
||||||
received_at: window.Signal.Util.incrementMessageCounter(),
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
@ -4633,12 +4634,14 @@ export class ConversationModel extends window.Backbone
|
||||||
window.Signal.Data.updateConversation(this.attributes);
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
|
||||||
const model = new window.Whisper.Message({
|
const model = new window.Whisper.Message({
|
||||||
group_update: { left: 'You' },
|
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'outgoing',
|
group_update: { left: 'You' },
|
||||||
sent_at: now,
|
readStatus: ReadStatus.Read,
|
||||||
received_at: window.Signal.Util.incrementMessageCounter(),
|
|
||||||
received_at_ms: now,
|
received_at_ms: now,
|
||||||
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
seenStatus: SeenStatus.NotApplicable,
|
||||||
|
sent_at: now,
|
||||||
|
type: 'group',
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
} as unknown as MessageAttributesType);
|
} as unknown as MessageAttributesType);
|
||||||
|
|
||||||
|
@ -4665,7 +4668,11 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
async markRead(
|
async markRead(
|
||||||
newestUnreadAt: number,
|
newestUnreadAt: number,
|
||||||
options: { readAt?: number; sendReadReceipts: boolean } = {
|
options: {
|
||||||
|
readAt?: number;
|
||||||
|
sendReadReceipts: boolean;
|
||||||
|
newestSentAt?: number;
|
||||||
|
} = {
|
||||||
sendReadReceipts: true,
|
sendReadReceipts: true,
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
|
@ -141,7 +141,7 @@ import {
|
||||||
} from '../messages/helpers';
|
} from '../messages/helpers';
|
||||||
import type { ReplacementValuesType } from '../types/I18N';
|
import type { ReplacementValuesType } from '../types/I18N';
|
||||||
import { viewOnceOpenJobQueue } from '../jobs/viewOnceOpenJobQueue';
|
import { viewOnceOpenJobQueue } from '../jobs/viewOnceOpenJobQueue';
|
||||||
import { getMessageIdForLogging } from '../util/getMessageIdForLogging';
|
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||||
import { hasAttachmentDownloads } from '../util/hasAttachmentDownloads';
|
import { hasAttachmentDownloads } from '../util/hasAttachmentDownloads';
|
||||||
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
|
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
|
||||||
import { findStoryMessage } from '../util/findStoryMessage';
|
import { findStoryMessage } from '../util/findStoryMessage';
|
||||||
|
@ -152,6 +152,7 @@ import { getMessageById } from '../messages/getMessageById';
|
||||||
import { shouldDownloadStory } from '../util/shouldDownloadStory';
|
import { shouldDownloadStory } from '../util/shouldDownloadStory';
|
||||||
import { shouldShowStoriesView } from '../state/selectors/stories';
|
import { shouldShowStoriesView } from '../state/selectors/stories';
|
||||||
import type { ContactWithHydratedAvatar } from '../textsecure/SendMessage';
|
import type { ContactWithHydratedAvatar } from '../textsecure/SendMessage';
|
||||||
|
import { SeenStatus } from '../MessageSeenStatus';
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
|
@ -2760,7 +2761,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
newReadStatus = ReadStatus.Read;
|
newReadStatus = ReadStatus.Read;
|
||||||
}
|
}
|
||||||
|
|
||||||
message.set('readStatus', newReadStatus);
|
message.set({
|
||||||
|
readStatus: newReadStatus,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
|
});
|
||||||
changed = true;
|
changed = true;
|
||||||
|
|
||||||
this.pendingMarkRead = Math.min(
|
this.pendingMarkRead = Math.min(
|
||||||
|
|
|
@ -50,8 +50,8 @@ export type MessageMetricsType = {
|
||||||
export type ConversationMetricsType = {
|
export type ConversationMetricsType = {
|
||||||
oldest?: MessageMetricsType;
|
oldest?: MessageMetricsType;
|
||||||
newest?: MessageMetricsType;
|
newest?: MessageMetricsType;
|
||||||
oldestUnread?: MessageMetricsType;
|
oldestUnseen?: MessageMetricsType;
|
||||||
totalUnread: number;
|
totalUnseen: number;
|
||||||
};
|
};
|
||||||
export type ConversationType = ConversationAttributesType;
|
export type ConversationType = ConversationAttributesType;
|
||||||
export type EmojiType = {
|
export type EmojiType = {
|
||||||
|
|
|
@ -110,6 +110,7 @@ import type {
|
||||||
UnprocessedType,
|
UnprocessedType,
|
||||||
UnprocessedUpdateType,
|
UnprocessedUpdateType,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
|
import { SeenStatus } from '../MessageSeenStatus';
|
||||||
|
|
||||||
type ConversationRow = Readonly<{
|
type ConversationRow = Readonly<{
|
||||||
json: string;
|
json: string;
|
||||||
|
@ -1737,6 +1738,20 @@ function saveMessageSync(
|
||||||
expireTimer,
|
expireTimer,
|
||||||
expirationStartTimestamp,
|
expirationStartTimestamp,
|
||||||
} = data;
|
} = 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 = {
|
const payload = {
|
||||||
id,
|
id,
|
||||||
|
@ -1762,6 +1777,7 @@ function saveMessageSync(
|
||||||
storyId: storyId || null,
|
storyId: storyId || null,
|
||||||
type: type || null,
|
type: type || null,
|
||||||
readStatus: readStatus ?? null,
|
readStatus: readStatus ?? null,
|
||||||
|
seenStatus: seenStatus ?? SeenStatus.NotApplicable,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (id && !forceSave) {
|
if (id && !forceSave) {
|
||||||
|
@ -1791,7 +1807,8 @@ function saveMessageSync(
|
||||||
sourceDevice = $sourceDevice,
|
sourceDevice = $sourceDevice,
|
||||||
storyId = $storyId,
|
storyId = $storyId,
|
||||||
type = $type,
|
type = $type,
|
||||||
readStatus = $readStatus
|
readStatus = $readStatus,
|
||||||
|
seenStatus = $seenStatus
|
||||||
WHERE id = $id;
|
WHERE id = $id;
|
||||||
`
|
`
|
||||||
).run(payload);
|
).run(payload);
|
||||||
|
@ -1834,7 +1851,8 @@ function saveMessageSync(
|
||||||
sourceDevice,
|
sourceDevice,
|
||||||
storyId,
|
storyId,
|
||||||
type,
|
type,
|
||||||
readStatus
|
readStatus,
|
||||||
|
seenStatus
|
||||||
) values (
|
) values (
|
||||||
$id,
|
$id,
|
||||||
$json,
|
$json,
|
||||||
|
@ -1858,7 +1876,8 @@ function saveMessageSync(
|
||||||
$sourceDevice,
|
$sourceDevice,
|
||||||
$storyId,
|
$storyId,
|
||||||
$type,
|
$type,
|
||||||
$readStatus
|
$readStatus,
|
||||||
|
$seenStatus
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
).run({
|
).run({
|
||||||
|
@ -2110,16 +2129,21 @@ async function getUnreadByConversationAndMarkRead({
|
||||||
UPDATE messages
|
UPDATE messages
|
||||||
SET
|
SET
|
||||||
readStatus = ${ReadStatus.Read},
|
readStatus = ${ReadStatus.Read},
|
||||||
|
seenStatus = ${SeenStatus.Seen},
|
||||||
json = json_patch(json, $jsonPatch)
|
json = json_patch(json, $jsonPatch)
|
||||||
WHERE
|
WHERE
|
||||||
readStatus = ${ReadStatus.Unread} AND
|
|
||||||
conversationId = $conversationId AND
|
conversationId = $conversationId AND
|
||||||
|
seenStatus = ${SeenStatus.Unseen} AND
|
||||||
|
isStory = 0 AND
|
||||||
(${_storyIdPredicate(storyId, isGroup)}) AND
|
(${_storyIdPredicate(storyId, isGroup)}) AND
|
||||||
received_at <= $newestUnreadAt;
|
received_at <= $newestUnreadAt;
|
||||||
`
|
`
|
||||||
).run({
|
).run({
|
||||||
conversationId,
|
conversationId,
|
||||||
jsonPatch: JSON.stringify({ readStatus: ReadStatus.Read }),
|
jsonPatch: JSON.stringify({
|
||||||
|
readStatus: ReadStatus.Read,
|
||||||
|
seenStatus: SeenStatus.Seen,
|
||||||
|
}),
|
||||||
newestUnreadAt,
|
newestUnreadAt,
|
||||||
storyId: storyId || null,
|
storyId: storyId || null,
|
||||||
});
|
});
|
||||||
|
@ -2644,7 +2668,7 @@ async function getLastConversationMessage({
|
||||||
return jsonToObject(row.json);
|
return jsonToObject(row.json);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOldestUnreadMessageForConversation(
|
function getOldestUnseenMessageForConversation(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
storyId?: UUIDStringType,
|
storyId?: UUIDStringType,
|
||||||
isGroup?: boolean
|
isGroup?: boolean
|
||||||
|
@ -2655,7 +2679,7 @@ function getOldestUnreadMessageForConversation(
|
||||||
`
|
`
|
||||||
SELECT * FROM messages WHERE
|
SELECT * FROM messages WHERE
|
||||||
conversationId = $conversationId AND
|
conversationId = $conversationId AND
|
||||||
readStatus = ${ReadStatus.Unread} AND
|
seenStatus = ${SeenStatus.Unseen} AND
|
||||||
isStory IS 0 AND
|
isStory IS 0 AND
|
||||||
(${_storyIdPredicate(storyId, isGroup)})
|
(${_storyIdPredicate(storyId, isGroup)})
|
||||||
ORDER BY received_at ASC, sent_at ASC
|
ORDER BY received_at ASC, sent_at ASC
|
||||||
|
@ -2709,6 +2733,35 @@ function getTotalUnreadForConversationSync(
|
||||||
|
|
||||||
return row['count(id)'];
|
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(
|
async function getMessageMetricsForConversation(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
|
@ -2732,12 +2785,12 @@ function getMessageMetricsForConversationSync(
|
||||||
storyId,
|
storyId,
|
||||||
isGroup
|
isGroup
|
||||||
);
|
);
|
||||||
const oldestUnread = getOldestUnreadMessageForConversation(
|
const oldestUnseen = getOldestUnseenMessageForConversation(
|
||||||
conversationId,
|
conversationId,
|
||||||
storyId,
|
storyId,
|
||||||
isGroup
|
isGroup
|
||||||
);
|
);
|
||||||
const totalUnread = getTotalUnreadForConversationSync(
|
const totalUnseen = getTotalUnseenForConversationSync(
|
||||||
conversationId,
|
conversationId,
|
||||||
storyId,
|
storyId,
|
||||||
isGroup
|
isGroup
|
||||||
|
@ -2746,10 +2799,10 @@ function getMessageMetricsForConversationSync(
|
||||||
return {
|
return {
|
||||||
oldest: oldest ? pick(oldest, ['received_at', 'sent_at', 'id']) : undefined,
|
oldest: oldest ? pick(oldest, ['received_at', 'sent_at', 'id']) : undefined,
|
||||||
newest: newest ? pick(newest, ['received_at', 'sent_at', 'id']) : undefined,
|
newest: newest ? pick(newest, ['received_at', 'sent_at', 'id']) : undefined,
|
||||||
oldestUnread: oldestUnread
|
oldestUnseen: oldestUnseen
|
||||||
? pick(oldestUnread, ['received_at', 'sent_at', 'id'])
|
? pick(oldestUnseen, ['received_at', 'sent_at', 'id'])
|
||||||
: undefined,
|
: undefined,
|
||||||
totalUnread,
|
totalUnseen,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
88
ts/sql/migrations/56-add-unseen-to-message.ts
Normal file
88
ts/sql/migrations/56-add-unseen-to-message.ts
Normal file
|
@ -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 updateToSchemaVersion53 from './53-gv2-banned-members';
|
||||||
import updateToSchemaVersion54 from './54-unprocessed-received-at-counter';
|
import updateToSchemaVersion54 from './54-unprocessed-received-at-counter';
|
||||||
import updateToSchemaVersion55 from './55-report-message-aci';
|
import updateToSchemaVersion55 from './55-report-message-aci';
|
||||||
|
import updateToSchemaVersion56 from './56-add-unseen-to-message';
|
||||||
|
|
||||||
function updateToSchemaVersion1(
|
function updateToSchemaVersion1(
|
||||||
currentVersion: number,
|
currentVersion: number,
|
||||||
|
@ -1925,6 +1926,7 @@ export const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion53,
|
updateToSchemaVersion53,
|
||||||
updateToSchemaVersion54,
|
updateToSchemaVersion54,
|
||||||
updateToSchemaVersion55,
|
updateToSchemaVersion55,
|
||||||
|
updateToSchemaVersion56,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function updateSchema(db: Database, logger: LoggerType): void {
|
export function updateSchema(db: Database, logger: LoggerType): void {
|
||||||
|
|
|
@ -234,8 +234,8 @@ type MessagePointerType = {
|
||||||
type MessageMetricsType = {
|
type MessageMetricsType = {
|
||||||
newest?: MessagePointerType;
|
newest?: MessagePointerType;
|
||||||
oldest?: MessagePointerType;
|
oldest?: MessagePointerType;
|
||||||
oldestUnread?: MessagePointerType;
|
oldestUnseen?: MessagePointerType;
|
||||||
totalUnread: number;
|
totalUnseen: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageLookupType = {
|
export type MessageLookupType = {
|
||||||
|
@ -2673,7 +2673,7 @@ export function reducer(
|
||||||
let metrics;
|
let metrics;
|
||||||
if (messageIds.length === 0) {
|
if (messageIds.length === 0) {
|
||||||
metrics = {
|
metrics = {
|
||||||
totalUnread: 0,
|
totalUnseen: 0,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
metrics = {
|
metrics = {
|
||||||
|
@ -2791,7 +2791,7 @@ export function reducer(
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { newest, oldest, oldestUnread, totalUnread } =
|
let { newest, oldest, oldestUnseen, totalUnseen } =
|
||||||
existingConversation.metrics;
|
existingConversation.metrics;
|
||||||
|
|
||||||
if (messages.length < 1) {
|
if (messages.length < 1) {
|
||||||
|
@ -2853,7 +2853,7 @@ export function reducer(
|
||||||
const newMessageIds = difference(newIds, existingConversation.messageIds);
|
const newMessageIds = difference(newIds, existingConversation.messageIds);
|
||||||
const { isNearBottom } = existingConversation;
|
const { isNearBottom } = existingConversation;
|
||||||
|
|
||||||
if ((!isNearBottom || !isActive) && !oldestUnread) {
|
if ((!isNearBottom || !isActive) && !oldestUnseen) {
|
||||||
const oldestId = newMessageIds.find(messageId => {
|
const oldestId = newMessageIds.find(messageId => {
|
||||||
const message = lookup[messageId];
|
const message = lookup[messageId];
|
||||||
|
|
||||||
|
@ -2861,7 +2861,7 @@ export function reducer(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (oldestId) {
|
if (oldestId) {
|
||||||
oldestUnread = pick(lookup[oldestId], [
|
oldestUnseen = pick(lookup[oldestId], [
|
||||||
'id',
|
'id',
|
||||||
'received_at',
|
'received_at',
|
||||||
'sent_at',
|
'sent_at',
|
||||||
|
@ -2869,14 +2869,14 @@ export function reducer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is a new incoming message, we'll increment our totalUnread count
|
// If this is a new incoming message, we'll increment our totalUnseen count
|
||||||
if (isNewMessage && !isJustSent && oldestUnread) {
|
if (isNewMessage && !isJustSent && oldestUnseen) {
|
||||||
const newUnread: number = newMessageIds.reduce((sum, messageId) => {
|
const newUnread: number = newMessageIds.reduce((sum, messageId) => {
|
||||||
const message = lookup[messageId];
|
const message = lookup[messageId];
|
||||||
|
|
||||||
return sum + (message && isMessageUnread(message) ? 1 : 0);
|
return sum + (message && isMessageUnread(message) ? 1 : 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
totalUnread = (totalUnread || 0) + newUnread;
|
totalUnseen = (totalUnseen || 0) + newUnread;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -2896,8 +2896,8 @@ export function reducer(
|
||||||
...existingConversation.metrics,
|
...existingConversation.metrics,
|
||||||
newest,
|
newest,
|
||||||
oldest,
|
oldest,
|
||||||
totalUnread,
|
totalUnseen,
|
||||||
oldestUnread,
|
oldestUnseen,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -2926,8 +2926,8 @@ export function reducer(
|
||||||
...existingConversation,
|
...existingConversation,
|
||||||
metrics: {
|
metrics: {
|
||||||
...existingConversation.metrics,
|
...existingConversation.metrics,
|
||||||
oldestUnread: undefined,
|
oldestUnseen: undefined,
|
||||||
totalUnread: 0,
|
totalUnseen: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -839,7 +839,7 @@ export function _conversationMessagesSelector(
|
||||||
const lastId =
|
const lastId =
|
||||||
messageIds.length === 0 ? undefined : messageIds[messageIds.length - 1];
|
messageIds.length === 0 ? undefined : messageIds[messageIds.length - 1];
|
||||||
|
|
||||||
const { oldestUnread } = metrics;
|
const { oldestUnseen } = metrics;
|
||||||
|
|
||||||
const haveNewest = !metrics.newest || !lastId || lastId === metrics.newest.id;
|
const haveNewest = !metrics.newest || !lastId || lastId === metrics.newest.id;
|
||||||
const haveOldest =
|
const haveOldest =
|
||||||
|
@ -847,13 +847,13 @@ export function _conversationMessagesSelector(
|
||||||
|
|
||||||
const items = messageIds;
|
const items = messageIds;
|
||||||
|
|
||||||
const oldestUnreadIndex = oldestUnread
|
const oldestUnseenIndex = oldestUnseen
|
||||||
? messageIds.findIndex(id => id === oldestUnread.id)
|
? messageIds.findIndex(id => id === oldestUnseen.id)
|
||||||
: undefined;
|
: undefined;
|
||||||
const scrollToIndex = scrollToMessageId
|
const scrollToIndex = scrollToMessageId
|
||||||
? messageIds.findIndex(id => id === scrollToMessageId)
|
? messageIds.findIndex(id => id === scrollToMessageId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const { totalUnread } = metrics;
|
const { totalUnseen } = metrics;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
haveNewest,
|
haveNewest,
|
||||||
|
@ -861,14 +861,14 @@ export function _conversationMessagesSelector(
|
||||||
isNearBottom,
|
isNearBottom,
|
||||||
items,
|
items,
|
||||||
messageLoadingState,
|
messageLoadingState,
|
||||||
oldestUnreadIndex:
|
oldestUnseenIndex:
|
||||||
isNumber(oldestUnreadIndex) && oldestUnreadIndex >= 0
|
isNumber(oldestUnseenIndex) && oldestUnseenIndex >= 0
|
||||||
? oldestUnreadIndex
|
? oldestUnseenIndex
|
||||||
: undefined,
|
: undefined,
|
||||||
scrollToIndex:
|
scrollToIndex:
|
||||||
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined,
|
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined,
|
||||||
scrollToIndexCounter: scrollToMessageCounter,
|
scrollToIndexCounter: scrollToMessageCounter,
|
||||||
totalUnread,
|
totalUnseen,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -901,7 +901,7 @@ export const getConversationMessagesSelector = createSelector(
|
||||||
haveOldest: false,
|
haveOldest: false,
|
||||||
messageLoadingState: TimelineMessageLoadingState.DoingInitialLoad,
|
messageLoadingState: TimelineMessageLoadingState.DoingInitialLoad,
|
||||||
scrollToIndexCounter: 0,
|
scrollToIndexCounter: 0,
|
||||||
totalUnread: 0,
|
totalUnseen: 0,
|
||||||
items: [],
|
items: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -352,7 +352,7 @@ describe('<Timeline> utilities', () => {
|
||||||
const props = {
|
const props = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
items: fakeItems(10),
|
items: fakeItems(10),
|
||||||
oldestUnreadIndex: 3,
|
oldestUnseenIndex: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
|
|
|
@ -692,9 +692,9 @@ describe('sql/timelineFetches', () => {
|
||||||
received_at: target - 8,
|
received_at: target - 8,
|
||||||
timestamp: target - 8,
|
timestamp: target - 8,
|
||||||
};
|
};
|
||||||
const oldestUnread: MessageAttributesType = {
|
const oldestUnseen: MessageAttributesType = {
|
||||||
id: getUuid(),
|
id: getUuid(),
|
||||||
body: 'oldestUnread',
|
body: 'oldestUnseen',
|
||||||
type: 'incoming',
|
type: 'incoming',
|
||||||
conversationId,
|
conversationId,
|
||||||
sent_at: target - 7,
|
sent_at: target - 7,
|
||||||
|
@ -748,7 +748,7 @@ describe('sql/timelineFetches', () => {
|
||||||
story,
|
story,
|
||||||
oldestInStory,
|
oldestInStory,
|
||||||
oldest,
|
oldest,
|
||||||
oldestUnread,
|
oldestUnseen,
|
||||||
oldestStoryUnread,
|
oldestStoryUnread,
|
||||||
anotherUnread,
|
anotherUnread,
|
||||||
newestInStory,
|
newestInStory,
|
||||||
|
@ -769,11 +769,11 @@ describe('sql/timelineFetches', () => {
|
||||||
);
|
);
|
||||||
assert.strictEqual(metricsInTimeline?.newest?.id, newest.id, 'newest');
|
assert.strictEqual(metricsInTimeline?.newest?.id, newest.id, 'newest');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
metricsInTimeline?.oldestUnread?.id,
|
metricsInTimeline?.oldestUnseen?.id,
|
||||||
oldestUnread.id,
|
oldestUnseen.id,
|
||||||
'oldestUnread'
|
'oldestUnseen'
|
||||||
);
|
);
|
||||||
assert.strictEqual(metricsInTimeline?.totalUnread, 3, 'totalUnread');
|
assert.strictEqual(metricsInTimeline?.totalUnseen, 3, 'totalUnseen');
|
||||||
|
|
||||||
const metricsInStory = await getMessageMetricsForConversation(
|
const metricsInStory = await getMessageMetricsForConversation(
|
||||||
conversationId,
|
conversationId,
|
||||||
|
@ -790,11 +790,11 @@ describe('sql/timelineFetches', () => {
|
||||||
'newestInStory'
|
'newestInStory'
|
||||||
);
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
metricsInStory?.oldestUnread?.id,
|
metricsInStory?.oldestUnseen?.id,
|
||||||
oldestStoryUnread.id,
|
oldestStoryUnread.id,
|
||||||
'oldestStoryUnread'
|
'oldestStoryUnread'
|
||||||
);
|
);
|
||||||
assert.strictEqual(metricsInStory?.totalUnread, 1, 'totalUnread');
|
assert.strictEqual(metricsInStory?.totalUnseen, 1, 'totalUnseen');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -332,7 +332,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
return {
|
return {
|
||||||
messageIds: [],
|
messageIds: [],
|
||||||
metrics: {
|
metrics: {
|
||||||
totalUnread: 0,
|
totalUnseen: 0,
|
||||||
},
|
},
|
||||||
scrollToMessageCounter: 0,
|
scrollToMessageCounter: 0,
|
||||||
};
|
};
|
||||||
|
@ -1008,7 +1008,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
...getDefaultConversationMessage(),
|
...getDefaultConversationMessage(),
|
||||||
messageIds: [messageIdThree, messageIdTwo, messageId],
|
messageIds: [messageIdThree, messageIdTwo, messageId],
|
||||||
metrics: {
|
metrics: {
|
||||||
totalUnread: 0,
|
totalUnseen: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1028,7 +1028,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
...getDefaultConversationMessage(),
|
...getDefaultConversationMessage(),
|
||||||
messageIds: [messageIdThree, messageIdTwo, messageId],
|
messageIds: [messageIdThree, messageIdTwo, messageId],
|
||||||
metrics: {
|
metrics: {
|
||||||
totalUnread: 0,
|
totalUnseen: 0,
|
||||||
newest: {
|
newest: {
|
||||||
id: messageId,
|
id: messageId,
|
||||||
received_at: time,
|
received_at: time,
|
||||||
|
@ -1058,7 +1058,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
...getDefaultConversationMessage(),
|
...getDefaultConversationMessage(),
|
||||||
messageIds: [],
|
messageIds: [],
|
||||||
metrics: {
|
metrics: {
|
||||||
totalUnread: 0,
|
totalUnseen: 0,
|
||||||
newest: {
|
newest: {
|
||||||
id: messageId,
|
id: messageId,
|
||||||
received_at: time,
|
received_at: time,
|
||||||
|
@ -1082,7 +1082,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
messageIds: [],
|
messageIds: [],
|
||||||
metrics: {
|
metrics: {
|
||||||
newest: undefined,
|
newest: undefined,
|
||||||
totalUnread: 0,
|
totalUnseen: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1118,7 +1118,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
...getDefaultConversationMessage(),
|
...getDefaultConversationMessage(),
|
||||||
messageIds: [messageId, messageIdTwo, messageIdThree],
|
messageIds: [messageId, messageIdTwo, messageIdThree],
|
||||||
metrics: {
|
metrics: {
|
||||||
totalUnread: 0,
|
totalUnseen: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1138,7 +1138,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
...getDefaultConversationMessage(),
|
...getDefaultConversationMessage(),
|
||||||
messageIds: [messageId, messageIdTwo, messageIdThree],
|
messageIds: [messageId, messageIdTwo, messageIdThree],
|
||||||
metrics: {
|
metrics: {
|
||||||
totalUnread: 0,
|
totalUnseen: 0,
|
||||||
oldest: {
|
oldest: {
|
||||||
id: messageId,
|
id: messageId,
|
||||||
received_at: time,
|
received_at: time,
|
||||||
|
@ -1168,7 +1168,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
...getDefaultConversationMessage(),
|
...getDefaultConversationMessage(),
|
||||||
messageIds: [],
|
messageIds: [],
|
||||||
metrics: {
|
metrics: {
|
||||||
totalUnread: 0,
|
totalUnseen: 0,
|
||||||
oldest: {
|
oldest: {
|
||||||
id: messageId,
|
id: messageId,
|
||||||
received_at: time,
|
received_at: time,
|
||||||
|
@ -1192,7 +1192,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
messageIds: [],
|
messageIds: [],
|
||||||
metrics: {
|
metrics: {
|
||||||
oldest: undefined,
|
oldest: undefined,
|
||||||
totalUnread: 0,
|
totalUnseen: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
insertJobSync,
|
insertJobSync,
|
||||||
_storyIdPredicate,
|
_storyIdPredicate,
|
||||||
} from '../sql/Server';
|
} from '../sql/Server';
|
||||||
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
|
import { SeenStatus } from '../MessageSeenStatus';
|
||||||
|
|
||||||
const OUR_UUID = generateGuid();
|
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}`;
|
|
||||||
}
|
|
31
ts/util/idForLogging.ts
Normal file
31
ts/util/idForLogging.ts
Normal file
|
@ -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 { notificationService } from '../services/notifications';
|
||||||
import { isGroup } from './whatTypeOfConversation';
|
import { isGroup } from './whatTypeOfConversation';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
import { getConversationIdForLogging } from './idForLogging';
|
||||||
|
|
||||||
export async function markConversationRead(
|
export async function markConversationRead(
|
||||||
conversationAttrs: ConversationAttributesType,
|
conversationAttrs: ConversationAttributesType,
|
||||||
newestUnreadAt: number,
|
newestUnreadAt: number,
|
||||||
options: { readAt?: number; sendReadReceipts: boolean } = {
|
options: {
|
||||||
|
readAt?: number;
|
||||||
|
sendReadReceipts: boolean;
|
||||||
|
newestSentAt?: number;
|
||||||
|
} = {
|
||||||
sendReadReceipts: true,
|
sendReadReceipts: true,
|
||||||
}
|
}
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
@ -32,7 +37,8 @@ export async function markConversationRead(
|
||||||
]);
|
]);
|
||||||
|
|
||||||
log.info('markConversationRead', {
|
log.info('markConversationRead', {
|
||||||
conversationId,
|
conversationId: getConversationIdForLogging(conversationAttrs),
|
||||||
|
newestSentAt: options.newestSentAt,
|
||||||
newestUnreadAt,
|
newestUnreadAt,
|
||||||
unreadMessages: unreadMessages.length,
|
unreadMessages: unreadMessages.length,
|
||||||
unreadReactions: unreadReactions.length,
|
unreadReactions: unreadReactions.length,
|
||||||
|
|
|
@ -13,7 +13,7 @@ import type {
|
||||||
import * as AttachmentDownloads from '../messageModifiers/AttachmentDownloads';
|
import * as AttachmentDownloads from '../messageModifiers/AttachmentDownloads';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { isLongMessage } from '../types/MIME';
|
import { isLongMessage } from '../types/MIME';
|
||||||
import { getMessageIdForLogging } from './getMessageIdForLogging';
|
import { getMessageIdForLogging } from './idForLogging';
|
||||||
import {
|
import {
|
||||||
copyStickerToAttachments,
|
copyStickerToAttachments,
|
||||||
savePackMetadata,
|
savePackMetadata,
|
||||||
|
|
|
@ -134,7 +134,7 @@ type ScrollAnchorBeforeUpdateProps = Readonly<
|
||||||
| 'isSomeoneTyping'
|
| 'isSomeoneTyping'
|
||||||
| 'items'
|
| 'items'
|
||||||
| 'messageLoadingState'
|
| 'messageLoadingState'
|
||||||
| 'oldestUnreadIndex'
|
| 'oldestUnseenIndex'
|
||||||
| 'scrollToIndex'
|
| 'scrollToIndex'
|
||||||
| 'scrollToIndexCounter'
|
| 'scrollToIndexCounter'
|
||||||
>
|
>
|
||||||
|
@ -169,7 +169,7 @@ export function getScrollAnchorBeforeUpdate(
|
||||||
if (props.isIncomingMessageRequest) {
|
if (props.isIncomingMessageRequest) {
|
||||||
return ScrollAnchor.ChangeNothing;
|
return ScrollAnchor.ChangeNothing;
|
||||||
}
|
}
|
||||||
if (isNumber(props.oldestUnreadIndex)) {
|
if (isNumber(props.oldestUnseenIndex)) {
|
||||||
return ScrollAnchor.ScrollToUnreadIndicator;
|
return ScrollAnchor.ScrollToUnreadIndicator;
|
||||||
}
|
}
|
||||||
return ScrollAnchor.ScrollToBottom;
|
return ScrollAnchor.ScrollToBottom;
|
||||||
|
|
|
@ -493,7 +493,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
throw new Error(`markMessageRead: failed to load message ${messageId}`);
|
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 =
|
const createMessageRequestResponseHandler =
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue