/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import findAndSplice from '../../helpers/array/findAndSplice'; import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; import assumeType from '../../helpers/assumeType'; import callbackify from '../../helpers/callbackify'; import callbackifyAll from '../../helpers/callbackifyAll'; import copy from '../../helpers/object/copy'; import pause from '../../helpers/schedulers/pause'; import tsNow from '../../helpers/tsNow'; import {AvailableReaction, Message, MessagePeerReaction, MessagesAvailableReactions, Reaction, ReactionCount, Update, Updates} from '../../layer'; import {ReferenceContext} from '../mtproto/referenceDatabase'; import {AppManager} from './manager'; import getServerMessageId from './utils/messageId/getServerMessageId'; import reactionsEqual from './utils/reactions/reactionsEqual'; const SAVE_DOC_KEYS = [ 'static_icon' as const, 'appear_animation' as const, 'select_animation' as const, 'activate_animation' as const, 'effect_animation' as const, 'around_animation' as const, 'center_icon' as const ]; const REFERENCE_CONTEXT: ReferenceContext = { type: 'reactions' }; export class AppReactionsManager extends AppManager { private availableReactions: AvailableReaction[]; private sendReactionPromises: Map>; private lastSendingTimes: Map; protected after() { this.rootScope.addEventListener('language_change', () => { this.availableReactions = undefined; this.getAvailableReactions(); }); this.sendReactionPromises = new Map(); this.lastSendingTimes = new Map(); this.rootScope.addEventListener('user_auth', () => { setTimeout(() => { Promise.resolve(this.getAvailableReactions()).then(async(availableReactions) => { for(const availableReaction of availableReactions) { await Promise.all([ availableReaction.around_animation && this.apiFileManager.downloadMedia({media: availableReaction.around_animation}), availableReaction.static_icon && this.apiFileManager.downloadMedia({media: availableReaction.static_icon}), availableReaction.appear_animation && this.apiFileManager.downloadMedia({media: availableReaction.appear_animation}), availableReaction.center_icon && this.apiFileManager.downloadMedia({media: availableReaction.center_icon}) ]); await pause(1000); } }); }, 7.5e3); }); } public getAvailableReactions() { if(this.availableReactions) return this.availableReactions; return this.apiManager.invokeApiSingleProcess({ method: 'messages.getAvailableReactions', processResult: (messagesAvailableReactions) => { assumeType(messagesAvailableReactions); const availableReactions = this.availableReactions = messagesAvailableReactions.reactions; for(const reaction of availableReactions) { for(const key of SAVE_DOC_KEYS) { if(!reaction[key]) { continue; } reaction[key] = this.appDocsManager.saveDoc(reaction[key], REFERENCE_CONTEXT); } } return availableReactions; }, params: { hash: 0 } }); } public getActiveAvailableReactions() { return callbackify(this.getAvailableReactions(), (availableReactions) => { return availableReactions.filter((availableReaction) => !availableReaction.pFlags.inactive); }); } public getAvailableReactionsForPeer(peerId: PeerId) { const activeAvailableReactions = this.getActiveAvailableReactions(); if(peerId.isUser()) { return this.unshiftQuickReaction(activeAvailableReactions); } const chatFull = this.appProfileManager.getChatFull(peerId.toChatId()); return callbackifyAll([activeAvailableReactions, chatFull, this.getQuickReaction()], ([activeAvailableReactions, chatFull, quickReaction]) => { const chatAvailableReactions = chatFull.available_reactions ?? {_: 'chatReactionsNone'}; let filteredChatAvailableReactions: AvailableReaction[] = []; if(chatAvailableReactions._ === 'chatReactionsAll') { filteredChatAvailableReactions = activeAvailableReactions; } else if(chatAvailableReactions._ === 'chatReactionsSome') { filteredChatAvailableReactions = chatAvailableReactions.reactions.map((reaction) => { return activeAvailableReactions.find((availableReaction) => availableReaction.reaction === (reaction as Reaction.reactionEmoji).emoticon); }).filter(Boolean); } return this.unshiftQuickReactionInner(filteredChatAvailableReactions, quickReaction); }); } private unshiftQuickReactionInner(availableReactions: AvailableReaction[], quickReaction: Reaction | AvailableReaction) { if(quickReaction && quickReaction._ !== 'reactionEmoji' && quickReaction._ !== 'availableReaction') return availableReactions; const emoticon = (quickReaction as Reaction.reactionEmoji).emoticon || (quickReaction as AvailableReaction).reaction; const availableReaction = findAndSplice(availableReactions, (availableReaction) => availableReaction.reaction === emoticon); if(availableReaction) { availableReactions.unshift(availableReaction); } return availableReactions; } private unshiftQuickReaction( availableReactions: AvailableReaction[] | PromiseLike, quickReaction: ReturnType = this.getQuickReaction() ) { return callbackifyAll([ availableReactions, quickReaction ], ([availableReactions, quickReaction]) => { return this.unshiftQuickReactionInner(availableReactions, quickReaction); }); } public getAvailableReactionsByMessage(message: Message.message) { if(!message) return []; const peerId = (message.fwd_from?.channel_post && this.appPeersManager.isMegagroup(message.peerId) && message.fwdFromId) || message.peerId; return this.getAvailableReactionsForPeer(peerId); } public isReactionActive(reaction: string) { if(!this.availableReactions) return false; return !!this.availableReactions.find((availableReaction) => availableReaction.reaction === reaction); } public getQuickReaction() { return callbackifyAll([ this.apiManager.getConfig(), this.getAvailableReactions() ], ([config, availableReactions]) => { const reaction = config.reactions_default; if(reaction?._ === 'reactionEmoji') { return availableReactions.find((availableReaction) => availableReaction.reaction === reaction.emoticon); } return reaction as Reaction.reactionCustomEmoji; }); } public getReactionCached(reaction: string) { return this.availableReactions.find((availableReaction) => availableReaction.reaction === reaction); } public getReaction(reaction: string) { return callbackify(this.getAvailableReactions(), () => { return this.getReactionCached(reaction); }); } public getMessagesReactions(peerId: PeerId, mids: number[]) { return this.apiManager.invokeApiSingleProcess({ method: 'messages.getMessagesReactions', params: { id: mids.map((mid) => getServerMessageId(mid)), peer: this.appPeersManager.getInputPeerById(peerId) }, processResult: (updates) => { this.apiUpdatesManager.processUpdateMessage(updates); // const update = (updates as Updates.updates).updates.find((update) => update._ === 'updateMessageReactions') as Update.updateMessageReactions; // return update.reactions; } }); } public getMessageReactionsList(peerId: PeerId, mid: number, limit: number, reaction?: Reaction, offset?: string) { return this.apiManager.invokeApiSingleProcess({ method: 'messages.getMessageReactionsList', params: { peer: this.appPeersManager.getInputPeerById(peerId), id: getServerMessageId(mid), limit, reaction, offset }, processResult: (messageReactionsList) => { this.appUsersManager.saveApiUsers(messageReactionsList.users); return messageReactionsList; } }); } public setDefaultReaction(reaction: Reaction) { return this.apiManager.invokeApi('messages.setDefaultReaction', {reaction}).then(async(value) => { if(value) { const appConfig = await this.apiManager.getConfig(); if(appConfig) { appConfig.reactions_default = reaction; }/* else { // if no config or loading it - overwrite this.apiManager.getAppConfig(true); } */ this.rootScope.dispatchEvent('quick_reaction', reaction); } return value; }); } public async sendReaction(message: Message.message, reaction?: Reaction | AvailableReaction, onlyLocal?: boolean) { if(reaction._ === 'availableReaction') { reaction = { _: 'reactionEmoji', emoticon: reaction.reaction }; } const limit = await this.apiManager.getLimit('reactions'); const lastSendingTimeKey = message.peerId + '_' + message.mid; const lastSendingTime = this.lastSendingTimes.get(lastSendingTimeKey); if(lastSendingTime) { return; } else { this.lastSendingTimes.set(lastSendingTimeKey, Date.now()); setTimeout(() => { this.lastSendingTimes.delete(lastSendingTimeKey); }, 333); } const {peerId, mid} = message; const myPeerId = this.appPeersManager.peerId; const unsetReactionCount = (reactionCount: ReactionCount) => { --reactionCount.count; delete reactionCount.chosen_order; if(reactionsEqual(reaction as Reaction, reactionCount.reaction)) { reaction = undefined as Reaction; } if(!reactionCount.count) { indexOfAndSplice(reactions.results, reactionCount); }/* else { insertInDescendSortedArray(reactions.results, chosenReaction, 'count', chosenReactionIdx); } */ if(reactions.recent_reactions) { findAndSplice(reactions.recent_reactions, (recentReaction) => reactionsEqual(recentReaction.reaction, reactionCount.reaction) && this.appPeersManager.getPeerId(recentReaction.peer_id) === myPeerId); } if(!reactions.results.length) { reactions = undefined; } }; const canSeeList = message.reactions?.pFlags?.can_see_list || !this.appPeersManager.isBroadcast(message.peerId) || message.peerId.isUser(); if(!message.reactions) { message.reactions = { _: 'messageReactions', results: [], recent_reactions: canSeeList ? [] : undefined, pFlags: { can_see_list: canSeeList || undefined } }; } let reactions = onlyLocal ? message.reactions : copy(message.reactions); const chosenReactions = reactions.results.filter((reactionCount) => reactionCount.chosen_order !== undefined); chosenReactions.sort((a, b) => b.chosen_order - a.chosen_order); const unsetReactions: ReactionCount[] = []; const chosenReactionIdx = chosenReactions.findIndex((reactionCount) => reactionsEqual(reactionCount.reaction, reaction as Reaction)); if(chosenReactionIdx !== -1) unsetReactions.push(...chosenReactions.splice(chosenReactionIdx, 1)); unsetReactions.push(...chosenReactions.splice(limit - +(chosenReactionIdx === -1))); unsetReactions.forEach((reactionCount) => { chosenReactions.forEach((chosenReactionCount) => { if(chosenReactionCount.chosen_order > reactionCount.chosen_order) { --chosenReactionCount.chosen_order; } }); unsetReactionCount(reactionCount); }); const chosenReactionsLength = chosenReactions.length; chosenReactions.forEach((reactionCount, idx) => { reactionCount.chosen_order = chosenReactionsLength - 1 - idx; }); if(reaction) { if(!reactions) { reactions/* = message.reactions */ = { _: 'messageReactions', results: [], pFlags: {} }; if(canSeeList) { reactions.pFlags.can_see_list = true; } } let reactionCountIdx = reactions.results.findIndex((reactionCount) => reactionsEqual(reactionCount.reaction, reaction as Reaction)); let reactionCount = reactionCountIdx !== -1 && reactions.results[reactionCountIdx]; if(!reactionCount) { reactionCount = { _: 'reactionCount', count: 0, reaction }; reactionCountIdx = reactions.results.push(reactionCount) - 1; } ++reactionCount.count; reactionCount.chosen_order = chosenReactions.length ? chosenReactions[0].chosen_order + 1 : 0; chosenReactions.unshift(reactionCount); if(!reactions.recent_reactions && canSeeList) { reactions.recent_reactions = []; } if(reactions.recent_reactions) { const peerReaction: MessagePeerReaction = { _: 'messagePeerReaction', reaction, peer_id: this.appPeersManager.getOutputPeer(myPeerId), pFlags: {}, date: tsNow(true) }; if(!this.appPeersManager.isMegagroup(peerId) && false) { reactions.recent_reactions.push(peerReaction); reactions.recent_reactions = reactions.recent_reactions.slice(-3); } else { reactions.recent_reactions.unshift(peerReaction); reactions.recent_reactions = reactions.recent_reactions.slice(0, 3); } } // insertInDescendSortedArray(reactions.results, reactionCount, 'count', reactionCountIdx); } const availableReactions = this.availableReactions; if(reactions && availableReactions?.length) { const indexes: Map = new Map(); availableReactions.forEach((availableReaction, idx) => { indexes.set(availableReaction.reaction, idx); }); reactions.results.sort((a, b) => { const id1 = (a.reaction as Reaction.reactionCustomEmoji).document_id || (a.reaction as Reaction.reactionEmoji).emoticon; const id2 = (b.reaction as Reaction.reactionCustomEmoji).document_id || (b.reaction as Reaction.reactionEmoji).emoticon; return (b.count - a.count) || ((indexes.get(id1) ?? 0) - (indexes.get(id2) ?? 0)); }); } if(onlyLocal) { message.reactions = reactions; this.rootScope.dispatchEvent('messages_reactions', [{message, changedResults: []}]); return Promise.resolve(); } this.apiUpdatesManager.processLocalUpdate({ _: 'updateMessageReactions', peer: message.peer_id, msg_id: message.id, reactions: reactions, local: true }); const promiseKey = [peerId, mid].join('-'); const msgId = getServerMessageId(mid); const promise = this.apiManager.invokeApi('messages.sendReaction', { peer: this.appPeersManager.getInputPeerById(peerId), msg_id: msgId, reaction: chosenReactions.map((reactionCount) => reactionCount.reaction) }).then((updates) => { assumeType(updates); const editMessageUpdateIdx = updates.updates.findIndex((update) => update._ === 'updateEditMessage' || update._ === 'updateEditChannelMessage'); if(editMessageUpdateIdx !== -1) { const editMessageUpdate = updates.updates[editMessageUpdateIdx] as Update.updateEditMessage | Update.updateEditChannelMessage; updates.updates[editMessageUpdateIdx] = { _: 'updateMessageReactions', msg_id: msgId, peer: this.appPeersManager.getOutputPeer(peerId), reactions: (editMessageUpdate.message as Message.message).reactions, pts: editMessageUpdate.pts, pts_count: editMessageUpdate.pts_count }; } this.apiUpdatesManager.processUpdateMessage(updates); }).catch((err) => { if(err.type === 'REACTION_INVALID' && this.sendReactionPromises.get(promiseKey) === promise) { this.sendReaction(message, chosenReactions[0]?.reaction, true); } }).finally(() => { if(this.sendReactionPromises.get(promiseKey) === promise) { this.sendReactionPromises.delete(promiseKey); } }); this.sendReactionPromises.set(promiseKey, promise); return promise; } }