2024-11-02 21:11:50 +04:00

1029 lines
28 KiB
TypeScript

import type {
ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo,
} from '../../api/types';
import type { FocusDirection, ScrollTargetPosition, ThreadId } from '../../types';
import type {
GlobalState, MessageList, MessageListType, TabArgs, TabState, TabThread, Thread,
} from '../types';
import { MAIN_THREAD_ID } from '../../api/types';
import {
IS_MOCKED_CLIENT, IS_TEST, MESSAGE_LIST_SLICE, MESSAGE_LIST_VIEWPORT_LIMIT, TMP_CHAT_ID,
} from '../../config';
import { areDeepEqual } from '../../util/areDeepEqual';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import {
areSortedArraysEqual, excludeSortedArray, omit, pick, pickTruthy, unique,
} from '../../util/iteratees';
import { isLocalMessageId, type MessageKey } from '../../util/keys/messageKey';
import {
hasMessageTtl, isMediaLoadableInViewer, mergeIdRanges, orderHistoryIds, orderPinnedIds,
} from '../helpers';
import {
selectChatMessage,
selectChatMessages,
selectChatScheduledMessages,
selectCurrentMessageIds,
selectCurrentMessageList,
selectListedIds,
selectMessageIdsByGroupId,
selectOutlyingLists,
selectPinnedIds,
selectPoll,
selectQuickReplyMessage,
selectScheduledIds,
selectScheduledMessage,
selectTabState,
selectThreadIdFromMessage,
selectThreadInfo,
selectViewportIds,
} from '../selectors';
import { removeIdFromSearchResults } from './middleSearch';
import { updateTabState } from './tabs';
import { clearMessageTranslation } from './translations';
type MessageStoreSections = GlobalState['messages']['byChatId'][string];
export function updateCurrentMessageList<T extends GlobalState>(
global: T,
chatId: string | undefined,
threadId: ThreadId = MAIN_THREAD_ID,
type: MessageListType = 'thread',
shouldReplaceHistory?: boolean,
shouldReplaceLast?: boolean,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const { messageLists } = selectTabState(global, tabId);
let newMessageLists: MessageList[] = messageLists;
if (shouldReplaceHistory || (IS_TEST && !IS_MOCKED_CLIENT)) {
newMessageLists = chatId ? [{ chatId, threadId, type }] : [];
} else if (chatId) {
const current = messageLists[messageLists.length - 1];
if (current?.chatId === chatId && current.threadId === threadId && current.type === type) {
return global;
}
if (current && (current.chatId === TMP_CHAT_ID || shouldReplaceLast)) {
newMessageLists = [...messageLists.slice(0, -1), { chatId, threadId, type }];
} else {
const previous = messageLists[messageLists.length - 2];
if (previous?.chatId === chatId && previous.threadId === threadId && previous.type === type) {
newMessageLists = messageLists.slice(0, -1);
} else {
newMessageLists = [...messageLists, { chatId, threadId, type }];
}
}
} else {
newMessageLists = messageLists.slice(0, -1);
}
return updateTabState(global, {
messageLists: newMessageLists,
}, tabId);
}
function replaceChatMessages<T extends GlobalState>(global: T, chatId: string, newById: Record<number, ApiMessage>): T {
return updateMessageStore(global, chatId, {
byId: newById,
});
}
export function updateTabThread<T extends GlobalState>(
global: T, chatId: string, threadId: ThreadId, threadUpdate: Partial<TabThread>,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const tabState = selectTabState(global, tabId);
const current = tabState.tabThreads[chatId]?.[threadId] || {};
return updateTabState(global, {
tabThreads: {
...tabState.tabThreads,
[chatId]: {
...tabState.tabThreads[chatId],
[threadId]: {
...current,
...threadUpdate,
},
},
},
}, tabId);
}
export function updateThread<T extends GlobalState>(
global: T, chatId: string, threadId: ThreadId, threadUpdate: Partial<Thread> | undefined,
): T {
if (!threadUpdate) {
return updateMessageStore(global, chatId, {
threadsById: omit(global.messages.byChatId[chatId]?.threadsById, [threadId]),
});
}
const current = global.messages.byChatId[chatId];
return updateMessageStore(global, chatId, {
threadsById: {
...(current?.threadsById),
[threadId]: {
...(current?.threadsById[threadId]),
...threadUpdate,
},
},
});
}
export function updateMessageStore<T extends GlobalState>(
global: T, chatId: string, update: Partial<MessageStoreSections>,
): T {
const current = global.messages.byChatId[chatId] || { byId: {}, threadsById: {} };
return {
...global,
messages: {
...global.messages,
byChatId: {
...global.messages.byChatId,
[chatId]: {
...current,
...update,
},
},
},
};
}
export function replaceTabThreadParam<T extends GlobalState, K extends keyof TabThread>(
global: T, chatId: string, threadId: ThreadId, paramName: K, newValue: TabThread[K] | undefined,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
if (paramName === 'viewportIds') {
global = replaceThreadParam(
global, chatId, threadId, 'lastViewportIds', newValue as number[] | undefined,
);
}
return updateTabThread(global, chatId, threadId, { [paramName]: newValue }, tabId);
}
export function replaceThreadParam<T extends GlobalState, K extends keyof Thread>(
global: T, chatId: string, threadId: ThreadId, paramName: K, newValue: Thread[K] | undefined,
) {
return updateThread(global, chatId, threadId, { [paramName]: newValue });
}
export function addMessages<T extends GlobalState>(
global: T, messages: ApiMessage[],
): T {
const addedByChatId = messages.reduce((messagesByChatId, message: ApiMessage) => {
if (!messagesByChatId[message.chatId]) {
messagesByChatId[message.chatId] = {};
}
messagesByChatId[message.chatId][message.id] = message;
return messagesByChatId;
}, {} as Record<string, Record<number, ApiMessage>>);
Object.keys(addedByChatId).forEach((chatId) => {
global = addChatMessagesById(global, chatId, addedByChatId[chatId]);
});
return global;
}
export function replaceMessages<T extends GlobalState>(
global: T, messages: ApiMessage[],
): T {
const updatedByChatId = messages.reduce((messagesByChatId, message: ApiMessage) => {
if (!messagesByChatId[message.chatId]) {
messagesByChatId[message.chatId] = {};
}
messagesByChatId[message.chatId][message.id] = message;
return messagesByChatId;
}, {} as Record<string, Record<number, ApiMessage>>);
Object.keys(updatedByChatId).forEach((chatId) => {
const currentById = selectChatMessages(global, chatId) || {};
const newById = {
...currentById,
...updatedByChatId[chatId],
};
global = replaceChatMessages(global, chatId, newById);
});
return global;
}
export function addChatMessagesById<T extends GlobalState>(
global: T, chatId: string, newById: Record<number, ApiMessage>,
): T {
const byId = selectChatMessages(global, chatId);
if (byId && Object.keys(newById).every((newId) => Boolean(byId[Number(newId)]))) {
return global;
}
return replaceChatMessages(global, chatId, {
...newById,
...byId,
});
}
export function updateChatMessage<T extends GlobalState>(
global: T, chatId: string, messageId: number, messageUpdate: Partial<ApiMessage>, withDeepCheck = false,
): T {
const byId = selectChatMessages(global, chatId) || {};
const message = byId[messageId];
if (withDeepCheck && message) {
const updateKeys = Object.keys(messageUpdate) as (keyof ApiMessage)[];
if (areDeepEqual(pick(message, updateKeys), messageUpdate)) {
return global;
}
}
if (message && messageUpdate.isMediaUnread === false && hasMessageTtl(message)) {
if (message.content.voice) {
messageUpdate.content = {
...messageUpdate.content,
voice: undefined,
isExpiredVoice: true,
};
} else if (message.content.video?.isRound) {
messageUpdate.content = {
...messageUpdate.content,
video: undefined,
isExpiredRoundVideo: true,
};
}
}
const updatedMessage = {
...message,
...messageUpdate,
};
if (!updatedMessage.id) {
return global;
}
return replaceChatMessages(global, chatId, {
...byId,
[messageId]: updatedMessage,
});
}
export function updateScheduledMessage<T extends GlobalState>(
global: T, chatId: string, messageId: number, messageUpdate: Partial<ApiMessage>,
): T {
const message = selectScheduledMessage(global, chatId, messageId)!;
const updatedMessage = {
...message,
...messageUpdate,
};
if (!updatedMessage.id) {
return global;
}
return updateScheduledMessages(global, chatId, {
[messageId]: updatedMessage,
});
}
export function updateQuickReplyMessage<T extends GlobalState>(
global: T, messageId: number, messageUpdate: Partial<ApiMessage>,
): T {
const message = selectQuickReplyMessage(global, messageId);
const updatedMessage = {
...message,
...messageUpdate,
};
if (!updatedMessage.id) {
return global;
}
return updateQuickReplyMessages(global, {
[messageId]: updatedMessage,
});
}
export function deleteQuickReplyMessages<T extends GlobalState>(
global: T, messageIds: number[],
): T {
const byId = global.quickReplies.messagesById;
const newById = omit(byId, messageIds);
return {
...global,
quickReplies: {
...global.quickReplies,
messagesById: newById,
},
};
}
export function deleteChatMessages<T extends GlobalState>(
global: T,
chatId: string,
messageIds: number[],
): T {
const byId = selectChatMessages(global, chatId);
if (!byId) {
return global;
}
orderHistoryIds(messageIds);
const updatedThreads = new Map<ThreadId, number[]>();
updatedThreads.set(MAIN_THREAD_ID, messageIds);
const mediaIdsToRemove: number[] = [];
messageIds.forEach((messageId) => {
const message = byId[messageId];
if (!message) return;
if (isMediaLoadableInViewer(message)) {
mediaIdsToRemove.push(messageId);
}
const threadId = selectThreadIdFromMessage(global, message);
if (!threadId || threadId === MAIN_THREAD_ID) {
return;
}
const threadMessages = updatedThreads.get(threadId) || [];
threadMessages.push(messageId);
updatedThreads.set(threadId, threadMessages);
global = clearMessageTranslation(global, chatId, messageId);
});
const deletedForwardedPosts = Object.values(pickTruthy(byId, messageIds)).filter(
({ forwardInfo }) => forwardInfo?.isLinkedChannelPost,
);
updatedThreads.forEach((threadMessageIds, threadId) => {
const threadInfo = selectThreadInfo(global, chatId, threadId);
let listedIds = selectListedIds(global, chatId, threadId);
let pinnedIds = selectPinnedIds(global, chatId, threadId);
let outlyingLists = selectOutlyingLists(global, chatId, threadId);
let newMessageCount = threadInfo?.messagesCount;
if (listedIds) {
listedIds = excludeSortedArray(listedIds, threadMessageIds);
}
if (outlyingLists) {
outlyingLists = outlyingLists.map((list) => excludeSortedArray(list, threadMessageIds));
}
if (pinnedIds) {
pinnedIds = excludeSortedArray(pinnedIds, orderPinnedIds(threadMessageIds));
}
const nonLocalMessageCount = threadMessageIds.filter((id) => !isLocalMessageId(id)).length;
if (newMessageCount !== undefined) {
newMessageCount -= nonLocalMessageCount;
}
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const tabState = selectTabState(global, tabId);
const activeDownloadsInChat = Object.entries(tabState.activeDownloads).filter(
([, { originChatId, originMessageId }]) => originChatId === chatId && originMessageId,
);
activeDownloadsInChat.forEach(([mediaHash, context]) => {
if (messageIds.includes(context.originMessageId!)) {
global = cancelMessageMediaDownload(global, [mediaHash], tabId);
}
});
mediaIdsToRemove.forEach((mediaId) => {
global = removeIdFromSearchResults(global, chatId, threadId, mediaId, tabId);
});
const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
if (!viewportIds) return;
const newViewportIds = excludeSortedArray(viewportIds, messageIds);
global = replaceTabThreadParam(
global,
chatId,
threadId,
'viewportIds',
newViewportIds.length === 0 ? undefined : newViewportIds,
tabId,
);
});
global = replaceThreadParam(global, chatId, threadId, 'listedIds', listedIds);
global = replaceThreadParam(global, chatId, threadId, 'outlyingLists', outlyingLists);
global = replaceThreadParam(global, chatId, threadId, 'pinnedIds', pinnedIds);
if (threadInfo && newMessageCount !== undefined) {
global = updateThreadInfo(global, chatId, threadId, {
messagesCount: newMessageCount,
});
}
});
if (deletedForwardedPosts.length) {
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const currentMessageList = selectCurrentMessageList(global, tabId);
const canDeleteCurrentThread = currentMessageList && currentMessageList.chatId === chatId
&& currentMessageList.type === 'thread';
const currentThreadId = currentMessageList?.threadId;
deletedForwardedPosts.forEach((message) => {
const { fromChatId, fromMessageId } = message.forwardInfo!;
const originalPost = selectChatMessage(global, fromChatId!, fromMessageId!);
if (canDeleteCurrentThread && currentThreadId === message.id) {
global = updateCurrentMessageList(global, chatId, undefined, undefined, undefined, undefined, tabId);
}
if (originalPost) {
global = updateThread(global, fromChatId!, fromMessageId!, undefined);
}
});
});
}
const newById = omit(byId, messageIds);
global = replaceChatMessages(global, chatId, newById);
return global;
}
export function deleteChatScheduledMessages<T extends GlobalState>(
global: T,
chatId: string,
messageIds: number[],
): T {
const byId = selectChatScheduledMessages(global, chatId);
if (!byId) {
return global;
}
const newById = omit(byId, messageIds);
let scheduledIds = selectScheduledIds(global, chatId, MAIN_THREAD_ID);
if (scheduledIds) {
messageIds.forEach((messageId) => {
if (scheduledIds!.includes(messageId)) {
scheduledIds = scheduledIds!.filter((id) => id !== messageId);
}
});
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'scheduledIds', scheduledIds);
Object.entries(global.messages.byChatId[chatId].threadsById).forEach(([threadId, thread]) => {
if (thread.scheduledIds) {
const newScheduledIds = thread.scheduledIds.filter((id) => !messageIds.includes(id));
global = replaceThreadParam(global, chatId, Number(threadId), 'scheduledIds', newScheduledIds);
}
});
}
global = {
...global,
scheduledMessages: {
byChatId: {
...global.scheduledMessages.byChatId,
[chatId]: {
byId: newById,
},
},
},
};
return global;
}
export function updateListedIds<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
idsUpdate: number[],
): T {
const listedIds = selectListedIds(global, chatId, threadId);
const newIds = listedIds?.length
? idsUpdate.filter((id) => !listedIds.includes(id))
: idsUpdate;
if (listedIds && !newIds.length) {
return global;
}
return replaceThreadParam(global, chatId, threadId, 'listedIds', orderHistoryIds([
...(listedIds || []),
...newIds,
]));
}
export function removeOutlyingList<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
list: number[],
): T {
const outlyingLists = selectOutlyingLists(global, chatId, threadId);
if (!outlyingLists) {
return global;
}
const newOutlyingLists = outlyingLists.filter((l) => l !== list);
return replaceThreadParam(global, chatId, threadId, 'outlyingLists', newOutlyingLists);
}
export function updateOutlyingLists<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
idsUpdate: number[],
): T {
if (!idsUpdate.length) return global;
const outlyingLists = selectOutlyingLists(global, chatId, threadId);
const newOutlyingLists = mergeIdRanges(outlyingLists || [], idsUpdate);
return replaceThreadParam(global, chatId, threadId, 'outlyingLists', newOutlyingLists);
}
export function addViewportId<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
newId: number,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const viewportIds = selectViewportIds(global, chatId, threadId, tabId) || [];
if (viewportIds.includes(newId)) {
return global;
}
const newIds = orderHistoryIds([
...(
viewportIds.length < MESSAGE_LIST_VIEWPORT_LIMIT
? viewportIds
: viewportIds.slice(-(MESSAGE_LIST_SLICE / 2))
),
newId,
]);
return replaceTabThreadParam(global, chatId, threadId, 'viewportIds', newIds, tabId);
}
export function safeReplaceViewportIds<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
newViewportIds: number[],
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const currentIds = selectViewportIds(global, chatId, threadId, tabId) || [];
const newIds = orderHistoryIds(newViewportIds);
return replaceTabThreadParam(
global,
chatId,
threadId,
'viewportIds',
areSortedArraysEqual(currentIds, newIds) ? currentIds : newIds,
tabId,
);
}
export function safeReplacePinnedIds<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
newPinnedIds: number[],
): T {
const currentIds = selectPinnedIds(global, chatId, threadId) || [];
const newIds = orderPinnedIds(newPinnedIds);
return replaceThreadParam(
global,
chatId,
threadId,
'pinnedIds',
areSortedArraysEqual(currentIds, newIds) ? currentIds : newIds,
);
}
export function updateThreadInfo<T extends GlobalState>(
global: T, chatId: string, threadId: ThreadId, update: Partial<ApiThreadInfo> | undefined,
doNotUpdateLinked?: boolean,
): T {
const newThreadInfo = {
...(selectThreadInfo(global, chatId, threadId) as ApiThreadInfo),
...update,
} as ApiThreadInfo;
if (!doNotUpdateLinked && !newThreadInfo.isCommentsInfo) {
const linkedUpdate = pick(newThreadInfo, ['messagesCount', 'lastMessageId', 'lastReadInboxMessageId']);
if (newThreadInfo.fromChannelId && newThreadInfo.fromMessageId) {
global = updateThreadInfo(
global, newThreadInfo.fromChannelId, newThreadInfo.fromMessageId, linkedUpdate, true,
);
}
}
return replaceThreadParam(global, chatId, threadId, 'threadInfo', newThreadInfo);
}
export function updateThreadInfos<T extends GlobalState>(
global: T, updates: Partial<ApiThreadInfo>[],
): T {
updates.forEach((update) => {
global = updateThreadInfo(
global,
update.isCommentsInfo ? update.originChannelId! : update.chatId!,
update.isCommentsInfo ? update.originMessageId! : update.threadId!,
update,
);
});
return global;
}
export function updateScheduledMessages<T extends GlobalState>(
global: T, chatId: string, newById: Record<number, ApiMessage>,
): T {
const current = global.scheduledMessages.byChatId[chatId] || { byId: {}, hash: 0 };
return {
...global,
scheduledMessages: {
byChatId: {
...global.scheduledMessages.byChatId,
[chatId]: {
...current,
byId: {
...current.byId,
...newById,
},
},
},
},
};
}
export function updateQuickReplyMessages<T extends GlobalState>(
global: T, update: Record<number, ApiMessage>,
): T {
return {
...global,
quickReplies: {
...global.quickReplies,
messagesById: {
...global.quickReplies.messagesById,
...update,
},
},
};
}
export function updateFocusedMessage<T extends GlobalState>(
{
global,
chatId,
messageId,
threadId = MAIN_THREAD_ID,
noHighlight = false,
isResizingContainer = false,
quote,
scrollTargetPosition,
}: {
global: T;
chatId?: string;
messageId?: number;
threadId?: ThreadId;
noHighlight?: boolean;
isResizingContainer?: boolean;
quote?: string;
scrollTargetPosition?: ScrollTargetPosition;
},
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
return updateTabState(global, {
focusedMessage: {
...selectTabState(global, tabId).focusedMessage,
chatId,
threadId,
messageId,
noHighlight,
isResizingContainer,
quote,
scrollTargetPosition,
},
}, tabId);
}
export function updateSponsoredMessage<T extends GlobalState>(
global: T, chatId: string, message: ApiSponsoredMessage,
): T {
return {
...global,
messages: {
...global.messages,
sponsoredByChatId: {
...global.messages.sponsoredByChatId,
[chatId]: message,
},
},
};
}
export function deleteSponsoredMessage<T extends GlobalState>(
global: T, chatId: string,
): T {
const byChatId = global.messages.sponsoredByChatId;
if (!byChatId[chatId]) {
return global;
}
return {
...global,
messages: {
...global.messages,
sponsoredByChatId: omit(byChatId, [chatId]),
},
};
}
export function updateFocusDirection<T extends GlobalState>(
global: T, direction?: FocusDirection,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
return updateTabState(global, {
focusedMessage: {
...selectTabState(global, tabId).focusedMessage,
direction,
},
}, tabId);
}
export function enterMessageSelectMode<T extends GlobalState>(
global: T,
chatId: string,
messageId?: number | number[],
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const messageIds = messageId ? Array.prototype.concat([], messageId) : [];
return updateTabState(global, {
selectedMessages: {
chatId,
messageIds,
},
}, tabId);
}
export function toggleMessageSelection<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
messageListType: MessageListType,
messageId: number,
groupedId?: string,
childMessageIds?: number[],
withShift = false,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const { selectedMessages: oldSelectedMessages } = selectTabState(global, tabId);
if (groupedId) {
childMessageIds = selectMessageIdsByGroupId(global, chatId, groupedId);
}
const selectedMessageIds = childMessageIds || [messageId];
if (!oldSelectedMessages) {
return enterMessageSelectMode(global, chatId, selectedMessageIds, tabId);
}
const { messageIds } = oldSelectedMessages;
let newMessageIds;
const newSelectedMessageIds = selectedMessageIds.filter((id) => !messageIds.includes(id));
if (newSelectedMessageIds && !newSelectedMessageIds.length) {
newMessageIds = messageIds.filter((id) => !selectedMessageIds.includes(id));
} else if (withShift && messageIds.length) {
const viewportIds = selectCurrentMessageIds(global, chatId, threadId, messageListType, tabId)!;
const prevIndex = viewportIds.indexOf(messageIds[messageIds.length - 1]);
const currentIndex = viewportIds.indexOf(messageId);
const from = Math.min(prevIndex, currentIndex);
const to = Math.max(prevIndex, currentIndex);
const slice = viewportIds.slice(from, to + 1);
newMessageIds = unique([...messageIds, ...slice]);
} else {
newMessageIds = [...messageIds, ...newSelectedMessageIds];
}
if (!newMessageIds.length) {
return exitMessageSelectMode(global, tabId);
}
return updateTabState(global, {
selectedMessages: {
...oldSelectedMessages,
messageIds: newMessageIds,
},
}, tabId);
}
export function exitMessageSelectMode<T extends GlobalState>(
global: T,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
return updateTabState(global, {
selectedMessages: undefined,
}, tabId);
}
export function updateThreadUnreadFromForwardedMessage<T extends GlobalState>(
global: T,
originMessage: ApiMessage,
chatId: string,
lastMessageId: number,
isDeleting?: boolean,
): T {
const { channelPostId, fromChatId } = originMessage.forwardInfo || {};
if (channelPostId && fromChatId) {
const threadInfoOld = selectThreadInfo(global, chatId, channelPostId);
if (threadInfoOld) {
global = replaceThreadParam(global, chatId, channelPostId, 'threadInfo', {
...threadInfoOld,
lastMessageId,
messagesCount: (threadInfoOld.messagesCount || 0) + (isDeleting ? -1 : 1),
});
}
}
return global;
}
export function addActiveMediaDownload<T extends GlobalState>(
global: T,
mediaHash: string,
metadata: TabState['activeDownloads'][string],
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const tabState = selectTabState(global, tabId);
global = updateTabState(global, {
activeDownloads: {
...tabState.activeDownloads,
[mediaHash]: metadata,
},
}, tabId);
return global;
}
export function cancelMessageMediaDownload<T extends GlobalState>(
global: T,
mediaHashes: string[],
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const tabState = selectTabState(global, tabId);
const newActiveDownloads = omit(tabState.activeDownloads, mediaHashes);
global = updateTabState(global, {
activeDownloads: newActiveDownloads,
}, tabId);
return global;
}
export function updateUploadByMessageKey<T extends GlobalState>(
global: T,
messageKey: MessageKey,
progress: number | undefined,
) {
return {
...global,
fileUploads: {
byMessageKey: progress !== undefined
? {
...global.fileUploads.byMessageKey,
[messageKey]: { progress },
}
: omit(global.fileUploads.byMessageKey, [messageKey]),
},
};
}
export function updateQuickReplies<T extends GlobalState>(
global: T,
quickRepliesUpdate: Record<number, ApiQuickReply>,
) {
return {
...global,
quickReplies: {
...global.quickReplies,
byId: {
...global.quickReplies.byId,
...quickRepliesUpdate,
},
},
};
}
export function deleteQuickReply<T extends GlobalState>(
global: T,
quickReplyId: number,
) {
return {
...global,
quickReplies: {
...global.quickReplies,
byId: omit(global.quickReplies.byId, [quickReplyId]),
},
};
}
export function updatePoll<T extends GlobalState>(
global: T,
pollId: string,
pollUpdate: Partial<ApiPoll>,
) {
const poll = selectPoll(global, pollId);
const oldResults = poll?.results;
let newResults = oldResults || pollUpdate.results;
if (poll && pollUpdate.results?.results) {
if (!poll.results || !pollUpdate.results.isMin) {
newResults = pollUpdate.results;
} else if (oldResults.results) {
// Update voters counts, but keep local `isChosen` values
newResults = {
...pollUpdate.results,
results: pollUpdate.results.results.map((result) => ({
...result,
isChosen: oldResults.results!.find((r) => r.option === result.option)?.isChosen,
})),
isMin: undefined,
};
}
}
const updatedPoll = {
...poll,
...pollUpdate,
results: newResults,
} satisfies ApiPoll;
if (!updatedPoll.id) {
return global;
}
return {
...global,
messages: {
...global.messages,
pollById: {
...global.messages.pollById,
[pollId]: updatedPoll,
},
},
};
}
export function updatePollVote<T extends GlobalState>(
global: T,
pollId: string,
peerId: string,
options: string[],
) {
const poll = selectPoll(global, pollId);
if (!poll) {
return global;
}
const { recentVoterIds, totalVoters, results } = poll.results;
const newRecentVoterIds = recentVoterIds ? [...recentVoterIds] : [];
const newTotalVoters = totalVoters ? totalVoters + 1 : 1;
const newResults = results ? [...results] : [];
newRecentVoterIds.push(peerId);
options.forEach((option) => {
const targetOptionIndex = newResults.findIndex((result) => result.option === option);
const targetOption = newResults[targetOptionIndex];
const updatedOption: ApiPollResult = targetOption ? { ...targetOption } : { option, votersCount: 0 };
updatedOption.votersCount += 1;
if (peerId === global.currentUserId) {
updatedOption.isChosen = true;
}
if (targetOptionIndex) {
newResults[targetOptionIndex] = updatedOption;
} else {
newResults.push(updatedOption);
}
});
return updatePoll(global, pollId, {
results: {
...poll.results,
recentVoterIds: newRecentVoterIds,
totalVoters: newTotalVoters,
results: newResults,
},
});
}