Alexander Zinchuk 11cbab7d39 Floating Actions Buttons: Fix animations (#3575)
Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
2024-04-19 13:38:50 +04:00

558 lines
16 KiB
TypeScript

import type { ApiMessage, ApiUpdateChat } from '../../../api/types';
import type { ActionReturnType } from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types';
import { ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS } from '../../../config';
import { buildCollectionByKey, omit } from '../../../util/iteratees';
import { isLocalMessageId } from '../../../util/messageKey';
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
import { buildLocalMessage } from '../../../api/gramjs/apiBuilders/messages';
import { checkIfHasUnreadReactions, isChatChannel } from '../../helpers';
import {
addActionHandler, getGlobal, setGlobal,
} from '../../index';
import {
deleteChatMessages,
leaveChat,
replaceThreadParam,
updateChat,
updateChatFullInfo,
updateChatListIds,
updateChatListType,
updatePeerStoriesHidden,
updateTopic,
} from '../../reducers';
import { updateUnreadReactions } from '../../reducers/reactions';
import { updateTabState } from '../../reducers/tabs';
import {
selectChat,
selectChatFullInfo,
selectChatLastMessageId,
selectChatListType,
selectChatMessages,
selectCommonBoxChatId,
selectCurrentMessageList,
selectIsChatListed,
selectTabState,
selectThreadParam,
selectTopicFromMessage,
} from '../../selectors';
const TYPING_STATUS_CLEAR_DELAY = 6000; // 6 seconds
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) {
case 'updateChat': {
const { isForum: prevIsForum, lastReadOutboxMessageId } = selectChat(global, update.id) || {};
if (update.chat.lastReadOutboxMessageId && lastReadOutboxMessageId
&& update.chat.lastReadOutboxMessageId < lastReadOutboxMessageId) {
update = {
...update,
chat: omit(update.chat, ['lastReadInboxMessageId']),
};
}
const localChat = selectChat(global, update.id);
global = updateChat(global, update.id, update.chat, update.newProfilePhoto);
if (localChat?.areStoriesHidden !== update.chat.areStoriesHidden) {
global = updatePeerStoriesHidden(global, update.id, update.chat.areStoriesHidden || false);
}
setGlobal(global);
if (!update.noTopChatsRequest && !selectIsChatListed(global, update.id)) {
// Chat can appear in dialogs list.
actions.loadTopChats();
}
if (update.chat.id) {
closeMessageNotifications({
chatId: update.chat.id,
lastReadInboxMessageId: update.chat.lastReadInboxMessageId,
});
}
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const { chatId: currentChatId } = selectCurrentMessageList(global, tabId) || {};
const chatUpdate = update as ApiUpdateChat;
// The property `isForum` was changed in another client
if (currentChatId === chatUpdate.id
&& 'isForum' in chatUpdate.chat && prevIsForum !== chatUpdate.chat.isForum) {
if (prevIsForum) {
actions.closeForumPanel({ tabId });
}
actions.openChat({ id: currentChatId, tabId });
}
});
return undefined;
}
case 'updateChatJoin': {
const listType = selectChatListType(global, update.id);
const chat = selectChat(global, update.id);
if (chat && isChatChannel(chat)) {
actions.fetchChannelRecommendations({ chatId: chat.id });
const lastMessageId = selectChatLastMessageId(global, chat.id);
const localMessage = buildLocalMessage(chat, lastMessageId);
localMessage.content.action = {
text: 'you joined this channel',
translationValues: ['ChannelJoined'],
type: 'joinedChannel',
targetChatId: chat.id,
};
actions.apiUpdate({
'@type': 'newMessage',
id: localMessage.id,
chatId: chat.id,
message: localMessage,
});
}
if (!listType) {
return undefined;
}
global = updateChatListIds(global, listType, [update.id]);
global = updateChat(global, update.id, { isNotJoined: false });
setGlobal(global);
if (chat) {
actions.requestChatUpdate({ chatId: chat.id });
}
return undefined;
}
case 'updateChatLeave': {
global = leaveChat(global, update.id);
const chat = selectChat(global, update.id);
if (chat && isChatChannel(chat)) {
const chatMessages = selectChatMessages(global, update.id);
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
global = deleteChatMessages(global, chat.id, localMessageIds);
}
return global;
}
case 'updateChatInbox': {
return updateChat(global, update.id, update.chat);
}
case 'updateChatTypingStatus': {
const { id, threadId = MAIN_THREAD_ID, typingStatus } = update;
global = replaceThreadParam(global, id, threadId, 'typingStatus', typingStatus);
setGlobal(global);
setTimeout(() => {
global = getGlobal();
const currentTypingStatus = selectThreadParam(global, id, threadId, 'typingStatus');
if (typingStatus && currentTypingStatus && typingStatus.timestamp === currentTypingStatus.timestamp) {
global = replaceThreadParam(global, id, threadId, 'typingStatus', undefined);
setGlobal(global);
}
}, TYPING_STATUS_CLEAR_DELAY);
return undefined;
}
case 'newMessage': {
const { message } = update;
if (message.senderId === global.currentUserId && !message.isFromScheduled) {
return undefined;
}
const chat = selectChat(global, update.chatId);
if (!chat) {
return undefined;
}
const hasMention = Boolean(update.message.id && update.message.hasUnreadMention);
global = updateChat(global, update.chatId, {
unreadCount: chat.unreadCount ? chat.unreadCount + 1 : 1,
...(hasMention && { unreadMentionsCount: (chat.unreadMentionsCount || 0) + 1 }),
});
if (hasMention) {
global = updateChat(global, update.chatId, {
unreadMentions: [...(chat.unreadMentions || []), update.message.id!],
});
}
const topic = chat.isForum ? selectTopicFromMessage(global, message as ApiMessage) : undefined;
if (topic) {
global = updateTopic(global, update.chatId, topic.id, {
unreadCount: topic.unreadCount ? topic.unreadCount + 1 : 1,
});
}
setGlobal(global);
notifyAboutMessage({
chat,
message,
});
return undefined;
}
case 'updateCommonBoxMessages':
case 'updateChannelMessages': {
const { ids, messageUpdate } = update;
ids.forEach((id) => {
const chatId = ('channelId' in update ? update.channelId : selectCommonBoxChatId(global, id))!;
const chat = selectChat(global, chatId);
if (messageUpdate.reactions && chat?.unreadReactionsCount
&& !checkIfHasUnreadReactions(global, messageUpdate.reactions)) {
global = updateUnreadReactions(global, chatId, {
unreadReactionsCount: Math.max(chat.unreadReactionsCount - 1, 0) || undefined,
unreadReactions: chat.unreadReactions?.filter((i) => i !== id),
});
}
if (!messageUpdate.hasUnreadMention && chat?.unreadMentionsCount) {
global = updateChat(global, chatId, {
unreadMentionsCount: Math.max(chat.unreadMentionsCount - 1, 0) || undefined,
unreadMentions: chat.unreadMentions?.filter((i) => i !== id),
});
}
});
return global;
}
case 'updateChatFullInfo': {
return updateChatFullInfo(global, update.id, update.fullInfo);
}
case 'updatePinnedChatIds': {
const { ids, folderId } = update;
const listType = folderId === ARCHIVED_FOLDER_ID ? 'archived' : 'active';
return {
...global,
chats: {
...global.chats,
orderedPinnedIds: {
...global.chats.orderedPinnedIds,
[listType]: ids.length ? ids : undefined,
},
},
};
}
case 'updatePinnedSavedDialogIds': {
const { ids } = update;
return {
...global,
chats: {
...global.chats,
orderedPinnedIds: {
...global.chats.orderedPinnedIds,
saved: ids.length ? ids : undefined,
},
},
};
}
case 'updateChatPinned': {
const { id, isPinned } = update;
const listType = selectChatListType(global, id);
if (!listType) {
return undefined;
}
const { [listType]: orderedPinnedIds } = global.chats.orderedPinnedIds;
let newOrderedPinnedIds = orderedPinnedIds || [];
if (!isPinned) {
newOrderedPinnedIds = newOrderedPinnedIds.filter((pinnedId) => pinnedId !== id);
} else if (!newOrderedPinnedIds.includes(id)) {
// When moving pinned chats to archive, active ordered pinned ids don't get updated
// (to preserve chat pinned state when it returns from archive)
// If user already has max pinned chats, we should check for orderedIds
// that don't point to listed chats
if (listType === 'active' && newOrderedPinnedIds.length >= MAX_ACTIVE_PINNED_CHATS) {
const listIds = global.chats.listIds.active;
newOrderedPinnedIds = newOrderedPinnedIds.filter((pinnedId) => listIds && listIds.includes(pinnedId));
}
newOrderedPinnedIds = [id, ...newOrderedPinnedIds];
}
return {
...global,
chats: {
...global.chats,
orderedPinnedIds: {
...global.chats.orderedPinnedIds,
[listType]: newOrderedPinnedIds.length ? newOrderedPinnedIds : undefined,
},
},
};
}
case 'updateSavedDialogPinned': {
const { id, isPinned } = update;
const { saved: orderedPinnedIds } = global.chats.orderedPinnedIds;
let newOrderedPinnedIds = orderedPinnedIds || [];
if (!isPinned) {
newOrderedPinnedIds = newOrderedPinnedIds.filter((pinnedId) => pinnedId !== id);
} else if (!newOrderedPinnedIds.includes(id)) {
newOrderedPinnedIds = [id, ...newOrderedPinnedIds];
}
return {
...global,
chats: {
...global.chats,
orderedPinnedIds: {
...global.chats.orderedPinnedIds,
saved: newOrderedPinnedIds.length ? newOrderedPinnedIds : undefined,
},
},
};
}
case 'updateChatListType': {
const { id, folderId } = update;
return updateChatListType(global, id, folderId);
}
case 'updateChatFolder': {
const { id, folder } = update;
const { byId: chatFoldersById, orderedIds } = global.chatFolders;
const isDeleted = folder === undefined;
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const tabState = selectTabState(global, tabId);
const isFolderActive = Object.values(chatFoldersById)[tabState.activeChatFolder - 1]?.id === id;
if (isFolderActive) {
global = updateTabState(global, { activeChatFolder: 0 }, tabId);
}
});
const newChatFoldersById = !isDeleted ? { ...chatFoldersById, [id]: folder } : omit(chatFoldersById, [id]);
const newOrderedIds = !isDeleted
? orderedIds?.includes(id) ? orderedIds : [...(orderedIds || []), id]
: orderedIds?.filter((orderedId) => orderedId !== id);
return {
...global,
chatFolders: {
...global.chatFolders,
byId: newChatFoldersById,
orderedIds: newOrderedIds,
invites: omit(global.chatFolders.invites, [id]),
},
};
}
case 'updateChatFoldersOrder': {
const { orderedIds } = update;
return {
...global,
chatFolders: {
...global.chatFolders,
orderedIds,
},
};
}
case 'updateRecommendedChatFolders': {
const { folders } = update;
return {
...global,
chatFolders: {
...global.chatFolders,
recommended: folders,
},
};
}
case 'updateChatMembers': {
const targetChatFullInfo = selectChatFullInfo(global, update.id);
const { replacedMembers, addedMember, deletedMemberId } = update;
if (!targetChatFullInfo) {
return undefined;
}
let shouldUpdate = false;
let members = targetChatFullInfo?.members
? [...targetChatFullInfo.members]
: [];
if (replacedMembers) {
members = replacedMembers;
shouldUpdate = true;
} else if (addedMember) {
if (
!members.length
|| !members.some((m) => m.userId === addedMember.userId)
) {
members.push(addedMember);
shouldUpdate = true;
}
} else if (members.length && deletedMemberId) {
const deleteIndex = members.findIndex((m) => m.userId === deletedMemberId);
if (deleteIndex > -1) {
members.slice(deleteIndex, 1);
shouldUpdate = true;
}
}
if (shouldUpdate) {
const adminMembers = members.filter(({ isOwner, isAdmin }) => isOwner || isAdmin);
// TODO Kicked members?
global = updateChat(global, update.id, { membersCount: members.length });
global = updateChatFullInfo(global, update.id, {
members,
adminMembersById: buildCollectionByKey(adminMembers, 'userId'),
});
return global;
}
return undefined;
}
case 'deleteProfilePhotos': {
const { chatId, ids } = update;
const chat = global.chats.byId[chatId];
if (chat?.photos) {
return updateChat(global, chatId, {
photos: chat.photos.filter((photo) => !ids.includes(photo.id)),
});
}
return undefined;
}
case 'draftMessage': {
const {
chatId, threadId, draft,
} = update;
const chat = global.chats.byId[chatId];
if (!chat) {
return undefined;
}
global = replaceThreadParam(global, chatId, threadId || MAIN_THREAD_ID, 'draft', draft);
global = updateChat(global, chatId, { draftDate: draft?.date });
return global;
}
case 'showInvite': {
const { data } = update;
Object.values(global.byTabId).forEach(({ id: tabId }) => {
actions.showDialog({ data, tabId });
});
return undefined;
}
case 'updatePendingJoinRequests': {
const { chatId, requestsPending, recentRequesterIds } = update;
const chat = global.chats.byId[chatId];
if (!chat) {
return undefined;
}
global = updateChatFullInfo(global, chatId, {
requestsPending,
recentRequesterIds,
});
setGlobal(global);
actions.loadChatJoinRequests({ chatId });
return undefined;
}
case 'updatePinnedTopic': {
const { chatId, topicId, isPinned } = update;
const chat = global.chats.byId[chatId];
if (!chat) {
return undefined;
}
global = updateTopic(global, chatId, topicId, {
isPinned,
});
setGlobal(global);
return undefined;
}
case 'updatePinnedTopicsOrder': {
const { chatId, order } = update;
const chat = global.chats.byId[chatId];
if (!chat) return undefined;
global = updateChat(global, chatId, {
orderedPinnedTopicIds: order,
});
setGlobal(global);
return undefined;
}
case 'updateTopic': {
const { chatId, topicId } = update;
const chat = selectChat(global, chatId);
if (!chat?.isForum) return undefined;
actions.loadTopicById({ chatId, topicId });
return undefined;
}
case 'updateTopics': {
const { chatId } = update;
const chat = selectChat(global, chatId);
if (!chat?.isForum) return undefined;
actions.loadTopics({ chatId, force: true });
return undefined;
}
case 'updateViewForumAsMessages': {
const { chatId, isEnabled } = update;
const chat = selectChat(global, chatId);
if (!chat?.isForum) return undefined;
global = updateChat(global, chatId, {
isForumAsMessages: isEnabled,
});
setGlobal(global);
}
}
return undefined;
});