fix: optmize markAllAsRead when no expiration timer

we basically do a single sql call to mark everything as read for that
conversation, force unreadCount to 0 and mention state to false, and
trigger read syncs if needed.

the optomization cannot work for conversation with expiration timer for
now
pull/2335/head
Audric Ackermann 3 years ago
parent ca0c74317f
commit 9251711fa5
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -54,6 +54,7 @@ import { ConversationMessageRequestButtons } from './ConversationRequestButtons'
import { ConversationRequestinfo } from './ConversationRequestInfo'; import { ConversationRequestinfo } from './ConversationRequestInfo';
import { getCurrentRecoveryPhrase } from '../../util/storage'; import { getCurrentRecoveryPhrase } from '../../util/storage';
import loadImage from 'blueimp-load-image'; import loadImage from 'blueimp-load-image';
import { markAllReadByConvoId } from '../../interactions/conversationInteractions';
// tslint:disable: jsx-curly-spacing // tslint:disable: jsx-curly-spacing
interface State { interface State {
@ -276,14 +277,17 @@ export class SessionConversation extends React.Component<Props, State> {
} }
private async scrollToNow() { private async scrollToNow() {
if (!this.props.selectedConversationKey) { const conversationKey = this.props.selectedConversationKey;
if (!conversationKey) {
return; return;
} }
const mostNowMessage = await getLastMessageInConversation(this.props.selectedConversationKey);
await markAllReadByConvoId(conversationKey);
const mostNowMessage = await getLastMessageInConversation(conversationKey);
if (mostNowMessage) { if (mostNowMessage) {
await openConversationToSpecificMessage({ await openConversationToSpecificMessage({
conversationKey: this.props.selectedConversationKey, conversationKey,
messageIdToNavigateTo: mostNowMessage.id, messageIdToNavigateTo: mostNowMessage.id,
shouldHighlightMessage: false, shouldHighlightMessage: false,
}); });

@ -403,6 +403,7 @@ export async function getMessageBySenderAndTimestamp({
source, source,
timestamp, timestamp,
}); });
if (!messages || !messages.length) { if (!messages || !messages.length) {
return null; return null;
} }
@ -415,6 +416,13 @@ export async function getUnreadByConversation(conversationId: string): Promise<M
return new MessageCollection(messages); return new MessageCollection(messages);
} }
export async function markAllAsReadByConversationNoExpiration(
conversationId: string
): Promise<Array<number>> {
const messagesIds = await channels.markAllAsReadByConversationNoExpiration(conversationId);
return messagesIds;
}
// might throw // might throw
export async function getUnreadCountByConversation(conversationId: string): Promise<number> { export async function getUnreadCountByConversation(conversationId: string): Promise<number> {
return channels.getUnreadCountByConversation(conversationId); return channels.getUnreadCountByConversation(conversationId);

@ -43,6 +43,7 @@ const channelsToMake = new Set([
'removeMessage', 'removeMessage',
'_removeMessages', '_removeMessages',
'getUnreadByConversation', 'getUnreadByConversation',
'markAllAsReadByConversationNoExpiration',
'getUnreadCountByConversation', 'getUnreadCountByConversation',
'getMessageCountByType', 'getMessageCountByType',
'removeAllMessagesInConversation', 'removeAllMessagesInConversation',

@ -292,7 +292,8 @@ export async function markAllReadByConvoId(conversationId: string) {
const conversation = getConversationController().get(conversationId); const conversation = getConversationController().get(conversationId);
perfStart(`markAllReadByConvoId-${conversationId}`); perfStart(`markAllReadByConvoId-${conversationId}`);
await conversation.markReadBouncy(Date.now()); await conversation?.markAllAsRead();
perfEnd(`markAllReadByConvoId-${conversationId}`, 'markAllReadByConvoId'); perfEnd(`markAllReadByConvoId-${conversationId}`, 'markAllReadByConvoId');
} }

@ -1,5 +1,5 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import _ from 'lodash'; import _, { uniq } from 'lodash';
import { getMessageQueue } from '../session'; import { getMessageQueue } from '../session';
import { getConversationController } from '../session/conversations'; import { getConversationController } from '../session/conversations';
import { ClosedGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage'; import { ClosedGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage';
@ -17,6 +17,7 @@ import {
getMessagesByConversation, getMessagesByConversation,
getUnreadByConversation, getUnreadByConversation,
getUnreadCountByConversation, getUnreadCountByConversation,
markAllAsReadByConversationNoExpiration,
removeMessage as dataRemoveMessage, removeMessage as dataRemoveMessage,
saveMessages, saveMessages,
updateConversation, updateConversation,
@ -1062,15 +1063,50 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
} }
/**
* Mark everything as read efficiently if possible.
*
* For convos with a expiration timer enable, start the timer as of no.
* Send read receipt if needed.
*/
public async markAllAsRead() {
if (this.isOpenGroupV2()) {
// for opengroups, we batch everything as there is no expiration timer to take care (and potentially a lot of messages)
await markAllAsReadByConversationNoExpiration(this.id);
this.set({ mentionedUs: false, unreadCount: 0 });
await this.commit();
return;
}
// if the conversation has no expiration timer, we can also batch everything, but we also need to send read receipts potentially
// so we grab them from the db
if (!this.get('expireTimer')) {
const allReadMessages = await markAllAsReadByConversationNoExpiration(this.id);
this.set({ mentionedUs: false, unreadCount: 0 });
await this.commit();
if (allReadMessages.length) {
await this.sendReadReceiptsIfNeeded(uniq(allReadMessages));
}
return;
}
await this.markReadBouncy(Date.now());
}
// tslint:disable-next-line: cyclomatic-complexity // tslint:disable-next-line: cyclomatic-complexity
public async markReadBouncy(newestUnreadDate: number, providedOptions: any = {}) { public async markReadBouncy(
newestUnreadDate: number,
providedOptions: { sendReadReceipts?: boolean; readAt?: number } = {}
) {
const lastReadTimestamp = this.lastReadTimestamp; const lastReadTimestamp = this.lastReadTimestamp;
if (newestUnreadDate < lastReadTimestamp) { if (newestUnreadDate < lastReadTimestamp) {
return; return;
} }
const options = providedOptions || {}; const defaultedReadAt = providedOptions?.readAt || Date.now();
_.defaults(options, { sendReadReceipts: true }); const defaultedSendReadReceipts = providedOptions?.sendReadReceipts || true;
const conversationId = this.id; const conversationId = this.id;
Notifications.clearByConversationID(conversationId); Notifications.clearByConversationID(conversationId);
@ -1084,7 +1120,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
// Build the list of updated message models so we can mark them all as read on a single sqlite call // Build the list of updated message models so we can mark them all as read on a single sqlite call
for (const nowRead of oldUnreadNowRead) { for (const nowRead of oldUnreadNowRead) {
nowRead.markReadNoCommit(options.readAt); nowRead.markReadNoCommit(defaultedReadAt);
const errors = nowRead.get('errors'); const errors = nowRead.get('errors');
read.push({ read.push({
@ -1146,7 +1182,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
// conversation is viewed, another error message shows up for the contact // conversation is viewed, another error message shows up for the contact
read = read.filter(item => !item.hasErrors); read = read.filter(item => !item.hasErrors);
if (read.length && options.sendReadReceipts) { if (read.length && defaultedSendReadReceipts) {
const timestamps = _.map(read, 'timestamp').filter(t => !!t) as Array<number>; const timestamps = _.map(read, 'timestamp').filter(t => !!t) as Array<number>;
await this.sendReadReceiptsIfNeeded(timestamps); await this.sendReadReceiptsIfNeeded(timestamps);
} }

@ -6,6 +6,7 @@ import { app, clipboard, dialog, Notification } from 'electron';
import { import {
chunk, chunk,
compact,
difference, difference,
flattenDeep, flattenDeep,
forEach, forEach,
@ -2378,6 +2379,38 @@ function getUnreadByConversation(conversationId: string) {
return map(rows, row => jsonToObject(row.json)); return map(rows, row => jsonToObject(row.json));
} }
/**
* Warning: This does not start expiration timer
*/
function markAllAsReadByConversationNoExpiration(
conversationId: string
): Array<{ id: string; timestamp: number }> {
const messagesUnreadBefore = assertGlobalInstance()
.prepare(
`SELECT json FROM ${MESSAGES_TABLE} WHERE
unread = $unread AND
conversationId = $conversationId;`
)
.all({
unread: 1,
conversationId,
});
assertGlobalInstance()
.prepare(
`UPDATE ${MESSAGES_TABLE} SET
unread = 0, json = json_set(json, '$.unread', 0)
WHERE unread = $unread AND
conversationId = $conversationId;`
)
.run({
unread: 1,
conversationId,
});
return compact(messagesUnreadBefore.map(row => jsonToObject(row.json).sent_at));
}
function getUnreadCountByConversation(conversationId: string) { function getUnreadCountByConversation(conversationId: string) {
const row = assertGlobalInstance() const row = assertGlobalInstance()
.prepare( .prepare(
@ -2610,7 +2643,7 @@ function getFirstUnreadMessageWithMention(conversationId: string, ourpubkey: str
function getMessagesBySentAt(sentAt: number) { function getMessagesBySentAt(sentAt: number) {
const rows = assertGlobalInstance() const rows = assertGlobalInstance()
.prepare( .prepare(
`SELECT * FROM ${MESSAGES_TABLE} `SELECT json FROM ${MESSAGES_TABLE}
WHERE sent_at = $sent_at WHERE sent_at = $sent_at
ORDER BY received_at DESC;` ORDER BY received_at DESC;`
) )
@ -3712,6 +3745,7 @@ export const sqlNode = {
saveMessages, saveMessages,
removeMessage, removeMessage,
getUnreadByConversation, getUnreadByConversation,
markAllAsReadByConversationNoExpiration,
getUnreadCountByConversation, getUnreadCountByConversation,
getMessageCountByType, getMessageCountByType,

@ -42,7 +42,7 @@ export async function appendFetchAvatarAndProfileJob(
// ); // );
return; return;
} }
window.log.info(`[profile-update] queuing fetching avatar for ${conversation.id}`); // window.log.info(`[profile-update] queuing fetching avatar for ${conversation.id}`);
const task = allowOnlyOneAtATime(oneAtaTimeStr, async () => { const task = allowOnlyOneAtATime(oneAtaTimeStr, async () => {
return createOrUpdateProfile(conversation, profile, profileKey); return createOrUpdateProfile(conversation, profile, profileKey);
}); });

Loading…
Cancel
Save