839 lines
25 KiB
TypeScript
839 lines
25 KiB
TypeScript
import type { ApiMessage } from '../../../api/types';
|
|
import type {
|
|
ActionReturnType,
|
|
GlobalState,
|
|
} from '../../types';
|
|
import { MAIN_THREAD_ID } from '../../../api/types';
|
|
import { FocusDirection } from '../../../types';
|
|
|
|
import {
|
|
ANIMATION_END_DELAY,
|
|
FAST_SMOOTH_MAX_DURATION,
|
|
RELEASE_DATETIME,
|
|
SERVICE_NOTIFICATIONS_USER_ID,
|
|
} from '../../../config';
|
|
import { copyHtmlToClipboard } from '../../../util/clipboard';
|
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
|
import { compact, findLast } from '../../../util/iteratees';
|
|
import * as langProvider from '../../../util/langProvider';
|
|
import parseMessageInput from '../../../util/parseMessageInput';
|
|
import { getServerTime } from '../../../util/serverTime';
|
|
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
|
import versionNotification from '../../../versionNotification.txt';
|
|
import { getMessageSummaryText, getSenderTitle, isChatChannel } from '../../helpers';
|
|
import { renderMessageSummaryHtml } from '../../helpers/renderMessageSummaryHtml';
|
|
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
|
import {
|
|
addActiveMessageMediaDownload,
|
|
cancelMessageMediaDownload,
|
|
enterMessageSelectMode,
|
|
exitMessageSelectMode,
|
|
replaceTabThreadParam,
|
|
replaceThreadParam,
|
|
toggleMessageSelection,
|
|
updateFocusDirection,
|
|
updateFocusedMessage,
|
|
} from '../../reducers';
|
|
import { updateTabState } from '../../reducers/tabs';
|
|
import {
|
|
selectAllowedMessageActions,
|
|
selectChat,
|
|
selectChatMessages,
|
|
selectChatScheduledMessages,
|
|
selectCurrentChat,
|
|
selectCurrentMessageList,
|
|
selectDraft,
|
|
selectForwardedMessageIdsByGroupId,
|
|
selectIsRightColumnShown,
|
|
selectIsViewportNewest,
|
|
selectMessageIdsByGroupId,
|
|
selectPinnedIds,
|
|
selectReplyStack,
|
|
selectRequestedChatTranslationLanguage,
|
|
selectRequestedMessageTranslationLanguage,
|
|
selectSender,
|
|
selectTabState,
|
|
selectThreadInfo,
|
|
selectViewportIds,
|
|
} from '../../selectors';
|
|
|
|
import { getIsMobile } from '../../../hooks/useAppLayout';
|
|
|
|
const FOCUS_DURATION = 1500;
|
|
const FOCUS_NO_HIGHLIGHT_DURATION = FAST_SMOOTH_MAX_DURATION + ANIMATION_END_DELAY;
|
|
const POLL_RESULT_OPEN_DELAY_MS = 450;
|
|
const VERSION_NOTIFICATION_DURATION = 1000 * 60 * 60 * 24 * 3; // 3 days
|
|
const SERVICE_NOTIFICATIONS_MAX_AMOUNT = 1e3;
|
|
|
|
let blurTimeout: number | undefined;
|
|
|
|
addActionHandler('setScrollOffset', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
chatId, threadId, scrollOffset, tabId = getCurrentTabId(),
|
|
} = payload;
|
|
|
|
global = replaceThreadParam(global, chatId, threadId, 'lastScrollOffset', scrollOffset);
|
|
|
|
return replaceTabThreadParam(global, chatId, threadId, 'scrollOffset', scrollOffset, tabId);
|
|
});
|
|
|
|
addActionHandler('setEditingId', (global, actions, payload): ActionReturnType => {
|
|
const { messageId, tabId = getCurrentTabId() } = payload;
|
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
|
if (!currentMessageList) {
|
|
return undefined;
|
|
}
|
|
|
|
const { chatId, threadId, type } = currentMessageList;
|
|
const paramName = type === 'scheduled' ? 'editingScheduledId' : 'editingId';
|
|
|
|
return replaceThreadParam(global, chatId, threadId, paramName, messageId);
|
|
});
|
|
|
|
addActionHandler('setEditingDraft', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
text, chatId, threadId, type,
|
|
} = payload;
|
|
|
|
const paramName = type === 'scheduled' ? 'editingScheduledDraft' : 'editingDraft';
|
|
|
|
return replaceThreadParam(global, chatId, threadId, paramName, text);
|
|
});
|
|
|
|
addActionHandler('editLastMessage', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
|
if (!chatId || !threadId) {
|
|
return undefined;
|
|
}
|
|
|
|
const chatMessages = selectChatMessages(global, chatId);
|
|
const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
|
|
if (!chatMessages || !viewportIds) {
|
|
return undefined;
|
|
}
|
|
|
|
const lastOwnEditableMessageId = findLast(viewportIds, (id) => {
|
|
return Boolean(chatMessages[id] && selectAllowedMessageActions(global, chatMessages[id], threadId).canEdit);
|
|
});
|
|
|
|
if (!lastOwnEditableMessageId) {
|
|
return undefined;
|
|
}
|
|
|
|
return replaceThreadParam(global, chatId, threadId, 'editingId', lastOwnEditableMessageId);
|
|
});
|
|
|
|
addActionHandler('replyToNextMessage', (global, actions, payload): ActionReturnType => {
|
|
const { targetIndexDelta, tabId = getCurrentTabId() } = payload;
|
|
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
|
if (!chatId || !threadId) {
|
|
return;
|
|
}
|
|
|
|
const chatMessages = selectChatMessages(global, chatId);
|
|
const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
|
|
if (!chatMessages || !viewportIds) {
|
|
return;
|
|
}
|
|
|
|
const replyInfo = selectDraft(global, chatId, threadId)?.replyInfo;
|
|
const isLatest = selectIsViewportNewest(global, chatId, threadId, tabId);
|
|
|
|
let messageId: number | undefined;
|
|
|
|
if (!isLatest || !replyInfo) {
|
|
if (threadId === MAIN_THREAD_ID) {
|
|
const chat = selectChat(global, chatId);
|
|
|
|
messageId = chat?.lastMessage?.id;
|
|
} else {
|
|
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
|
|
|
messageId = threadInfo?.lastMessageId;
|
|
}
|
|
} else {
|
|
const chatMessageKeys = Object.keys(chatMessages);
|
|
const indexOfCurrent = chatMessageKeys.indexOf(replyInfo.toString());
|
|
const newIndex = indexOfCurrent + targetIndexDelta;
|
|
messageId = newIndex <= chatMessageKeys.length + 1 && newIndex >= 0
|
|
? Number(chatMessageKeys[newIndex])
|
|
: undefined;
|
|
}
|
|
actions.updateDraftReplyInfo({ replyToMsgId: messageId, tabId });
|
|
actions.focusMessage({
|
|
chatId,
|
|
threadId,
|
|
messageId: messageId!,
|
|
tabId,
|
|
});
|
|
});
|
|
|
|
addActionHandler('openAudioPlayer', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
chatId, threadId, messageId, origin, volume, playbackRate, isMuted,
|
|
tabId = getCurrentTabId(),
|
|
} = payload;
|
|
|
|
const tabState = selectTabState(global, tabId);
|
|
return updateTabState(global, {
|
|
audioPlayer: {
|
|
chatId,
|
|
threadId,
|
|
messageId,
|
|
origin: origin ?? tabState.audioPlayer.origin,
|
|
volume: volume ?? tabState.audioPlayer.volume,
|
|
playbackRate: playbackRate || tabState.audioPlayer.playbackRate || global.audioPlayer.lastPlaybackRate,
|
|
isPlaybackRateActive: (tabState.audioPlayer.isPlaybackRateActive === undefined
|
|
? global.audioPlayer.isLastPlaybackRateActive
|
|
: tabState.audioPlayer.isPlaybackRateActive),
|
|
isMuted: isMuted || tabState.audioPlayer.isMuted,
|
|
},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('setAudioPlayerVolume', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
volume, tabId = getCurrentTabId(),
|
|
} = payload;
|
|
|
|
return updateTabState(global, {
|
|
audioPlayer: {
|
|
...selectTabState(global, tabId).audioPlayer,
|
|
volume,
|
|
isMuted: false,
|
|
},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('setAudioPlayerPlaybackRate', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
playbackRate, isPlaybackRateActive, tabId = getCurrentTabId(),
|
|
} = payload;
|
|
|
|
global = {
|
|
...global,
|
|
audioPlayer: {
|
|
...global.audioPlayer,
|
|
lastPlaybackRate: playbackRate,
|
|
isLastPlaybackRateActive: isPlaybackRateActive,
|
|
},
|
|
};
|
|
|
|
return updateTabState(global, {
|
|
audioPlayer: {
|
|
...selectTabState(global, tabId).audioPlayer,
|
|
playbackRate,
|
|
isPlaybackRateActive,
|
|
},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('setAudioPlayerMuted', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
isMuted, tabId = getCurrentTabId(),
|
|
} = payload;
|
|
|
|
return updateTabState(global, {
|
|
audioPlayer: {
|
|
...selectTabState(global, tabId).audioPlayer,
|
|
isMuted,
|
|
},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('setAudioPlayerOrigin', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
origin, tabId = getCurrentTabId(),
|
|
} = payload;
|
|
|
|
return updateTabState(global, {
|
|
audioPlayer: {
|
|
...selectTabState(global, tabId).audioPlayer,
|
|
origin,
|
|
},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('closeAudioPlayer', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
const tabState = selectTabState(global, tabId);
|
|
return updateTabState(global, {
|
|
audioPlayer: {
|
|
volume: tabState.audioPlayer.volume,
|
|
playbackRate: tabState.audioPlayer.playbackRate,
|
|
isPlaybackRateActive: tabState.audioPlayer.isPlaybackRateActive,
|
|
isMuted: tabState.audioPlayer.isMuted,
|
|
},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('openPollResults', (global, actions, payload): ActionReturnType => {
|
|
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
|
|
|
|
const shouldOpenInstantly = selectIsRightColumnShown(global, getIsMobile(), tabId);
|
|
const tabState = selectTabState(global, tabId);
|
|
|
|
if (!shouldOpenInstantly) {
|
|
window.setTimeout(() => {
|
|
global = getGlobal();
|
|
|
|
global = updateTabState(global, {
|
|
pollResults: {
|
|
chatId,
|
|
messageId,
|
|
voters: {},
|
|
},
|
|
}, tabId);
|
|
setGlobal(global);
|
|
}, POLL_RESULT_OPEN_DELAY_MS);
|
|
} else if (chatId !== tabState.pollResults.chatId || messageId !== tabState.pollResults.messageId) {
|
|
return updateTabState(global, {
|
|
pollResults: {
|
|
chatId,
|
|
messageId,
|
|
voters: {},
|
|
},
|
|
}, tabId);
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
|
|
addActionHandler('closePollResults', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
return updateTabState(global, {
|
|
pollResults: {},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('focusLastMessage', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
|
if (!currentMessageList) {
|
|
return;
|
|
}
|
|
|
|
const { chatId, threadId, type } = currentMessageList;
|
|
|
|
let lastMessageId: number | undefined;
|
|
if (threadId === MAIN_THREAD_ID) {
|
|
if (type === 'pinned') {
|
|
const pinnedMessageIds = selectPinnedIds(global, chatId, MAIN_THREAD_ID);
|
|
if (!pinnedMessageIds?.length) {
|
|
return;
|
|
}
|
|
|
|
lastMessageId = pinnedMessageIds[pinnedMessageIds.length - 1];
|
|
} else {
|
|
const chat = selectChat(global, chatId);
|
|
|
|
lastMessageId = chat?.lastMessage?.id;
|
|
}
|
|
} else {
|
|
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
|
|
|
lastMessageId = threadInfo?.lastMessageId;
|
|
}
|
|
|
|
if (!lastMessageId) {
|
|
return;
|
|
}
|
|
|
|
actions.focusMessage({
|
|
chatId,
|
|
threadId,
|
|
messageListType: type,
|
|
messageId: lastMessageId,
|
|
noHighlight: true,
|
|
noForumTopicPanel: true,
|
|
tabId,
|
|
});
|
|
});
|
|
|
|
addActionHandler('focusNextReply', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
|
if (!currentMessageList) {
|
|
return undefined;
|
|
}
|
|
|
|
const { chatId, threadId } = currentMessageList;
|
|
|
|
const replyStack = selectReplyStack(global, chatId, threadId, tabId);
|
|
|
|
if (!replyStack || replyStack.length === 0) {
|
|
actions.focusLastMessage({ tabId });
|
|
} else {
|
|
const messageId = replyStack.pop();
|
|
|
|
global = replaceTabThreadParam(global, chatId, threadId, 'replyStack', [...replyStack], tabId);
|
|
|
|
setGlobal(global);
|
|
|
|
actions.focusMessage({
|
|
chatId,
|
|
threadId,
|
|
messageId: messageId!,
|
|
tabId,
|
|
noForumTopicPanel: true,
|
|
});
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
|
|
addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId,
|
|
replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel,
|
|
tabId = getCurrentTabId(),
|
|
} = payload;
|
|
|
|
let { messageId } = payload;
|
|
|
|
if (groupedId !== undefined) {
|
|
const ids = selectForwardedMessageIdsByGroupId(global, groupedChatId!, groupedId);
|
|
if (ids?.length) {
|
|
([messageId] = compact(ids));
|
|
}
|
|
}
|
|
|
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
|
const shouldSwitchChat = !currentMessageList || (
|
|
chatId !== currentMessageList.chatId
|
|
|| threadId !== currentMessageList.threadId
|
|
|| messageListType !== currentMessageList.type
|
|
);
|
|
|
|
if (blurTimeout) {
|
|
clearTimeout(blurTimeout);
|
|
blurTimeout = undefined;
|
|
}
|
|
blurTimeout = window.setTimeout(() => {
|
|
global = getGlobal();
|
|
global = updateFocusedMessage(global, undefined, undefined, undefined, undefined, undefined, tabId);
|
|
global = updateFocusDirection(global, undefined, tabId);
|
|
setGlobal(global);
|
|
}, noHighlight ? FOCUS_NO_HIGHLIGHT_DURATION : FOCUS_DURATION);
|
|
|
|
global = updateFocusedMessage(global, chatId, messageId, threadId, noHighlight, isResizingContainer, tabId);
|
|
global = updateFocusDirection(global, undefined, tabId);
|
|
|
|
if (replyMessageId) {
|
|
const replyStack = selectReplyStack(global, chatId, threadId, tabId) || [];
|
|
global = replaceTabThreadParam(global, chatId, threadId, 'replyStack', [...replyStack, replyMessageId], tabId);
|
|
}
|
|
|
|
if (shouldSwitchChat) {
|
|
global = updateFocusDirection(global, FocusDirection.Static, tabId);
|
|
}
|
|
|
|
const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
|
|
if (viewportIds && viewportIds.includes(messageId)) {
|
|
setGlobal(global, { forceOnHeavyAnimation: true });
|
|
actions.openChat({
|
|
id: chatId,
|
|
threadId,
|
|
type: messageListType,
|
|
shouldReplaceHistory,
|
|
noForumTopicPanel,
|
|
tabId,
|
|
});
|
|
return undefined;
|
|
}
|
|
|
|
if (shouldSwitchChat) {
|
|
global = replaceTabThreadParam(global, chatId, threadId, 'viewportIds', undefined, tabId);
|
|
}
|
|
|
|
if (viewportIds && !shouldSwitchChat) {
|
|
const direction = messageId > viewportIds[0] ? FocusDirection.Down : FocusDirection.Up;
|
|
global = updateFocusDirection(global, direction, tabId);
|
|
}
|
|
|
|
setGlobal(global, { forceOnHeavyAnimation: true });
|
|
|
|
actions.openChat({
|
|
id: chatId,
|
|
threadId,
|
|
type: messageListType,
|
|
shouldReplaceHistory,
|
|
noForumTopicPanel,
|
|
tabId,
|
|
});
|
|
actions.loadViewportMessages({
|
|
tabId,
|
|
shouldForceRender: true,
|
|
});
|
|
return undefined;
|
|
});
|
|
|
|
addActionHandler('openForwardMenu', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
fromChatId, messageIds, storyId, groupedId, withMyScore, tabId = getCurrentTabId(),
|
|
} = payload;
|
|
let groupedMessageIds;
|
|
if (groupedId) {
|
|
groupedMessageIds = selectMessageIdsByGroupId(global, fromChatId, groupedId);
|
|
}
|
|
return updateTabState(global, {
|
|
forwardMessages: {
|
|
fromChatId,
|
|
messageIds: groupedMessageIds || messageIds,
|
|
storyId,
|
|
isModalShown: true,
|
|
withMyScore,
|
|
},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('changeForwardRecipient', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
return updateTabState(global, {
|
|
forwardMessages: {
|
|
...selectTabState(global, tabId).forwardMessages,
|
|
toChatId: undefined,
|
|
isModalShown: true,
|
|
noAuthors: false,
|
|
noCaptions: false,
|
|
},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('setForwardNoAuthors', (global, actions, payload): ActionReturnType => {
|
|
const { noAuthors, tabId = getCurrentTabId() } = payload;
|
|
const tabState = selectTabState(global, tabId);
|
|
return updateTabState(global, {
|
|
forwardMessages: {
|
|
...tabState.forwardMessages,
|
|
noAuthors,
|
|
// `noCaptions` cannot be true when `noAuthors` is false
|
|
noCaptions: noAuthors && tabState.forwardMessages.noCaptions,
|
|
},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('setForwardNoCaptions', (global, actions, payload): ActionReturnType => {
|
|
const { noCaptions, tabId = getCurrentTabId() } = payload;
|
|
return updateTabState(global, {
|
|
forwardMessages: {
|
|
...selectTabState(global, tabId).forwardMessages,
|
|
noCaptions,
|
|
noAuthors: noCaptions, // On other clients `noAuthors` updates together with `noCaptions`
|
|
},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('exitForwardMode', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
|
|
global = updateTabState(global, {
|
|
forwardMessages: {},
|
|
}, tabId);
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('openForwardMenuForSelectedMessages', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
const tabState = selectTabState(global, tabId);
|
|
if (!tabState.selectedMessages) {
|
|
return;
|
|
}
|
|
|
|
const { chatId: fromChatId, messageIds } = tabState.selectedMessages;
|
|
|
|
actions.openForwardMenu({ fromChatId, messageIds, tabId });
|
|
});
|
|
|
|
addActionHandler('cancelMessageMediaDownload', (global, actions, payload): ActionReturnType => {
|
|
const { message, tabId = getCurrentTabId() } = payload;
|
|
|
|
return cancelMessageMediaDownload(global, message, tabId);
|
|
});
|
|
|
|
addActionHandler('cancelMessagesMediaDownload', (global, actions, payload): ActionReturnType => {
|
|
const { messages, tabId = getCurrentTabId() } = payload;
|
|
|
|
for (const message of messages) {
|
|
global = cancelMessageMediaDownload(global, message, tabId);
|
|
}
|
|
|
|
return global;
|
|
});
|
|
|
|
addActionHandler('downloadMessageMedia', (global, actions, payload): ActionReturnType => {
|
|
const { message, tabId = getCurrentTabId() } = payload;
|
|
|
|
return addActiveMessageMediaDownload(global, message, tabId);
|
|
});
|
|
|
|
addActionHandler('downloadSelectedMessages', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
const tabState = selectTabState(global, tabId);
|
|
if (!tabState.selectedMessages) {
|
|
return;
|
|
}
|
|
|
|
const { chatId, messageIds } = tabState.selectedMessages;
|
|
const { threadId } = selectCurrentMessageList(global, tabId) || {};
|
|
|
|
const chatMessages = selectChatMessages(global, chatId);
|
|
if (!chatMessages || !threadId) return;
|
|
const messages = messageIds.map((id) => chatMessages[id])
|
|
.filter((message) => selectAllowedMessageActions(global, message, threadId).canDownload);
|
|
messages.forEach((message) => actions.downloadMessageMedia({ message, tabId }));
|
|
});
|
|
|
|
addActionHandler('enterMessageSelectMode', (global, actions, payload): ActionReturnType => {
|
|
const { messageId, tabId = getCurrentTabId() } = payload || {};
|
|
const openChat = selectCurrentChat(global, tabId);
|
|
if (!openChat) {
|
|
return global;
|
|
}
|
|
|
|
return enterMessageSelectMode(global, openChat.id, messageId, tabId);
|
|
});
|
|
|
|
addActionHandler('toggleMessageSelection', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
messageId,
|
|
groupedId,
|
|
childMessageIds,
|
|
withShift,
|
|
tabId = getCurrentTabId(),
|
|
} = payload;
|
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
|
if (!currentMessageList) {
|
|
return;
|
|
}
|
|
|
|
const { chatId, threadId, type: messageListType } = currentMessageList;
|
|
|
|
global = toggleMessageSelection(
|
|
global, chatId, threadId, messageListType, messageId, groupedId, childMessageIds, withShift, tabId,
|
|
);
|
|
|
|
setGlobal(global);
|
|
|
|
if (global.shouldShowContextMenuHint) {
|
|
actions.disableContextMenuHint();
|
|
actions.showNotification({
|
|
// eslint-disable-next-line max-len
|
|
message: `To **edit** or **reply**, close this menu. Then ${IS_TOUCH_ENV ? 'long tap' : 'right click'} on a message.`,
|
|
tabId,
|
|
});
|
|
}
|
|
});
|
|
|
|
addActionHandler('disableContextMenuHint', (global): ActionReturnType => {
|
|
if (!global.shouldShowContextMenuHint) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
...global,
|
|
shouldShowContextMenuHint: false,
|
|
};
|
|
});
|
|
|
|
addActionHandler('exitMessageSelectMode', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
return exitMessageSelectMode(global, tabId);
|
|
});
|
|
|
|
addActionHandler('openPollModal', (global, actions, payload): ActionReturnType => {
|
|
const { isQuiz, tabId = getCurrentTabId() } = payload || {};
|
|
|
|
return updateTabState(global, {
|
|
pollModal: {
|
|
isOpen: true,
|
|
isQuiz,
|
|
},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('closePollModal', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
|
|
return updateTabState(global, {
|
|
pollModal: {
|
|
isOpen: false,
|
|
},
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('checkVersionNotification', (global, actions): ActionReturnType => {
|
|
if (RELEASE_DATETIME && Date.now() > Number(RELEASE_DATETIME) + VERSION_NOTIFICATION_DURATION) {
|
|
return;
|
|
}
|
|
|
|
const currentVersion = APP_VERSION.split('.').slice(0, 2).join('.');
|
|
const { serviceNotifications } = global;
|
|
|
|
if (serviceNotifications.find(({ version }) => version === currentVersion)) {
|
|
return;
|
|
}
|
|
|
|
const message: Omit<ApiMessage, 'id'> = {
|
|
chatId: SERVICE_NOTIFICATIONS_USER_ID,
|
|
date: getServerTime(),
|
|
content: {
|
|
text: parseMessageInput(versionNotification, true),
|
|
},
|
|
isOutgoing: false,
|
|
};
|
|
|
|
actions.createServiceNotification({
|
|
message: message as ApiMessage,
|
|
version: currentVersion,
|
|
});
|
|
});
|
|
|
|
addActionHandler('createServiceNotification', (global, actions, payload): ActionReturnType => {
|
|
const { message, version } = payload;
|
|
const { serviceNotifications } = global;
|
|
const serviceChat = selectChat(global, SERVICE_NOTIFICATIONS_USER_ID)!;
|
|
|
|
const maxId = Math.max(
|
|
serviceChat.lastMessage?.id || 0,
|
|
...serviceNotifications.map(({ id }) => id),
|
|
);
|
|
const fractionalPart = (serviceNotifications.length + 1) / SERVICE_NOTIFICATIONS_MAX_AMOUNT;
|
|
// The fractional ID is made of the largest integer ID and an incremented fractional part
|
|
const id = Math.floor(maxId) + fractionalPart;
|
|
|
|
message.previousLocalId = message.id;
|
|
message.id = id;
|
|
|
|
const serviceNotification = {
|
|
id,
|
|
message,
|
|
version,
|
|
isUnread: true,
|
|
};
|
|
|
|
global = {
|
|
...global,
|
|
serviceNotifications: [
|
|
...serviceNotifications.slice(-SERVICE_NOTIFICATIONS_MAX_AMOUNT),
|
|
serviceNotification,
|
|
],
|
|
};
|
|
setGlobal(global);
|
|
|
|
actions.apiUpdate({
|
|
'@type': 'newMessage',
|
|
id: message.id,
|
|
chatId: message.chatId,
|
|
message,
|
|
});
|
|
});
|
|
|
|
addActionHandler('openReactorListModal', (global, actions, payload): ActionReturnType => {
|
|
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
|
|
|
|
return updateTabState(global, {
|
|
reactorModal: { chatId, messageId },
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('closeReactorListModal', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
|
|
return updateTabState(global, {
|
|
reactorModal: undefined,
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('openSeenByModal', (global, actions, payload): ActionReturnType => {
|
|
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
|
|
|
|
return updateTabState(global, {
|
|
seenByModal: { chatId, messageId },
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('closeSeenByModal', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
|
|
return updateTabState(global, {
|
|
seenByModal: undefined,
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('openChatLanguageModal', (global, actions, payload): ActionReturnType => {
|
|
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
|
|
|
|
const activeLanguage = messageId
|
|
? selectRequestedMessageTranslationLanguage(global, chatId, messageId, tabId)
|
|
: selectRequestedChatTranslationLanguage(global, chatId, tabId);
|
|
|
|
return updateTabState(global, {
|
|
chatLanguageModal: { chatId, messageId, activeLanguage },
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('closeChatLanguageModal', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
|
|
return updateTabState(global, {
|
|
chatLanguageModal: undefined,
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('copySelectedMessages', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
const tabState = selectTabState(global, tabId);
|
|
if (!tabState.selectedMessages) {
|
|
return;
|
|
}
|
|
|
|
const { chatId, messageIds } = tabState.selectedMessages;
|
|
copyTextForMessages(global, chatId, messageIds);
|
|
});
|
|
|
|
addActionHandler('copyMessagesByIds', (global, actions, payload): ActionReturnType => {
|
|
const { messageIds, tabId = getCurrentTabId() } = payload;
|
|
const chat = selectCurrentChat(global, tabId);
|
|
if (!messageIds || messageIds.length === 0 || !chat) {
|
|
return;
|
|
}
|
|
|
|
copyTextForMessages(global, chat.id, messageIds);
|
|
});
|
|
|
|
function copyTextForMessages(global: GlobalState, chatId: string, messageIds: number[]) {
|
|
const { type: messageListType, threadId } = selectCurrentMessageList(global) || {};
|
|
const lang = langProvider.translate;
|
|
|
|
const chat = selectChat(global, chatId);
|
|
|
|
const chatMessages = messageListType === 'scheduled'
|
|
? selectChatScheduledMessages(global, chatId)
|
|
: selectChatMessages(global, chatId);
|
|
|
|
if (!chat || !chatMessages || !threadId) return;
|
|
|
|
const messages = messageIds
|
|
.map((id) => chatMessages[id])
|
|
.filter((message) => selectAllowedMessageActions(global, message, threadId).canCopy)
|
|
.sort((message1, message2) => message1.id - message2.id);
|
|
|
|
const resultHtml: string[] = [];
|
|
const resultText: string[] = [];
|
|
|
|
messages.forEach((message) => {
|
|
const sender = isChatChannel(chat) ? chat : selectSender(global, message);
|
|
const senderTitle = `> ${sender ? getSenderTitle(lang, sender) : message.forwardInfo?.hiddenUserName || ''}:`;
|
|
|
|
resultHtml.push(senderTitle);
|
|
resultHtml.push(`${renderMessageSummaryHtml(lang, message)}\n`);
|
|
|
|
resultText.push(senderTitle);
|
|
resultText.push(`${getMessageSummaryText(lang, message, false, 0, true)}\n`);
|
|
});
|
|
|
|
copyHtmlToClipboard(resultHtml.join('\n'), resultText.join('\n'));
|
|
}
|