Merge pull request #2335 from Bilb/mark-all-as-read-optimization

optimize markAllAsRead when no expiration timer & fix read receipts
pull/2524/head
Audric Ackermann 3 years ago committed by GitHub
commit c33e4e00d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -280,5 +280,3 @@
line-height: 18px;
}
}

@ -54,6 +54,8 @@ import { ConversationMessageRequestButtons } from './ConversationRequestButtons'
import { ConversationRequestinfo } from './ConversationRequestInfo';
import { getCurrentRecoveryPhrase } from '../../util/storage';
import loadImage from 'blueimp-load-image';
import { markAllReadByConvoId } from '../../interactions/conversationInteractions';
import { SessionSpinner } from '../basic/SessionSpinner';
import styled from 'styled-components';
// tslint:disable: jsx-curly-spacing
@ -307,17 +309,18 @@ export class SessionConversation extends React.Component<Props, State> {
}
private async scrollToNow() {
if (!this.props.selectedConversationKey) {
const conversationKey = this.props.selectedConversationKey;
if (!conversationKey) {
return;
}
const mostNowMessage = await Data.getLastMessageInConversation(
this.props.selectedConversationKey
);
if (mostNowMessage) {
await markAllReadByConvoId(conversationKey);
const mostRecentMessage = await Data.getLastMessageInConversation(conversationKey);
if (mostRecentMessage) {
await openConversationToSpecificMessage({
conversationKey: this.props.selectedConversationKey,
messageIdToNavigateTo: mostNowMessage.id,
conversationKey,
messageIdToNavigateTo: mostRecentMessage.id,
shouldHighlightMessage: false,
});
const messageContainer = this.messageContainerRef.current;

@ -1,5 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { MessageRenderingProps } from '../../../../models/messageType';
import { PubKey } from '../../../../session/types';
import {
@ -19,6 +20,8 @@ type Props = {
messageId: string;
};
const StyledAuthorContainer = styled(Flex)`color: var(--color-text)`;
export const MessageAuthorText = (props: Props) => {
const selected = useSelector(state => getMessageAuthorProps(state as any, props.messageId));
@ -38,7 +41,7 @@ export const MessageAuthorText = (props: Props) => {
const displayedPubkey = authorProfileName ? PubKey.shorten(sender) : sender;
return (
<Flex container={true}>
<StyledAuthorContainer container={true}>
<ContactName
pubkey={displayedPubkey}
name={authorName}
@ -47,6 +50,6 @@ export const MessageAuthorText = (props: Props) => {
boldProfileName={true}
shouldShowPubkey={Boolean(isPublic)}
/>
</Flex>
</StyledAuthorContainer>
);
};

@ -144,17 +144,17 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
const found = await Data.getMessageById(messageId);
if (found && Boolean(found.get('unread'))) {
const foundReceivedAt = found.get('received_at');
const foundSentAt = found.get('sent_at');
// mark the message as read.
// this will trigger the expire timer.
await found.markRead(Date.now());
// we should stack those and send them in a single message once every 5secs or something.
// this would be part of an redesign of the sending pipeline
if (foundReceivedAt) {
if (foundSentAt && selectedConversationKey) {
void getConversationController()
.get(found.id)
?.sendReadReceiptsIfNeeded([foundReceivedAt]);
.get(selectedConversationKey)
?.sendReadReceiptsIfNeeded([foundSentAt]);
}
}
}

@ -148,6 +148,7 @@ export const Data = {
getMessageBySenderAndTimestamp,
getUnreadByConversation,
getUnreadCountByConversation,
markAllAsReadByConversationNoExpiration,
getMessageCountByType,
getMessagesByConversation,
getLastMessagesByConversation,
@ -472,6 +473,7 @@ async function getMessageBySenderAndTimestamp({
source,
timestamp,
});
if (!messages || !messages.length) {
return null;
}
@ -484,6 +486,13 @@ async function getUnreadByConversation(conversationId: string): Promise<MessageC
return new MessageCollection(messages);
}
async function markAllAsReadByConversationNoExpiration(
conversationId: string
): Promise<Array<number>> {
const messagesIds = await channels.markAllAsReadByConversationNoExpiration(conversationId);
return messagesIds;
}
// might throw
async function getUnreadCountByConversation(conversationId: string): Promise<number> {
return channels.getUnreadCountByConversation(conversationId);

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

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

@ -25,6 +25,7 @@ import { SignalService } from '../protobuf';
import { MessageModel, sliceQuoteText } from './message';
import { MessageAttributesOptionals, MessageDirection } from './messageType';
import autoBind from 'auto-bind';
import { Data } from '../../ts/data/data';
import { toHex } from '../session/utils/String';
import {
@ -1223,15 +1224,49 @@ 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 now.
* 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 Data.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 Data.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
public async markReadBouncy(newestUnreadDate: number, providedOptions: any = {}) {
public async markReadBouncy(
newestUnreadDate: number,
providedOptions: { sendReadReceipts?: boolean; readAt?: number } = {}
) {
const lastReadTimestamp = this.lastReadTimestamp;
if (newestUnreadDate < lastReadTimestamp) {
return;
}
const options = providedOptions || {};
defaults(options, { sendReadReceipts: true });
const readAt = providedOptions?.readAt || Date.now();
const sendReadReceipts = providedOptions?.sendReadReceipts || true;
const conversationId = this.id;
Notifications.clearByConversationID(conversationId);
@ -1245,7 +1280,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
for (const nowRead of oldUnreadNowRead) {
nowRead.markReadNoCommit(options.readAt);
nowRead.markReadNoCommit(readAt);
const errors = nowRead.get('errors');
read.push({
@ -1307,7 +1342,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
// conversation is viewed, another error message shows up for the contact
read = read.filter(item => !item.hasErrors);
if (read.length && options.sendReadReceipts) {
if (read.length && sendReadReceipts) {
const timestamps = map(read, 'timestamp').filter(t => !!t) as Array<number>;
await this.sendReadReceiptsIfNeeded(timestamps);
}

@ -6,6 +6,7 @@ import { app, clipboard, dialog, Notification } from 'electron';
import {
chunk,
compact,
difference,
forEach,
fromPairs,
@ -1112,6 +1113,38 @@ function getUnreadByConversation(conversationId: string) {
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) {
const row = assertGlobalInstance()
.prepare(
@ -1346,7 +1379,7 @@ function getFirstUnreadMessageWithMention(
function getMessagesBySentAt(sentAt: number) {
const rows = assertGlobalInstance()
.prepare(
`SELECT * FROM ${MESSAGES_TABLE}
`SELECT json FROM ${MESSAGES_TABLE}
WHERE sent_at = $sent_at
ORDER BY received_at DESC;`
)
@ -2403,6 +2436,7 @@ export const sqlNode = {
saveMessages,
removeMessage,
getUnreadByConversation,
markAllAsReadByConversationNoExpiration,
getUnreadCountByConversation,
getMessageCountByType,

Loading…
Cancel
Save