2022-12-27 02:46:16 +01:00

398 lines
11 KiB
TypeScript

import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { callApi } from '../../../api/gramjs';
import * as mediaLoader from '../../../util/mediaLoader';
import { ApiMediaFormat } from '../../../api/types';
import {
selectChat,
selectChatMessage, selectCurrentChat,
selectDefaultReaction,
selectLocalAnimatedEmojiEffectByName,
selectMaxUserReactions,
selectMessageIdsByGroupId,
} from '../../selectors';
import { addMessageReaction, subtractXForEmojiInteraction, updateUnreadReactions } from '../../reducers/reactions';
import {
addChatMessagesById, addChats, addUsers, updateChatMessage,
} from '../../reducers';
import { buildCollectionByKey, omit } from '../../../util/iteratees';
import { ANIMATION_LEVEL_MAX } from '../../../config';
import { isSameReaction, getUserReactions, isMessageLocal } from '../../helpers';
const INTERACTION_RANDOM_OFFSET = 40;
let interactionLocalId = 0;
addActionHandler('loadAvailableReactions', async () => {
const result = await callApi('getAvailableReactions');
if (!result) {
return;
}
// Preload animations
result.forEach((availableReaction) => {
if (availableReaction.aroundAnimation) {
mediaLoader.fetch(`sticker${availableReaction.aroundAnimation.id}`, ApiMediaFormat.BlobUrl);
}
if (availableReaction.centerIcon) {
mediaLoader.fetch(`sticker${availableReaction.centerIcon.id}`, ApiMediaFormat.BlobUrl);
}
});
setGlobal({
...getGlobal(),
availableReactions: result,
});
});
addActionHandler('interactWithAnimatedEmoji', (global, actions, payload) => {
const {
emoji, x, y, localEffect, startSize, isReversed,
} = payload!;
const activeEmojiInteraction = {
id: interactionLocalId++,
animatedEffect: emoji || localEffect,
x: subtractXForEmojiInteraction(global, x) + Math.random()
* INTERACTION_RANDOM_OFFSET - INTERACTION_RANDOM_OFFSET / 2,
y: y + Math.random() * INTERACTION_RANDOM_OFFSET - INTERACTION_RANDOM_OFFSET / 2,
startSize,
isReversed,
};
return {
...global,
activeEmojiInteractions: [...(global.activeEmojiInteractions || []), activeEmojiInteraction],
};
});
addActionHandler('sendEmojiInteraction', (global, actions, payload) => {
const {
messageId, chatId, emoji, interactions, localEffect,
} = payload!;
const chat = selectChat(global, chatId);
if (!chat || (!emoji && !localEffect) || chatId === global.currentUserId) {
return;
}
void callApi('sendEmojiInteraction', {
chat,
messageId,
emoticon: emoji || selectLocalAnimatedEmojiEffectByName(localEffect),
timestamps: interactions,
});
});
addActionHandler('sendDefaultReaction', (global, actions, payload) => {
const {
chatId, messageId,
} = payload;
const reaction = selectDefaultReaction(global, chatId);
const message = selectChatMessage(global, chatId, messageId);
if (!reaction || !message || isMessageLocal(message)) return;
actions.toggleReaction({
chatId,
messageId,
reaction,
});
});
addActionHandler('toggleReaction', (global, actions, payload) => {
const { chatId, reaction } = payload;
let { messageId } = payload;
const chat = selectChat(global, chatId);
let message = selectChatMessage(global, chatId, messageId);
if (!chat || !message) {
return undefined;
}
const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum;
const documentGroupFirstMessageId = isInDocumentGroup
? selectMessageIdsByGroupId(global, chatId, message.groupedId!)![0]
: undefined;
message = isInDocumentGroup
? selectChatMessage(global, chatId, documentGroupFirstMessageId!) || message
: message;
messageId = message?.id || messageId;
const userReactions = getUserReactions(message);
const hasReaction = userReactions.some((userReaction) => isSameReaction(userReaction, reaction));
const newUserReactions = hasReaction
? userReactions.filter((userReaction) => !isSameReaction(userReaction, reaction)) : [...userReactions, reaction];
const limit = selectMaxUserReactions(global);
const reactions = newUserReactions.slice(-limit);
void callApi('sendReaction', { chat, messageId, reactions });
const { animationLevel } = global.settings.byKey;
if (animationLevel === ANIMATION_LEVEL_MAX) {
const newActiveReactions = hasReaction ? omit(global.activeReactions, [messageId]) : {
...global.activeReactions,
[messageId]: [
...(global.activeReactions[messageId] || []),
{
messageId,
reaction,
},
],
};
global = {
...global,
activeReactions: newActiveReactions,
};
}
return addMessageReaction(global, message, reactions);
});
addActionHandler('openChat', (global) => {
return {
...global,
activeReactions: {},
};
});
addActionHandler('stopActiveReaction', (global, actions, payload) => {
const { messageId, reaction } = payload;
if (!global.activeReactions[messageId]?.some((active) => isSameReaction(active.reaction, reaction))) {
return global;
}
const newMessageActiveReactions = global.activeReactions[messageId]
.filter((active) => !isSameReaction(active.reaction, reaction));
const newActiveReactions = newMessageActiveReactions.length ? {
...global.activeReactions,
[messageId]: newMessageActiveReactions,
} : omit(global.activeReactions, [messageId]);
return {
...global,
activeReactions: newActiveReactions,
};
});
addActionHandler('setDefaultReaction', async (global, actions, payload) => {
const { reaction } = payload;
const result = await callApi('setDefaultReaction', { reaction });
if (!result) {
return;
}
global = getGlobal();
if (!global.config) {
actions.loadConfig(); // Refetch new config, if it is somehow not loaded
return;
}
setGlobal({
...global,
config: {
...global.config,
defaultReaction: reaction,
},
});
});
addActionHandler('stopActiveEmojiInteraction', (global, actions, payload) => {
const { id } = payload;
return {
...global,
activeEmojiInteractions: global.activeEmojiInteractions?.filter((active) => active.id !== id),
};
});
addActionHandler('loadReactors', async (global, actions, payload) => {
const { chatId, messageId, reaction } = payload;
const chat = selectChat(global, chatId);
const message = selectChatMessage(global, chatId, messageId);
if (!chat || !message) {
return;
}
const offset = message.reactors?.nextOffset;
const result = await callApi('fetchMessageReactionsList', {
reaction,
chat,
messageId,
offset,
});
if (!result) {
return;
}
global = getGlobal();
if (result.users?.length) {
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
}
setGlobal(updateChatMessage(global, chatId, messageId, {
reactors: result,
}));
});
addActionHandler('loadMessageReactions', (global, actions, payload) => {
const { ids, chatId } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
callApi('fetchMessageReactions', { ids, chat });
});
addActionHandler('sendWatchingEmojiInteraction', (global, actions, payload) => {
const {
chatId, emoticon, x, y, startSize, isReversed, id,
} = payload;
const chat = selectChat(global, chatId);
if (!chat || !global.activeEmojiInteractions?.some((interaction) => interaction.id === id)
|| chatId === global.currentUserId) {
return undefined;
}
callApi('sendWatchingEmojiInteraction', { chat, emoticon });
return {
...global,
activeEmojiInteractions: global.activeEmojiInteractions.map((activeEmojiInteraction) => {
if (activeEmojiInteraction.id === id) {
return {
...activeEmojiInteraction,
x: subtractXForEmojiInteraction(global, x),
y,
startSize,
isReversed,
};
}
return activeEmojiInteraction;
}),
};
});
addActionHandler('fetchUnreadReactions', async (global, actions, payload) => {
const { chatId, offsetId } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const result = await callApi('fetchUnreadReactions', { chat, offsetId, addOffset: offsetId ? -1 : undefined });
// Server side bug, when server returns unread reactions count > 0 for deleted messages
if (!result || !result.messages.length) {
global = getGlobal();
global = updateUnreadReactions(global, chatId, {
unreadReactionsCount: 0,
});
setGlobal(global);
return;
}
const { messages, chats, users } = result;
const byId = buildCollectionByKey(messages, 'id');
const ids = Object.keys(byId).map(Number);
global = getGlobal();
global = addChatMessagesById(global, chat.id, byId);
global = addUsers(global, buildCollectionByKey(users, 'id'));
global = addChats(global, buildCollectionByKey(chats, 'id'));
global = updateUnreadReactions(global, chatId, {
unreadReactions: [...(chat.unreadReactions || []), ...ids],
});
setGlobal(global);
});
addActionHandler('animateUnreadReaction', (global, actions, payload) => {
const { messageIds } = payload;
const { animationLevel } = global.settings.byKey;
const chat = selectCurrentChat(global);
if (!chat) return undefined;
if (chat.unreadReactionsCount) {
const unreadReactionsCount = chat.unreadReactionsCount - messageIds.length;
const unreadReactions = (chat.unreadReactions || []).filter((id) => !messageIds.includes(id));
global = updateUnreadReactions(global, chat.id, {
unreadReactions,
});
setGlobal(global);
if (!unreadReactions.length && unreadReactionsCount) {
actions.fetchUnreadReactions({ chatId: chat.id, offsetId: Math.min(...messageIds) });
}
}
actions.markMessagesRead({ messageIds });
if (animationLevel !== ANIMATION_LEVEL_MAX) return undefined;
global = getGlobal();
return {
...global,
activeReactions: {
...global.activeReactions,
...Object.fromEntries(messageIds.map((messageId) => {
const message = selectChatMessage(global, chat.id, messageId);
if (!message) return undefined;
const unread = message.reactions?.recentReactions?.filter(({ isUnread }) => isUnread);
if (!unread) return undefined;
const reactions = unread.map((recent) => recent.reaction);
return [messageId, reactions.map((r) => ({
messageId,
reaction: r,
}))];
}).filter(Boolean)),
},
};
});
addActionHandler('focusNextReaction', (global, actions) => {
const chat = selectCurrentChat(global);
if (!chat?.unreadReactions) return;
actions.focusMessage({ chatId: chat.id, messageId: chat.unreadReactions[0] });
});
addActionHandler('readAllReactions', (global) => {
const chat = selectCurrentChat(global);
if (!chat) return undefined;
callApi('readAllReactions', { chat });
return updateUnreadReactions(global, chat.id, {
unreadReactionsCount: undefined,
unreadReactions: undefined,
});
});