fix: speed up reaction UI update for opengroups

This commit is contained in:
William Grant 2022-08-30 11:37:20 +10:00
parent f309bf40f8
commit b33ea096b4
6 changed files with 138 additions and 60 deletions

View File

@ -737,7 +737,12 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const chatMessagePrivate = new VisibleMessage(chatMessageParams);
await getMessageQueue().sendToPubKey(destinationPubkey, chatMessagePrivate);
await handleMessageReaction(reaction, UserUtils.getOurPubKeyStrFromCache(), true);
await handleMessageReaction({
reaction,
sender: UserUtils.getOurPubKeyStrFromCache(),
you: true,
isOpenGroup: false,
});
return;
}
@ -749,7 +754,12 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
});
// we need the return await so that errors are caught in the catch {}
await getMessageQueue().sendToGroup(closedGroupVisibleMessage);
await handleMessageReaction(reaction, UserUtils.getOurPubKeyStrFromCache(), true);
await handleMessageReaction({
reaction,
sender: UserUtils.getOurPubKeyStrFromCache(),
you: true,
isOpenGroup: false,
});
return;
}

View File

@ -322,11 +322,12 @@ async function handleSwarmMessage(
// this call has to be made inside the queueJob!
// We handle reaction DataMessages separately
if (!msgModel.get('isPublic') && rawDataMessage.reaction) {
await handleMessageReaction(
rawDataMessage.reaction,
msgModel.get('source'),
isUsFromCache(msgModel.get('source'))
);
await handleMessageReaction({
reaction: rawDataMessage.reaction,
sender: msgModel.get('source'),
you: isUsFromCache(msgModel.get('source')),
isOpenGroup: false,
});
if (
convoToAddMessageTo.isPrivate() &&
msgModel.get('unread') &&

View File

@ -1,5 +1,6 @@
import AbortController from 'abort-controller';
import { OpenGroupReactionResponse } from '../../../../types/Reaction';
import { handleClearReaction } from '../../../../util/reactions';
import { OpenGroupRequestCommonType } from '../opengroupV2/ApiUtil';
import {
batchFirstSubIsSuccess,
@ -25,8 +26,13 @@ export const clearSogsReactionByServerId = async (
serverId: number,
roomInfos: OpenGroupRequestCommonType
): Promise<boolean> => {
const canReact = await hasReactionSupport(serverId);
if (!canReact) {
const { supported, conversation } = await hasReactionSupport(serverId);
if (!supported) {
return false;
}
if (!conversation) {
window.log.warn(`Conversation for ${reaction} not found in db`);
return false;
}
@ -44,10 +50,17 @@ export const clearSogsReactionByServerId = async (
addToMutationCache(cacheEntry);
// Since responses can take a long time we immediately update the moderators's UI and if there is a problem it is overwritten by handleOpenGroupMessageReactions later.
await handleClearReaction(serverId, reaction);
const options: Array<OpenGroupBatchRow> = [
{
type: 'deleteReaction',
deleteReaction: { reaction, messageId: serverId, roomId: roomInfos.roomId },
deleteReaction: {
reaction,
messageId: serverId,
roomId: roomInfos.roomId,
},
},
];
const result = await sogsBatchSend(

View File

@ -1,10 +1,13 @@
import { AbortSignal } from 'abort-controller';
import { Data } from '../../../../data/data';
import { ConversationModel } from '../../../../models/conversation';
import { Action, OpenGroupReactionResponse, Reaction } from '../../../../types/Reaction';
import { getEmojiDataFromNative } from '../../../../util/emoji';
import { hitRateLimit } from '../../../../util/reactions';
import { handleMessageReaction, hitRateLimit } from '../../../../util/reactions';
import { OnionSending } from '../../../onions/onionSend';
import { UserUtils } from '../../../utils';
import { OpenGroupPollingUtils } from '../opengroupV2/OpenGroupPollingUtils';
import { getUsBlindedInThatServer } from './knownBlindedkeys';
import { batchGlobalIsSuccess, parseBatchGlobalStatusCode } from './sogsV3BatchPoll';
import {
addToMutationCache,
@ -13,25 +16,27 @@ import {
updateMutationCache,
} from './sogsV3MutationCache';
export const hasReactionSupport = async (serverId: number): Promise<boolean> => {
export const hasReactionSupport = async (
serverId: number
): Promise<{ supported: boolean; conversation: ConversationModel | null }> => {
const found = await Data.getMessageByServerId(serverId);
if (!found) {
window.log.warn(`Open Group Message ${serverId} not found in db`);
return false;
return { supported: false, conversation: null };
}
const conversationModel = found?.getConversation();
if (!conversationModel) {
window.log.warn(`Conversation for ${serverId} not found in db`);
return false;
return { supported: false, conversation: null };
}
if (!conversationModel.hasReactions()) {
window.log.warn("This open group doesn't have reaction support. Server Message ID", serverId);
return false;
return { supported: false, conversation: null };
}
return true;
return { supported: true, conversation: conversationModel };
};
export const sendSogsReactionOnionV4 = async (
@ -47,8 +52,8 @@ export const sendSogsReactionOnionV4 = async (
throw new Error(`Could not find sogs pubkey of url:${serverUrl}`);
}
const canReact = await hasReactionSupport(reaction.id);
if (!canReact) {
const { supported, conversation } = await hasReactionSupport(reaction.id);
if (!supported) {
return false;
}
@ -56,6 +61,11 @@ export const sendSogsReactionOnionV4 = async (
return false;
}
if (!conversation) {
window.log.warn(`Conversation for ${reaction.id} not found in db`);
return false;
}
// The SOGS endpoint supports any text input so we need to make sure we are sending a valid unicode emoji
// for an invalid input we use https://emojipedia.org/frame-with-an-x/ as a replacement since it cannot rendered as an emoji but is valid unicode
const emoji = getEmojiDataFromNative(reaction.emoji) ? reaction.emoji : '🖾';
@ -77,6 +87,15 @@ export const sendSogsReactionOnionV4 = async (
addToMutationCache(cacheEntry);
// Since responses can take a long time we immediately update the sender's UI and if there is a problem it is overwritten by handleOpenGroupMessageReactions later.
const me = UserUtils.getOurPubKeyStrFromCache();
await handleMessageReaction({
reaction,
sender: blinded ? getUsBlindedInThatServer(conversation) || me : me,
you: true,
isOpenGroup: true,
});
// reaction endpoint requires an empty dict {}
const stringifiedBody = null;
const result = await OnionSending.sendJsonViaOnionV4ToSogs({

View File

@ -52,11 +52,12 @@ describe('ReactionMessage', () => {
expect(reaction?.action, 'action should be 0').to.be.equal(0);
// Handling reaction
const updatedMessage = await handleMessageReaction(
reaction as SignalService.DataMessage.IReaction,
ourNumber,
true
);
const updatedMessage = await handleMessageReaction({
reaction: reaction as SignalService.DataMessage.IReaction,
sender: ourNumber,
you: true,
isOpenGroup: false,
});
expect(updatedMessage?.get('reacts'), 'original message should have reacts').to.not.be
.undefined;
@ -84,11 +85,12 @@ describe('ReactionMessage', () => {
expect(reaction?.action, 'action should be 1').to.be.equal(1);
// Handling reaction
const updatedMessage = await handleMessageReaction(
reaction as SignalService.DataMessage.IReaction,
ourNumber,
true
);
const updatedMessage = await handleMessageReaction({
reaction: reaction as SignalService.DataMessage.IReaction,
sender: ourNumber,
you: true,
isOpenGroup: false,
});
expect(updatedMessage?.get('reacts'), 'original message reacts should be undefined').to.be
.undefined;

View File

@ -37,23 +37,28 @@ export function hitRateLimit(): boolean {
* Retrieves the original message of a reaction
*/
const getMessageByReaction = async (
reaction: SignalService.DataMessage.IReaction
reaction: SignalService.DataMessage.IReaction,
isOpenGroup: boolean
): Promise<MessageModel | null> => {
let originalMessage = null;
const originalMessageId = Number(reaction.id);
const originalMessageAuthor = reaction.author;
const collection = await Data.getMessagesBySentAt(originalMessageId);
originalMessage = collection.find((item: MessageModel) => {
const messageTimestamp = item.get('sent_at');
const author = item.get('source');
return Boolean(
messageTimestamp &&
messageTimestamp === originalMessageId &&
author &&
author === originalMessageAuthor
);
});
if (isOpenGroup) {
originalMessage = await Data.getMessageByServerId(originalMessageId);
} else {
const collection = await Data.getMessagesBySentAt(originalMessageId);
originalMessage = collection.find((item: MessageModel) => {
const messageTimestamp = item.get('sent_at');
const author = item.get('source');
return Boolean(
messageTimestamp &&
messageTimestamp === originalMessageId &&
author &&
author === originalMessageAuthor
);
});
}
if (!originalMessage) {
window?.log?.warn(`Cannot find the original reacted message ${originalMessageId}.`);
@ -140,19 +145,25 @@ export const sendMessageReaction = async (messageId: string, emoji: string) => {
/**
* Handle reactions on the client by updating the state of the source message
* Do not use for Open Groups
* Used in OpenGroups for sending reactions only, not handling responses
*/
export const handleMessageReaction = async (
reaction: SignalService.DataMessage.IReaction,
sender: string,
you: boolean
) => {
export const handleMessageReaction = async ({
reaction,
sender,
you,
isOpenGroup,
}: {
reaction: SignalService.DataMessage.IReaction;
sender: string;
you: boolean;
isOpenGroup: boolean;
}) => {
if (!reaction.emoji) {
window?.log?.warn(`There is no emoji for the reaction ${reaction}.`);
return;
}
const originalMessage = await getMessageByReaction(reaction);
const originalMessage = await getMessageByReaction(reaction, isOpenGroup);
if (!originalMessage) {
return;
}
@ -163,20 +174,15 @@ export const handleMessageReaction = async (
const senders = details.senders;
let count = details.count || 0;
if (originalMessage.get('isPublic')) {
window.log.warn("handleMessageReaction() shouldn't be used in opengroups");
return;
} else {
if (details.you && senders.includes(sender)) {
if (reaction.action === Action.REACT) {
window.log.warn('Received duplicate message for your reaction. Ignoring it');
return;
} else {
details.you = false;
}
if (details.you && senders.includes(sender)) {
if (reaction.action === Action.REACT) {
window.log.warn('Received duplicate message for your reaction. Ignoring it');
return;
} else {
details.you = you;
details.you = false;
}
} else {
details.you = you;
}
switch (reaction.action) {
@ -230,7 +236,34 @@ export const handleMessageReaction = async (
};
/**
* Handles all message reaction updates for opengroups
* Handles updating the UI when clearing all reactions for a certain emoji
* Only usable by moderators in opengroups and runs on their client
*/
export const handleClearReaction = async (serverId: number, emoji: string) => {
const originalMessage = await Data.getMessageByServerId(serverId);
if (!originalMessage) {
window?.log?.warn(`Cannot find the original reacted message ${serverId}.`);
return;
}
const reacts: ReactionList | undefined = originalMessage.get('reacts');
if (reacts) {
// tslint:disable-next-line: no-dynamic-delete
delete reacts[emoji];
}
originalMessage.set({
reacts: !isEmpty(reacts) ? reacts : undefined,
});
await originalMessage.commit();
window.log.info(`You cleared all ${emoji} reactions on message ${serverId}`);
return originalMessage;
};
/**
* Handles all message reaction updates/responses for opengroups
*/
export const handleOpenGroupMessageReactions = async (
reactions: OpenGroupReactionList,