398 lines
11 KiB
TypeScript
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,
|
|
});
|
|
});
|