498 lines
13 KiB
TypeScript
498 lines
13 KiB
TypeScript
import { GlobalState, MessageListType, Thread } from '../../global/types';
|
|
import { ApiMessage, ApiThreadInfo, MAIN_THREAD_ID } from '../../api/types';
|
|
import { FocusDirection } from '../../types';
|
|
|
|
import { MESSAGE_LIST_SLICE, MESSAGE_LIST_VIEWPORT_LIMIT } from '../../config';
|
|
import {
|
|
selectListedIds,
|
|
selectChatMessages,
|
|
selectViewportIds,
|
|
selectOutlyingIds,
|
|
selectPinnedIds,
|
|
selectThreadInfo,
|
|
selectMessageIdsByGroupId,
|
|
selectScheduledMessages,
|
|
selectScheduledIds,
|
|
selectCurrentMessageIds,
|
|
selectChatMessage,
|
|
selectCurrentMessageList,
|
|
} from '../selectors';
|
|
import {
|
|
areSortedArraysEqual, omit, pickTruthy, unique,
|
|
} from '../../util/iteratees';
|
|
|
|
type MessageStoreSections = {
|
|
byId: Record<number, ApiMessage>;
|
|
threadsById: Record<number, Thread>;
|
|
};
|
|
|
|
export function updateCurrentMessageList(
|
|
global: GlobalState,
|
|
chatId: number | undefined,
|
|
threadId: number = MAIN_THREAD_ID,
|
|
type: MessageListType = 'thread',
|
|
): GlobalState {
|
|
return {
|
|
...global,
|
|
messages: {
|
|
...global.messages,
|
|
// TODO Support stack navigation
|
|
messageLists: chatId ? [{ chatId, threadId, type }] : undefined,
|
|
},
|
|
};
|
|
}
|
|
|
|
function replaceChatMessages(global: GlobalState, chatId: number, newById: Record<number, ApiMessage>): GlobalState {
|
|
return updateMessageStore(global, chatId, {
|
|
byId: newById,
|
|
});
|
|
}
|
|
|
|
function updateThread(
|
|
global: GlobalState, chatId: number, threadId: number, threadUpdate: Partial<Thread>,
|
|
): GlobalState {
|
|
const current = global.messages.byChatId[chatId];
|
|
|
|
return updateMessageStore(global, chatId, {
|
|
threadsById: {
|
|
...(current && current.threadsById),
|
|
[threadId]: {
|
|
...(current && current.threadsById[threadId]),
|
|
...threadUpdate,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
function updateMessageStore(
|
|
global: GlobalState, chatId: number, update: Partial<MessageStoreSections>,
|
|
): GlobalState {
|
|
const current = global.messages.byChatId[chatId] || { byId: {}, threadsById: {} };
|
|
|
|
return {
|
|
...global,
|
|
messages: {
|
|
...global.messages,
|
|
byChatId: {
|
|
...global.messages.byChatId,
|
|
[chatId]: {
|
|
...current,
|
|
...update,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function replaceThreadParam<T extends keyof Thread>(
|
|
global: GlobalState, chatId: number, threadId: number, paramName: T, newValue: Thread[T] | undefined,
|
|
) {
|
|
return updateThread(global, chatId, threadId, { [paramName]: newValue });
|
|
}
|
|
|
|
export function addMessages(
|
|
global: GlobalState, messages: ApiMessage[],
|
|
): GlobalState {
|
|
const addedByChatId = messages.reduce((messagesByChatId, message: ApiMessage) => {
|
|
if (!messagesByChatId[message.chatId]) {
|
|
messagesByChatId[message.chatId] = {};
|
|
}
|
|
messagesByChatId[message.chatId][message.id] = message;
|
|
|
|
return messagesByChatId;
|
|
}, {} as Record<number, Record<number, ApiMessage>>);
|
|
|
|
Object.keys(addedByChatId).map(Number).forEach((chatId) => {
|
|
global = addChatMessagesById(global, chatId, addedByChatId[chatId]);
|
|
});
|
|
|
|
return global;
|
|
}
|
|
|
|
export function addChatMessagesById(
|
|
global: GlobalState, chatId: number, newById: Record<number, ApiMessage>,
|
|
): GlobalState {
|
|
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(
|
|
global: GlobalState, chatId: number, messageId: number, messageUpdate: Partial<ApiMessage>,
|
|
): GlobalState {
|
|
const byId = selectChatMessages(global, chatId) || {};
|
|
const message = byId[messageId];
|
|
const updatedMessage = {
|
|
...message,
|
|
...messageUpdate,
|
|
};
|
|
|
|
if (!updatedMessage.id) {
|
|
return global;
|
|
}
|
|
|
|
return replaceChatMessages(global, chatId, {
|
|
...byId,
|
|
[messageId]: updatedMessage,
|
|
});
|
|
}
|
|
|
|
export function updateScheduledMessage(
|
|
global: GlobalState, chatId: number, messageId: number, messageUpdate: Partial<ApiMessage>,
|
|
): GlobalState {
|
|
const byId = selectScheduledMessages(global, chatId) || {};
|
|
const message = byId[messageId];
|
|
const updatedMessage = {
|
|
...message,
|
|
...messageUpdate,
|
|
};
|
|
|
|
if (!updatedMessage.id) {
|
|
return global;
|
|
}
|
|
|
|
return replaceScheduledMessages(global, chatId, {
|
|
...byId,
|
|
[messageId]: updatedMessage,
|
|
}, undefined);
|
|
}
|
|
|
|
export function deleteChatMessages(
|
|
global: GlobalState,
|
|
chatId: number,
|
|
messageIds: number[],
|
|
): GlobalState {
|
|
const byId = selectChatMessages(global, chatId);
|
|
if (!byId) {
|
|
return global;
|
|
}
|
|
const newById = omit(byId, messageIds);
|
|
const deletedForwardedPosts = Object.values(pickTruthy(byId, messageIds)).filter(
|
|
({ forwardInfo }) => forwardInfo && forwardInfo.isLinkedChannelPost,
|
|
);
|
|
|
|
const threadIds = Object.keys(global.messages.byChatId[chatId].threadsById).map(Number);
|
|
threadIds.forEach((threadId) => {
|
|
let listedIds = selectListedIds(global, chatId, threadId);
|
|
let outlyingIds = selectOutlyingIds(global, chatId, threadId);
|
|
let viewportIds = selectViewportIds(global, chatId, threadId);
|
|
let pinnedIds = selectPinnedIds(global, chatId);
|
|
|
|
messageIds.forEach((messageId) => {
|
|
if (listedIds && listedIds.includes(messageId)) {
|
|
listedIds = listedIds.filter((id) => id !== messageId);
|
|
}
|
|
|
|
if (outlyingIds && outlyingIds.includes(messageId)) {
|
|
outlyingIds = outlyingIds.filter((id) => id !== messageId);
|
|
}
|
|
|
|
if (viewportIds && viewportIds.includes(messageId)) {
|
|
viewportIds = viewportIds.filter((id) => id !== messageId);
|
|
}
|
|
|
|
if (pinnedIds && pinnedIds.includes(messageId)) {
|
|
pinnedIds = pinnedIds.filter((id) => id !== messageId);
|
|
}
|
|
});
|
|
|
|
global = replaceThreadParam(global, chatId, threadId, 'listedIds', listedIds);
|
|
global = replaceThreadParam(global, chatId, threadId, 'outlyingIds', outlyingIds);
|
|
global = replaceThreadParam(global, chatId, threadId, 'viewportIds', viewportIds);
|
|
global = replaceThreadParam(global, chatId, threadId, 'pinnedIds', pinnedIds);
|
|
});
|
|
|
|
if (deletedForwardedPosts.length) {
|
|
const currentMessageList = selectCurrentMessageList(global);
|
|
const canDeleteCurrentThread = currentMessageList && currentMessageList.chatId === chatId
|
|
&& currentMessageList.type === 'thread';
|
|
const currentThreadId = currentMessageList && currentMessageList.threadId;
|
|
|
|
deletedForwardedPosts.forEach((message) => {
|
|
const { fromChatId, fromMessageId } = message.forwardInfo!;
|
|
const originalPost = selectChatMessage(global, fromChatId!, fromMessageId!);
|
|
|
|
if (canDeleteCurrentThread && currentThreadId === fromMessageId) {
|
|
global = updateCurrentMessageList(global, chatId);
|
|
}
|
|
if (originalPost) {
|
|
global = updateChatMessage(global, fromChatId!, fromMessageId!, { threadInfo: undefined });
|
|
}
|
|
});
|
|
}
|
|
|
|
global = replaceChatMessages(global, chatId, newById);
|
|
|
|
return global;
|
|
}
|
|
|
|
export function deleteChatScheduledMessages(
|
|
global: GlobalState,
|
|
chatId: number,
|
|
messageIds: number[],
|
|
): GlobalState {
|
|
const byId = selectScheduledMessages(global, chatId);
|
|
if (!byId) {
|
|
return global;
|
|
}
|
|
const newById = omit(byId, messageIds);
|
|
|
|
let scheduledIds = selectScheduledIds(global, chatId);
|
|
if (scheduledIds) {
|
|
messageIds.forEach((messageId) => {
|
|
if (scheduledIds!.includes(messageId)) {
|
|
scheduledIds = scheduledIds!.filter((id) => id !== messageId);
|
|
}
|
|
});
|
|
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'scheduledIds', scheduledIds);
|
|
}
|
|
|
|
global = replaceScheduledMessages(global, chatId, newById, undefined);
|
|
|
|
return global;
|
|
}
|
|
|
|
export function updateListedIds(
|
|
global: GlobalState,
|
|
chatId: number,
|
|
threadId: number,
|
|
idsUpdate: number[],
|
|
): GlobalState {
|
|
const listedIds = selectListedIds(global, chatId, threadId);
|
|
const newIds = listedIds && 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 updateOutlyingIds(
|
|
global: GlobalState,
|
|
chatId: number,
|
|
threadId: number,
|
|
idsUpdate: number[],
|
|
): GlobalState {
|
|
const outlyingIds = selectOutlyingIds(global, chatId, threadId);
|
|
const newIds = outlyingIds && outlyingIds.length
|
|
? idsUpdate.filter((id) => !outlyingIds.includes(id))
|
|
: idsUpdate;
|
|
|
|
if (outlyingIds && !newIds.length) {
|
|
return global;
|
|
}
|
|
|
|
return replaceThreadParam(global, chatId, threadId, 'outlyingIds', orderHistoryIds([
|
|
...(outlyingIds || []),
|
|
...newIds,
|
|
]));
|
|
}
|
|
|
|
function orderHistoryIds(listedIds: number[]) {
|
|
return listedIds.sort((a, b) => a - b);
|
|
}
|
|
|
|
export function addViewportId(
|
|
global: GlobalState,
|
|
chatId: number,
|
|
threadId: number,
|
|
newId: number,
|
|
): GlobalState {
|
|
const viewportIds = selectViewportIds(global, chatId, threadId) || [];
|
|
if (viewportIds.includes(newId)) {
|
|
return global;
|
|
}
|
|
|
|
const newIds = orderHistoryIds([
|
|
...(
|
|
viewportIds.length < MESSAGE_LIST_VIEWPORT_LIMIT
|
|
? viewportIds
|
|
: viewportIds.slice(-(MESSAGE_LIST_SLICE / 2))
|
|
),
|
|
newId,
|
|
]);
|
|
|
|
return replaceThreadParam(global, chatId, threadId, 'viewportIds', newIds);
|
|
}
|
|
|
|
export function safeReplaceViewportIds(
|
|
global: GlobalState,
|
|
chatId: number,
|
|
threadId: number,
|
|
newViewportIds: number[],
|
|
): GlobalState {
|
|
const viewportIds = selectViewportIds(global, chatId, threadId) || [];
|
|
|
|
return replaceThreadParam(
|
|
global,
|
|
chatId,
|
|
threadId,
|
|
'viewportIds',
|
|
areSortedArraysEqual(viewportIds, newViewportIds) ? viewportIds : newViewportIds,
|
|
);
|
|
}
|
|
|
|
export function updateThreadInfo(
|
|
global: GlobalState, chatId: number, threadId: number, update: Partial<ApiThreadInfo> | undefined,
|
|
): GlobalState {
|
|
const newThreadInfo = {
|
|
...(selectThreadInfo(global, chatId, threadId) as ApiThreadInfo),
|
|
...update,
|
|
};
|
|
|
|
if (!newThreadInfo.threadId) {
|
|
return global;
|
|
}
|
|
|
|
return replaceThreadParam(global, chatId, threadId, 'threadInfo', newThreadInfo);
|
|
}
|
|
|
|
export function updateThreadInfos(
|
|
global: GlobalState, chatId: number, updates: Partial<ApiThreadInfo>[],
|
|
): GlobalState {
|
|
updates.forEach((update) => {
|
|
global = updateThreadInfo(global, update.chatId!, update.threadId!, update);
|
|
});
|
|
|
|
return global;
|
|
}
|
|
|
|
export function replaceScheduledMessages(
|
|
global: GlobalState, chatId: number, newById: Record<number, ApiMessage>, hash: number | undefined,
|
|
): GlobalState {
|
|
return updateScheduledMessages(global, chatId, {
|
|
byId: newById,
|
|
hash,
|
|
});
|
|
}
|
|
|
|
function updateScheduledMessages(
|
|
global: GlobalState, chatId: number, update: Partial<{ byId: Record<number, ApiMessage>; hash: number }>,
|
|
): GlobalState {
|
|
const current = global.scheduledMessages.byChatId[chatId] || { byId: {}, hash: 0 };
|
|
|
|
return {
|
|
...global,
|
|
scheduledMessages: {
|
|
byChatId: {
|
|
...global.scheduledMessages.byChatId,
|
|
[chatId]: {
|
|
...current,
|
|
...update,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function updateFocusedMessage(
|
|
global: GlobalState, chatId?: number, messageId?: number, noHighlight = false,
|
|
): GlobalState {
|
|
return {
|
|
...global,
|
|
focusedMessage: {
|
|
...global.focusedMessage,
|
|
chatId,
|
|
messageId,
|
|
noHighlight,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function updateFocusDirection(
|
|
global: GlobalState, direction?: FocusDirection,
|
|
): GlobalState {
|
|
return {
|
|
...global,
|
|
focusedMessage: {
|
|
...global.focusedMessage,
|
|
direction,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function enterMessageSelectMode(
|
|
global: GlobalState,
|
|
chatId: number,
|
|
messageId?: number | number[],
|
|
): GlobalState {
|
|
const messageIds = messageId ? Array.prototype.concat([], messageId) : [];
|
|
return {
|
|
...global,
|
|
selectedMessages: {
|
|
chatId,
|
|
messageIds,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function toggleMessageSelection(
|
|
global: GlobalState,
|
|
chatId: number,
|
|
threadId: number,
|
|
messageListType: MessageListType,
|
|
messageId: number,
|
|
groupedId?: string,
|
|
childMessageIds?: number[],
|
|
withShift = false,
|
|
): GlobalState {
|
|
const { selectedMessages: oldSelectedMessages } = global;
|
|
if (groupedId) {
|
|
childMessageIds = selectMessageIdsByGroupId(global, chatId, groupedId);
|
|
}
|
|
const selectedMessageIds = childMessageIds || [messageId];
|
|
if (!oldSelectedMessages) {
|
|
return enterMessageSelectMode(global, chatId, selectedMessageIds);
|
|
}
|
|
|
|
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)!;
|
|
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);
|
|
}
|
|
|
|
return {
|
|
...global,
|
|
selectedMessages: {
|
|
...oldSelectedMessages,
|
|
messageIds: newMessageIds,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function exitMessageSelectMode(global: GlobalState): GlobalState {
|
|
return {
|
|
...global,
|
|
selectedMessages: undefined,
|
|
};
|
|
}
|