Merge pull request #2445 from yougotwill/reactions_fixes

Reactions Updates
pull/2448/head
Audric Ackermann 3 years ago committed by GitHub
commit e5f484f5bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -454,9 +454,13 @@
"hideBanner": "Hide",
"openMessageRequestInboxDescription": "View your Message Request inbox",
"clearAllReactions": "Are you sure you want to clear all $emoji$ ?",
"reactionTooltip": "reacted with",
"expandedReactionsText": "Show Less",
"reactionNotification": "Reacts to a message with $emoji$",
"readableListCounterSingular": "other",
"readableListCounterPlural": "others"
"otherSingular": "$number$ other",
"otherPlural": "$number$ others",
"reactionPopup": "reacted with",
"reactionPopupOne": "$name$",
"reactionPopupTwo": "$name$ & $name2$",
"reactionPopupThree": "$name$, $name2$ & $name3$",
"reactionPopupMany": "$name$, $name2$, $name3$ &"
}

@ -69,8 +69,8 @@ export const Reaction = (props: ReactionProps): ReactElement => {
handlePopupClick,
} = props;
const reactionsMap = (reactions && Object.fromEntries(reactions)) || {};
const senders = reactionsMap[emoji].senders ? Object.keys(reactionsMap[emoji].senders) : [];
const count = reactionsMap[emoji].count;
const senders = reactionsMap[emoji]?.senders || [];
const count = reactionsMap[emoji]?.count;
const showCount = count !== undefined && (count > 1 || inGroup);
const reactionRef = useRef<HTMLDivElement>(null);
@ -138,7 +138,8 @@ export const Reaction = (props: ReactionProps): ReactElement => {
<ReactionPopup
messageId={messageId}
emoji={popupReaction}
senders={Object.keys(reactionsMap[popupReaction].senders)}
count={reactionsMap[popupReaction]?.count}
senders={reactionsMap[popupReaction]?.senders}
tooltipPosition={tooltipPosition}
onClick={() => {
if (handlePopupReaction) {

@ -3,7 +3,6 @@ import styled from 'styled-components';
import { Data } from '../../../../data/data';
import { PubKey } from '../../../../session/types/PubKey';
import { nativeEmojiData } from '../../../../util/emoji';
import { readableList } from '../../../../util/readableList';
export type TipPosition = 'center' | 'left' | 'right';
@ -59,8 +58,11 @@ const StyledOthers = styled.span`
color: var(--color-accent);
`;
const generateContacts = async (messageId: string, senders: Array<string>) => {
let results = null;
const generateContactsString = async (
messageId: string,
senders: Array<string>
): Promise<Array<string> | null> => {
let results = [];
const message = await Data.getMessageById(messageId);
if (message) {
let meIndex = -1;
@ -75,53 +77,71 @@ const generateContacts = async (messageId: string, senders: Array<string>) => {
results.splice(meIndex, 1);
results = [window.i18n('you'), ...results];
}
if (results && results.length > 0) {
return results;
}
}
return results;
return null;
};
const Contacts = (contacts: string) => {
if (!contacts) {
const Contacts = (contacts: Array<string>, count: number) => {
if (!Boolean(contacts?.length > 0)) {
return;
}
if (contacts.includes('&') && contacts.includes('other')) {
const [names, others] = contacts.split('&');
const reactors = contacts.length;
if (reactors === 1 || reactors === 2 || reactors === 3) {
return (
<span>
{window.i18n(
reactors === 1
? 'reactionPopupOne'
: reactors === 2
? 'reactionPopupTwo'
: 'reactionPopupThree',
contacts
)}{' '}
{window.i18n('reactionPopup')}
</span>
);
} else if (reactors > 3) {
return (
<span>
{names} & <StyledOthers>{others}</StyledOthers> {window.i18n('reactionTooltip')}
{window.i18n('reactionPopupMany', [contacts[0], contacts[1], contacts[3]])}{' '}
<StyledOthers>
{window.i18n(reactors === 4 ? 'otherSingular' : 'otherPlural', [`${count - 3}`])}
</StyledOthers>{' '}
{window.i18n('reactionPopup')}
</span>
);
} else {
return null;
}
return (
<span>
{contacts} {window.i18n('reactionTooltip')}
</span>
);
};
type Props = {
messageId: string;
emoji: string;
count: number;
senders: Array<string>;
tooltipPosition?: TipPosition;
onClick: (...args: Array<any>) => void;
};
export const ReactionPopup = (props: Props): ReactElement => {
const { messageId, emoji, senders, tooltipPosition = 'center', onClick } = props;
const { messageId, emoji, count, senders, tooltipPosition = 'center', onClick } = props;
const [contacts, setContacts] = useState('');
const [contacts, setContacts] = useState<Array<string>>([]);
useEffect(() => {
let isCancelled = false;
generateContacts(messageId, senders)
generateContactsString(messageId, senders)
.then(async results => {
if (isCancelled) {
return;
}
if (results && results.length > 0) {
setContacts(readableList(results));
if (results) {
setContacts(results);
}
})
.catch(() => {
@ -133,11 +153,11 @@ export const ReactionPopup = (props: Props): ReactElement => {
return () => {
isCancelled = true;
};
}, [generateContacts]);
}, [count, messageId, senders]);
return (
<StyledPopupContainer tooltipPosition={tooltipPosition} onClick={onClick}>
{Contacts(contacts)}
{Contacts(contacts, count)}
<StyledEmoji role={'img'} aria-label={nativeEmojiData?.ariaLabels?.[emoji]}>
{emoji}
</StyledEmoji>

@ -165,7 +165,7 @@ type Props = {
};
const handleSenders = (senders: Array<string>, me: string) => {
let updatedSenders = senders;
let updatedSenders = [...senders];
const blindedMe = updatedSenders.filter(isUsAnySogsFromCache);
let meIndex = -1;
@ -217,7 +217,7 @@ export const ReactListModal = (props: Props): ReactElement => {
let _senders =
reactionsMap && reactionsMap[currentReact] && reactionsMap[currentReact].senders
? Object.keys(reactionsMap[currentReact].senders)
? reactionsMap[currentReact].senders
: null;
if (_senders && !isEqual(senders, _senders)) {

@ -89,12 +89,10 @@ import { addMessagePadding } from '../session/crypto/BufferPadding';
import { getSodiumRenderer } from '../session/crypto';
import {
findCachedOurBlindedPubkeyOrLookItUp,
getUsBlindedInThatServer,
isUsAnySogsFromCache,
} from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { sogsV3FetchPreviewAndSaveIt } from '../session/apis/open_group_api/sogsv3/sogsV3FetchFile';
import { Reaction } from '../types/Reaction';
import { handleMessageReaction } from '../util/reactions';
export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public updateLastMessage: () => any;
@ -678,8 +676,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
throw new Error('Only opengroupv2 are supported now');
}
let sender = UserUtils.getOurPubKeyStrFromCache();
// an OpenGroupV2 message is just a visible message
const chatMessageParams: VisibleMessageParams = {
body: '',
@ -697,7 +693,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
if (this.id.startsWith('15')) {
window.log.info('Sending a blinded message to this user: ', this.id);
// TODO confirm this works with Reacts
await this.sendBlindedMessageRequest(chatMessageParams);
return;
}
@ -725,26 +720,14 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const openGroup = OpenGroupData.getV2OpenGroupRoom(this.id);
const blinded = Boolean(roomHasBlindEnabled(openGroup));
if (blinded) {
const blindedSender = getUsBlindedInThatServer(this);
if (blindedSender) {
sender = blindedSender;
}
}
await handleMessageReaction(reaction, sender, true);
// send with blinding if we need to
await getMessageQueue().sendToOpenGroupV2(chatMessageOpenGroupV2, roomInfos, blinded, []);
return;
} else {
await handleMessageReaction(reaction, sender, false);
}
const destinationPubkey = new PubKey(destination);
if (this.isPrivate()) {
// TODO is this still fine without isMe?
const chatMessageMe = new VisibleMessage({
...chatMessageParams,
syncTarget: this.id,

@ -319,17 +319,12 @@ async function handleSwarmMessage(
void convoToAddMessageTo.queueJob(async () => {
// this call has to be made inside the queueJob!
if (!msgModel.get('isPublic') && rawDataMessage.reaction && rawDataMessage.syncTarget) {
await handleMessageReaction(
rawDataMessage.reaction,
msgModel.get('source'),
false,
messageHash
);
// We handle reaction DataMessages separately
if (!msgModel.get('isPublic') && rawDataMessage.reaction) {
await handleMessageReaction(rawDataMessage.reaction, msgModel.get('source'));
confirm();
return;
}
const isDuplicate = await isSwarmMessageDuplicate({
source: msgModel.get('source'),
sentAt,

@ -16,7 +16,6 @@ import { GoogleChrome } from '../util';
import { appendFetchAvatarAndProfileJob } from './userProfileImageUpdates';
import { ConversationTypeEnum } from '../models/conversationAttributes';
import { getUsBlindedInThatServer } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { handleMessageReaction } from '../util/reactions';
import { Action, Reaction } from '../types/Reaction';
function contentTypeSupported(type: string): boolean {
@ -341,8 +340,6 @@ export async function handleMessageJob(
);
if (!messageModel.get('isPublic') && regularDataMessage.reaction) {
await handleMessageReaction(regularDataMessage.reaction, source, false, messageHash);
if (
regularDataMessage.reaction.action === Action.REACT &&
conversation.isPrivate() &&

@ -8,6 +8,7 @@ import {
import { addJsonContentTypeToHeaders } from './sogsV3SendMessage';
import { AbortSignal } from 'abort-controller';
import { roomHasBlindEnabled } from './sogsV3Capabilities';
import { SOGSReactorsFetchCount } from '../../../../util/reactions';
type BatchFetchRequestOptions = {
method: 'POST' | 'PUT' | 'GET' | 'DELETE';
@ -238,10 +239,9 @@ const makeBatchRequestPayload = (
if (options.messages) {
return {
method: 'GET',
// TODO Consistency across platforms with fetching reactors
path: isNumber(options.messages.sinceSeqNo)
? `/room/${options.messages.roomId}/messages/since/${options.messages.sinceSeqNo}?t=r`
: `/room/${options.messages.roomId}/messages/recent`,
? `/room/${options.messages.roomId}/messages/since/${options.messages.sinceSeqNo}?t=r&reactors=${SOGSReactorsFetchCount}`
: `/room/${options.messages.roomId}/messages/recent?reactors=${SOGSReactorsFetchCount}`,
};
}
break;

@ -2,6 +2,7 @@ import { AbortSignal } from 'abort-controller';
import { Data } from '../../../../data/data';
import { Action, OpenGroupReactionResponse, Reaction } from '../../../../types/Reaction';
import { getEmojiDataFromNative } from '../../../../util/emoji';
import { hitRateLimit } from '../../../../util/reactions';
import { OnionSending } from '../../../onions/onionSend';
import { OpenGroupPollingUtils } from '../opengroupV2/OpenGroupPollingUtils';
import { batchGlobalIsSuccess, parseBatchGlobalStatusCode } from './sogsV3BatchPoll';
@ -45,7 +46,12 @@ export const sendSogsReactionOnionV4 = async (
return false;
}
// for an invalid reaction we use https://emojipedia.org/frame-with-an-x/ as a replacement since it cannot rendered as an emoji
if (hitRateLimit()) {
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 : '🖾';
const endpoint = `/room/${room}/reaction/${reaction.id}/${emoji}`;
const method = reaction.action === Action.REACT ? 'PUT' : 'DELETE';

@ -23,6 +23,8 @@ import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/Ope
import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage';
import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage';
import { OpenGroupMessageV2 } from '../apis/open_group_api/opengroupV2/OpenGroupMessageV2';
import { AbortController } from 'abort-controller';
import { sendSogsReactionOnionV4 } from '../apis/open_group_api/sogsv3/sogsV3SendReaction';
type ClosedGroupMessageType =
| ClosedGroupVisibleMessage
@ -75,6 +77,18 @@ export class MessageQueue {
// Skipping the queue for Open Groups v2; the message is sent directly
try {
// NOTE Reactions are handled separately
if (message.reaction) {
await sendSogsReactionOnionV4(
roomInfos.serverUrl,
roomInfos.roomId,
new AbortController().signal,
message.reaction,
blinded
);
return;
}
const result = await MessageSender.sendToOpenGroupV2(
message,
roomInfos,
@ -82,11 +96,6 @@ export class MessageQueue {
filesToLink
);
// NOTE Reactions are handled in the MessageSender
if (message.reaction) {
return;
}
const { sentTimestamp, serverId } = result as OpenGroupMessageV2;
if (!serverId || serverId === -1) {
throw new Error(`Invalid serverId returned by server: ${serverId}`);

@ -26,7 +26,6 @@ import {
sendSogsMessageOnionV4,
} from '../apis/open_group_api/sogsv3/sogsV3SendMessage';
import { AbortController } from 'abort-controller';
import { sendSogsReactionOnionV4 } from '../apis/open_group_api/sogsv3/sogsV3SendReaction';
const DEFAULT_CONNECTIONS = 1;
@ -288,25 +287,14 @@ export async function sendToOpenGroupV2(
filesToLink,
});
if (rawMessage.reaction) {
const msg = await sendSogsReactionOnionV4(
roomInfos.serverUrl,
roomInfos.roomId,
new AbortController().signal,
rawMessage.reaction,
blinded
);
return msg;
} else {
const msg = await sendSogsMessageOnionV4(
roomInfos.serverUrl,
roomInfos.roomId,
new AbortController().signal,
v2Message,
blinded
);
return msg;
}
const msg = await sendSogsMessageOnionV4(
roomInfos.serverUrl,
roomInfos.roomId,
new AbortController().signal,
v2Message,
blinded
);
return msg;
}
/**

@ -915,6 +915,16 @@ export const getMessageReactsProps = createSelector(getMessagePropsByMessageId,
]);
if (msgProps.reacts) {
// NOTE we don't want to render reactions that have 'senders' as an object this is a deprecated type used during development 25/08/2022
const oldReactions = Object.values(msgProps.reacts).filter(
reaction => !Array.isArray(reaction.senders)
);
if (oldReactions.length > 0) {
msgProps.reacts = undefined;
return msgProps;
}
const sortedReacts = Object.entries(msgProps.reacts).sort((a, b) => {
return a[1].index < b[1].index ? -1 : a[1].index > b[1].index ? 1 : 0;
});

@ -54,9 +54,7 @@ describe('ReactionMessage', () => {
// Handling reaction
const updatedMessage = await handleMessageReaction(
reaction as SignalService.DataMessage.IReaction,
ourNumber,
false,
originalMessage.get('id')
ourNumber
);
expect(updatedMessage?.get('reacts'), 'original message should have reacts').to.not.be
@ -65,7 +63,7 @@ describe('ReactionMessage', () => {
expect(updatedMessage?.get('reacts')!['😄'], 'reacts should have 😄 key').to.not.be.undefined;
// tslint:disable: no-non-null-assertion
expect(
Object.keys(updatedMessage!.get('reacts')!['😄'].senders)[0],
updatedMessage!.get('reacts')!['😄'].senders[0],
'sender pubkey should match'
).to.be.equal(ourNumber);
expect(updatedMessage!.get('reacts')!['😄'].count, 'count should be 1').to.be.equal(1);
@ -87,9 +85,7 @@ describe('ReactionMessage', () => {
// Handling reaction
const updatedMessage = await handleMessageReaction(
reaction as SignalService.DataMessage.IReaction,
ourNumber,
false,
originalMessage.get('id')
ourNumber
);
expect(updatedMessage?.get('reacts'), 'original message reacts should be undefined').to.be

@ -9,6 +9,7 @@ export type LocalizerKeys =
| 'unblocked'
| 'keepDisabled'
| 'userAddedToModerators'
| 'otherSingular'
| 'to'
| 'sent'
| 'requestsPlaceholder'
@ -39,6 +40,7 @@ export type LocalizerKeys =
| 'autoUpdateLaterButtonLabel'
| 'maximumAttachments'
| 'deviceOnly'
| 'reactionPopupTwo'
| 'beginYourSession'
| 'typingIndicatorsSettingDescription'
| 'changePasswordToastDescription'
@ -140,7 +142,6 @@ export type LocalizerKeys =
| 'banUser'
| 'answeredACall'
| 'sendMessage'
| 'readableListCounterSingular'
| 'recoveryPhraseRevealMessage'
| 'showRecoveryPhrase'
| 'autoUpdateSettingDescription'
@ -186,7 +187,6 @@ export type LocalizerKeys =
| 'nameAndMessage'
| 'autoUpdateDownloadedMessage'
| 'onionPathIndicatorTitle'
| 'readableListCounterPlural'
| 'unknown'
| 'mediaMessage'
| 'addAsModerator'
@ -253,6 +253,7 @@ export type LocalizerKeys =
| 'setPassword'
| 'editMenuDeleteContact'
| 'hideMenuBarTitle'
| 'reactionPopupOne'
| 'imageCaptionIconAlt'
| 'sendRecoveryPhraseTitle'
| 'multipleJoinedTheGroup'
@ -268,6 +269,7 @@ export type LocalizerKeys =
| 'editMenuRedo'
| 'hideRequestBanner'
| 'changeNicknameMessage'
| 'reactionPopupThree'
| 'close'
| 'deleteMessageQuestion'
| 'newMessage'
@ -296,6 +298,7 @@ export type LocalizerKeys =
| 'timerOption_6_hours_abbreviated'
| 'timerOption_1_week_abbreviated'
| 'timerSetTo'
| 'otherPlural'
| 'enable'
| 'notificationSubtitle'
| 'youChangedTheTimer'
@ -307,6 +310,7 @@ export type LocalizerKeys =
| 'noNameOrMessage'
| 'pinConversationLimitTitle'
| 'noSearchResults'
| 'reactionPopup'
| 'changeNickname'
| 'userUnbanned'
| 'respondingToRequestWarning'
@ -325,7 +329,6 @@ export type LocalizerKeys =
| 'media'
| 'noMembersInThisGroup'
| 'saveLogToDesktop'
| 'reactionTooltip'
| 'copyErrorAndQuit'
| 'onlyAdminCanRemoveMembers'
| 'passwordTypeError'
@ -433,6 +436,7 @@ export type LocalizerKeys =
| 'settingsHeader'
| 'autoUpdateNewVersionMessage'
| 'oneNonImageAtATimeToast'
| 'reactionPopupMany'
| 'removePasswordTitle'
| 'iAmSure'
| 'selectMessage'

@ -123,21 +123,21 @@ export type ReactionList = Record<
{
count: number;
index: number; // relies on reactsIndex in the message model
senders: Record<string, string>; // <sender pubkey, messageHash or serverId>
senders: Array<string>;
you?: boolean; // whether we are in the senders because sometimes we dont have the full list of senders yet.
}
>;
// used when rendering reactions to guarantee sorted order using the index
export type SortedReactionList = Array<
[string, { count: number; index: number; senders: Record<string, string> }]
[string, { count: number; index: number; senders: Array<string>; you?: boolean }]
>;
export interface OpenGroupReaction {
index: number;
count: number;
first: number;
reactors: Array<string>;
you: boolean;
reactors: Array<string>;
}
export type OpenGroupReactionList = Record<string, OpenGroupReaction>;

@ -76,7 +76,7 @@ export function initialiseEmojiData(data: any) {
}
// Synchronous version of Emoji Mart's SearchIndex.search()
// If you upgrade the package things will probably break
// If we upgrade the package things will probably break
export function searchSync(query: string, args?: any): Array<any> {
if (!nativeEmojiData) {
window.log.error('No native emoji data found');

@ -2,42 +2,58 @@ import { isEmpty } from 'lodash';
import { Data } from '../data/data';
import { MessageModel } from '../models/message';
import { SignalService } from '../protobuf';
import { getUsBlindedInThatServer } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import {
getUsBlindedInThatServer,
isUsAnySogsFromCache,
} from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { UserUtils } from '../session/utils';
import { Action, OpenGroupReactionList, ReactionList, RecentReactions } from '../types/Reaction';
import { getRecentReactions, saveRecentReations } from '../util/storage';
export const SOGSReactorsFetchCount = 5;
const rateCountLimit = 20;
const rateTimeLimit = 60 * 1000;
const latestReactionTimestamps: Array<number> = [];
export function hitRateLimit(): boolean {
const timestamp = Date.now();
latestReactionTimestamps.push(timestamp);
if (latestReactionTimestamps.length > rateCountLimit) {
const firstTimestamp = latestReactionTimestamps[0];
if (timestamp - firstTimestamp < rateTimeLimit) {
latestReactionTimestamps.pop();
window.log.warn('Only 20 reactions are allowed per minute');
return true;
} else {
latestReactionTimestamps.shift();
}
}
return false;
}
/**
* Retrieves the original message of a reaction
*/
const getMessageByReaction = async (
reaction: SignalService.DataMessage.IReaction,
isOpenGroup: boolean
reaction: SignalService.DataMessage.IReaction
): Promise<MessageModel | null> => {
let originalMessage = null;
const originalMessageId = Number(reaction.id);
const originalMessageAuthor = reaction.author;
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
);
});
}
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}.`);
@ -48,7 +64,7 @@ const getMessageByReaction = async (
};
/**
* Sends a Reaction Data Message, don't use for OpenGroups
* Sends a Reaction Data Message
*/
export const sendMessageReaction = async (messageId: string, emoji: string) => {
const found = await Data.getMessageById(messageId);
@ -64,34 +80,29 @@ export const sendMessageReaction = async (messageId: string, emoji: string) => {
return;
}
const timestamp = Date.now();
latestReactionTimestamps.push(timestamp);
if (hitRateLimit()) {
return;
}
if (latestReactionTimestamps.length > rateCountLimit) {
const firstTimestamp = latestReactionTimestamps[0];
if (timestamp - firstTimestamp < rateTimeLimit) {
latestReactionTimestamps.pop();
return;
let me = UserUtils.getOurPubKeyStrFromCache();
let id = Number(found.get('sent_at'));
if (found.get('isPublic')) {
if (found.get('serverId')) {
id = found.get('serverId') || id;
me = getUsBlindedInThatServer(conversationModel) || me;
} else {
latestReactionTimestamps.shift();
window.log.warn(`Server Id was not found in message ${messageId} for opengroup reaction`);
return;
}
}
const isOpenGroup = Boolean(found?.get('isPublic'));
const id = (isOpenGroup && found.get('serverId')) || Number(found.get('sent_at'));
const me =
(isOpenGroup && getUsBlindedInThatServer(conversationModel)) ||
UserUtils.getOurPubKeyStrFromCache();
const author = found.get('source');
let action: Action = Action.REACT;
const reacts = found.get('reacts');
if (
reacts &&
Object.keys(reacts).includes(emoji) &&
Object.keys(reacts[emoji].senders).includes(me)
) {
window.log.info('found matching reaction removing it');
if (reacts?.[emoji]?.senders?.includes(me)) {
window.log.info('Found matching reaction removing it');
action = Action.REMOVE;
} else {
const reactions = getRecentReactions();
@ -124,27 +135,32 @@ 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
*/
export const handleMessageReaction = async (
reaction: SignalService.DataMessage.IReaction,
sender: string,
isOpenGroup: boolean,
messageId?: string
sender: string
) => {
if (!reaction.emoji) {
window?.log?.warn(`There is no emoji for the reaction ${messageId}.`);
window?.log?.warn(`There is no emoji for the reaction ${reaction}.`);
return;
}
const originalMessage = await getMessageByReaction(reaction, isOpenGroup);
const originalMessage = await getMessageByReaction(reaction);
if (!originalMessage) {
return;
}
if (originalMessage.get('isPublic')) {
window.log.warn("handleMessageReaction() shouldn't be used in opengroups");
return;
}
const reacts: ReactionList = originalMessage.get('reacts') ?? {};
reacts[reaction.emoji] = reacts[reaction.emoji] || { count: null, senders: {} };
reacts[reaction.emoji] = reacts[reaction.emoji] || { count: null, senders: [] };
const details = reacts[reaction.emoji] ?? {};
const senders = Object.keys(details.senders);
const senders = details.senders;
let count = details.count || 0;
window.log.info(
`${sender} ${reaction.action === Action.REACT ? 'added' : 'removed'} a ${
@ -153,33 +169,30 @@ export const handleMessageReaction = async (
);
switch (reaction.action) {
case SignalService.DataMessage.Reaction.Action.REACT:
if (senders.includes(sender) && details.senders[sender] !== '') {
window?.log?.info(
'Received duplicate message reaction. Dropping it. id:',
details.senders[sender]
);
case Action.REACT:
if (senders.includes(sender)) {
window.log.warn('Received duplicate reaction message. Ignoring it', reaction, sender);
return;
}
details.senders[sender] = messageId ?? '';
details.senders.push(sender);
count += 1;
break;
case SignalService.DataMessage.Reaction.Action.REMOVE:
case Action.REMOVE:
default:
if (senders.length > 0) {
if (senders.indexOf(sender) >= 0) {
// tslint:disable-next-line: no-dynamic-delete
delete details.senders[sender];
if (senders?.length > 0) {
const sendersIndex = senders.indexOf(sender);
if (sendersIndex >= 0) {
details.senders.splice(sendersIndex, 1);
count -= 1;
}
}
}
const count = Object.keys(details.senders).length;
if (count > 0) {
reacts[reaction.emoji].count = count;
reacts[reaction.emoji].senders = details.senders;
// sorting for open groups convos is handled by SOGS
if (!isOpenGroup && details && details.index === undefined) {
if (details && details.index === undefined) {
reacts[reaction.emoji].index = originalMessage.get('reactsIndex') ?? 0;
originalMessage.set('reactsIndex', (originalMessage.get('reactsIndex') ?? 0) + 1);
}
@ -197,7 +210,7 @@ export const handleMessageReaction = async (
};
/**
* Handle all updates to messages reactions from the SOGS API
* Handles all message reaction updates for opengroups
*/
export const handleOpenGroupMessageReactions = async (
reactions: OpenGroupReactionList,
@ -209,6 +222,11 @@ export const handleOpenGroupMessageReactions = async (
return;
}
if (!originalMessage.get('isPublic')) {
window.log.warn('handleOpenGroupMessageReactions() should only be used in opengroups');
return;
}
if (isEmpty(reactions)) {
if (originalMessage.get('reacts')) {
originalMessage.set({
@ -219,11 +237,39 @@ export const handleOpenGroupMessageReactions = async (
const reacts: ReactionList = {};
Object.keys(reactions).forEach(key => {
const emoji = decodeURI(key);
const senders: Record<string, string> = {};
const you = reactions[key].you || false;
if (you) {
if (reactions[key]?.reactors.length > 0) {
const reactorsWithoutMe = reactions[key].reactors.filter(
reactor => !isUsAnySogsFromCache(reactor)
);
// If we aren't included in the reactors then remove the extra reactor to match with the SOGSReactorsFetchCount.
if (reactorsWithoutMe.length === SOGSReactorsFetchCount) {
reactorsWithoutMe.pop();
}
const conversationModel = originalMessage?.getConversation();
if (conversationModel) {
const me =
getUsBlindedInThatServer(conversationModel) || UserUtils.getOurPubKeyStrFromCache();
reactions[key].reactors = [me, ...reactorsWithoutMe];
}
}
}
const senders: Array<string> = [];
reactions[key].reactors.forEach(reactor => {
senders[reactor] = String(serverId);
senders.push(reactor);
});
reacts[emoji] = { count: reactions[key].count, index: reactions[key].index, senders };
reacts[emoji] = {
count: reactions[key].count,
index: reactions[key].index,
senders,
you,
};
});
originalMessage.set({

@ -1,41 +0,0 @@
export const readableList = (
arr: Array<string>,
conjunction: string = '&',
limit: number = 3
): string => {
if (arr.length === 0) {
return '';
}
const count = arr.length;
switch (count) {
case 1:
return arr[0];
default:
let result = '';
let others = 0;
for (let i = 0; i < count; i++) {
if (others === 0 && i === count - 1 && i < limit) {
result += ` ${conjunction} `;
} else if (i !== 0 && i < limit) {
result += ', ';
} else if (i >= limit) {
others++;
}
if (others === 0) {
result += arr[i];
}
}
if (others > 0) {
result += ` ${conjunction} ${others} ${
others > 1
? window.i18n('readableListCounterPlural')
: window.i18n('readableListCounterSingular')
}`;
}
return result;
}
};
Loading…
Cancel
Save