diff --git a/src/components/common/MessageOutgoingStatus.scss b/src/components/common/MessageOutgoingStatus.scss index 9f5b9e38c..86ba4847d 100644 --- a/src/components/common/MessageOutgoingStatus.scss +++ b/src/components/common/MessageOutgoingStatus.scss @@ -1,5 +1,4 @@ .MessageOutgoingStatus { - position: relative; width: 1.19rem; height: 1.19rem; overflow: hidden; diff --git a/src/components/left/main/Badge.scss b/src/components/left/main/Badge.scss index 5777b9578..ac702edb8 100644 --- a/src/components/left/main/Badge.scss +++ b/src/components/left/main/Badge.scss @@ -1,5 +1,4 @@ .Badge-transition { - transform: scale(1); opacity: 1; transition: transform .3s cubic-bezier(0.34, 1.56, 0.64, 1); diff --git a/src/components/left/main/Chat.scss b/src/components/left/main/Chat.scss index 72efa7be9..15c439de5 100644 --- a/src/components/left/main/Chat.scss +++ b/src/components/left/main/Chat.scss @@ -53,7 +53,6 @@ } .status { - position: relative; flex-shrink: 0; } @@ -106,10 +105,6 @@ margin-inline-end: .25rem; } - .media-preview { - position: relative; - } - img { width: 1.25rem; height: 1.25rem; @@ -130,11 +125,13 @@ } .icon-play { + position: relative; + display: inline-block; font-size: .75rem; color: #fff; - position: absolute; - top: .1875rem; margin-inline-start: -1.25rem; + margin-inline-end: 0.5rem; + bottom: 0.0625rem; } } } diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 5c09b5cc7..af3776579 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -59,7 +59,6 @@ type OwnProps = { folderId?: number; orderDiff: number; animationType: ChatAnimationTypes; - isSelected: boolean; isPinned?: boolean; }; @@ -75,6 +74,7 @@ type StateProps = { draft?: ApiFormattedText; messageListType?: MessageListType; animationLevel?: number; + isSelected?: boolean; lastSyncTime?: number; }; @@ -88,7 +88,6 @@ const Chat: FC = ({ folderId, orderDiff, animationType, - isSelected, isPinned, chat, isMuted, @@ -101,6 +100,7 @@ const Chat: FC = ({ draft, messageListType, animationLevel, + isSelected, lastSyncTime, openChat, focusLastMessage, @@ -324,7 +324,11 @@ export default memo(withGlobal( : undefined; const { targetUserId: actionTargetUserId, targetChatId: actionTargetChatId } = lastMessageAction || {}; const privateChatUserId = getPrivateChatUserId(chat); - const { type: messageListType } = selectCurrentMessageList(global) || {}; + const { + chatId: currentChatId, + threadId: currentThreadId, + type: messageListType, + } = selectCurrentMessageList(global) || {}; return { chat, @@ -338,6 +342,7 @@ export default memo(withGlobal( draft: selectDraft(global, chatId, MAIN_THREAD_ID), messageListType, animationLevel: global.settings.byKey.animationLevel, + isSelected: chatId === currentChatId && currentThreadId === MAIN_THREAD_ID, lastSyncTime: global.lastSyncTime, }; }, diff --git a/src/components/left/main/ChatList.tsx b/src/components/left/main/ChatList.tsx index 33ab6e735..9e739168a 100644 --- a/src/components/left/main/ChatList.tsx +++ b/src/components/left/main/ChatList.tsx @@ -5,7 +5,7 @@ import { withGlobal } from '../../../lib/teact/teactn'; import { GlobalActions } from '../../../global/types'; import { - ApiChat, ApiChatFolder, ApiUser, MAIN_THREAD_ID, + ApiChat, ApiChatFolder, ApiUser, } from '../../../api/types'; import { NotifyException, NotifySettings } from '../../../types'; @@ -15,7 +15,7 @@ import usePrevious from '../../../hooks/usePrevious'; import { mapValues, pick } from '../../../util/iteratees'; import { getChatOrder, prepareChatList, prepareFolderListIds } from '../../../modules/helpers'; import { - selectChatFolder, selectCurrentMessageList, selectNotifyExceptions, selectNotifySettings, + selectChatFolder, selectNotifyExceptions, selectNotifySettings, } from '../../../modules/selectors'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import { useChatAnimationType } from './hooks'; @@ -36,15 +36,13 @@ type StateProps = { usersById: Record; chatFolder?: ApiChatFolder; listIds?: number[]; - currentChatId?: number; orderedPinnedIds?: number[]; lastSyncTime?: number; - isInDiscussionThread?: boolean; notifySettings: NotifySettings; notifyExceptions?: Record; }; -type DispatchProps = Pick; +type DispatchProps = Pick; enum FolderTypeToListType { 'all' = 'active', @@ -60,15 +58,14 @@ const ChatList: FC = ({ chatsById, usersById, listIds, - currentChatId, orderedPinnedIds, lastSyncTime, - isInDiscussionThread, notifySettings, notifyExceptions, loadMoreChats, preloadTopChatMessages, openChat, + openNextChat, }) => { const [currentListIds, currentPinnedIds] = useMemo(() => { return folderType === 'folder' && chatFolder @@ -140,7 +137,6 @@ const ChatList: FC = ({ chatId={id} isPinned folderId={folderId} - isSelected={id === currentChatId && !isInDiscussionThread} animationType={getAnimationType(id)} orderDiff={orderDiffById[id]} // @ts-ignore @@ -153,7 +149,6 @@ const ChatList: FC = ({ teactOrderKey={getChatOrder(chat)} chatId={chat.id} folderId={folderId} - isSelected={chat.id === currentChatId && !isInDiscussionThread} animationType={getAnimationType(chat.id)} orderDiff={orderDiffById[chat.id]} // @ts-ignore @@ -181,21 +176,8 @@ const ChatList: FC = ({ const targetIndexDelta = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : undefined; if (!targetIndexDelta) return; - if (!currentChatId) { - e.preventDefault(); - openChat({ id: orderedIds[0] }); - return; - } - - const position = orderedIds.indexOf(currentChatId); - - if (position === -1) { - return; - } - const nextId = orderedIds[position + targetIndexDelta]; - e.preventDefault(); - openChat({ id: nextId }); + openNextChat({ targetIndexDelta, orderedIds }); } } }; @@ -238,15 +220,12 @@ export default memo(withGlobal( users: { byId: usersById }, lastSyncTime, } = global; - const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {}; - const listType = folderType !== 'folder' ? FolderTypeToListType[folderType] : undefined; const chatFolder = folderId ? selectChatFolder(global, folderId) : undefined; return { chatsById, usersById, - currentChatId, lastSyncTime, ...(listType ? { listIds: listIds[listType], @@ -254,7 +233,6 @@ export default memo(withGlobal( } : { chatFolder, }), - isInDiscussionThread: currentThreadId !== MAIN_THREAD_ID, notifySettings: selectNotifySettings(global), notifyExceptions: selectNotifyExceptions(global), }; @@ -263,5 +241,6 @@ export default memo(withGlobal( 'loadMoreChats', 'preloadTopChatMessages', 'openChat', + 'openNextChat', ]), )(ChatList)); diff --git a/src/components/left/search/ChatMessage.scss b/src/components/left/search/ChatMessage.scss index 025e7f7ed..5a8019a44 100644 --- a/src/components/left/search/ChatMessage.scss +++ b/src/components/left/search/ChatMessage.scss @@ -48,10 +48,6 @@ } } - .media-preview { - position: relative; - } - img { width: 1.25rem; height: 1.25rem; @@ -62,11 +58,13 @@ } .icon-play { + position: relative; + display: inline-block; font-size: .75rem; color: #fff; - position: absolute; - top: .1875rem; margin-inline-start: -1.25rem; + margin-inline-end: 0.5rem; + bottom: 0.0625rem; } } } diff --git a/src/components/main/Main.scss b/src/components/main/Main.scss index e3e66212c..05bc84307 100644 --- a/src/components/main/Main.scss +++ b/src/components/main/Main.scss @@ -81,13 +81,6 @@ } } - // @optimization - #Main.middle-column-open & { - .custom-scroll { - overflow: hidden; - } - } - #Main.history-animation-disabled & { transition: none; @@ -184,17 +177,3 @@ } } } - -.SymbolMenu { - @media (max-width: 600px) { - transition: transform var(--layer-transition); - - body.animation-level-0 & { - transition: none; - } - - body:not(.is-middle-column-open) & { - transform: translate3d(100vw, 0, 0) !important; - } - } -} diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 262ec6ebd..b41cf65c7 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -113,11 +113,6 @@ const Main: FC = ({ shouldSkipHistoryAnimations && 'history-animation-disabled', ); - useEffect(() => { - // For animating Symbol Menu on mobile - document.body.classList.toggle('is-middle-column-open', className.includes('middle-column-open')); - }, [className]); - // Add `body` classes when toggling right column useEffect(() => { if (animationLevel > 0) { diff --git a/src/components/middle/MessageList.scss b/src/components/middle/MessageList.scss index d0fe8a94a..80c9dd0a9 100644 --- a/src/components/middle/MessageList.scss +++ b/src/components/middle/MessageList.scss @@ -205,11 +205,19 @@ } } - &.scrolled .sticky-date { + &.scrolled:not(.is-animating) .sticky-date { position: sticky; top: 0.625rem; } + &.is-animating { + overflow: hidden; + } + + &.is-animating .message-select-control { + display: none !important; + } + .has-header-tools & .sticky-date { top: 3.75rem; } diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index b8a5bf2ec..460cb6f49 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -69,6 +69,7 @@ type OwnProps = { threadId: number; type: MessageListType; canPost: boolean; + isReady: boolean; onFabToggle: (shouldShow: boolean) => void; onNotchToggle: (shouldShow: boolean) => void; hasTools?: boolean; @@ -122,6 +123,7 @@ const MessageList: FC = ({ isChatLoaded, isChannelChat, canPost, + isReady, isChatWithSelf, messageIds, messagesById, @@ -331,9 +333,12 @@ const MessageList: FC = ({ // Memorize height for scroll animation const { height: windowHeight } = useWindowSize(); + useEffect(() => { - containerRef.current!.dataset.normalHeight = String(containerRef.current!.offsetHeight); - }, [windowHeight]); + if (isReady) { + containerRef.current!.dataset.normalHeight = String(containerRef.current!.offsetHeight); + } + }, [windowHeight, isReady]); // Initial message loading useEffect(() => { @@ -353,7 +358,7 @@ const MessageList: FC = ({ // Remember scroll position before repositioning it useOnChange(() => { - if (!messageIds || !listItemElementsRef.current) { + if (!messageIds || !listItemElementsRef.current || !isReady) { return; } @@ -370,7 +375,7 @@ const MessageList: FC = ({ anchorIdRef.current = anchor.id; anchorTopRef.current = anchor.getBoundingClientRect().top; // This should match deps for `useLayoutEffectWithPrevDeps` below - }, [messageIds, isViewportNewest, containerHeight, hasTools]); + }, [messageIds, isViewportNewest, containerHeight, hasTools, isReady]); // Handles updated message list, takes care of scroll repositioning useLayoutEffectWithPrevDeps(([ @@ -519,6 +524,7 @@ const MessageList: FC = ({ isSelectModeActive && 'select-mode-active', hasFocusing && 'has-focusing', isScrolled && 'scrolled', + !isReady && 'is-animating', ); return ( diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index 7cfe0197b..d22986db3 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -18,6 +18,7 @@ import { ANIMATION_END_DELAY, DARK_THEME_BG_COLOR, LIGHT_THEME_BG_COLOR, + ANIMATION_LEVEL_MIN, } from '../../config'; import { IS_SINGLE_COLUMN_LAYOUT, @@ -85,8 +86,9 @@ type StateProps = { shouldSkipHistoryAnimations?: boolean; }; -type DispatchProps = Pick; +type DispatchProps = Pick; const CLOSE_ANIMATION_DURATION = IS_SINGLE_COLUMN_LAYOUT ? 450 + ANIMATION_END_DELAY : undefined; @@ -129,6 +131,7 @@ const MiddleColumn: FC = ({ const [isFabShown, setIsFabShown] = useState(); const [isNotchShown, setIsNotchShown] = useState(); const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false); + const [isReady, setIsReady] = useState(!IS_SINGLE_COLUMN_LAYOUT || animationLevel === ANIMATION_LEVEL_MIN); const hasTools = hasPinnedOrAudioMessage && ( windowWidth < MOBILE_SCREEN_MAX_WIDTH @@ -162,6 +165,18 @@ const MiddleColumn: FC = ({ setIsNotchShown(undefined); }, [chatId]); + useEffect(() => { + if (animationLevel === ANIMATION_LEVEL_MIN) { + setIsReady(true); + } + }, [animationLevel]); + + const handleTransitionEnd = (e: React.TransitionEvent) => { + if (e.propertyName === 'transform' && e.target === e.currentTarget) { + setIsReady(Boolean(chatId)); + } + }; + useEffect(() => { if (isPrivate) { loadUser({ userId: chatId }); @@ -263,6 +278,7 @@ const MiddleColumn: FC = ({
= ({ chatId={renderingChatId} threadId={renderingThreadId} messageListType={renderingMessageListType} + isReady={isReady} /> = ({ hasTools={renderingHasTools} onFabToggle={setIsFabShown} onNotchToggle={setIsNotchShown} + isReady={isReady} />
{renderingCanPost && ( @@ -316,6 +334,7 @@ const MiddleColumn: FC = ({ messageListType={renderingMessageListType} dropAreaState={dropAreaState} onDropHide={handleHideDropArea} + isReady={isReady} /> )} {isPinnedMessageList && ( diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index c1c3485c6..526e71a42 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -73,6 +73,7 @@ type OwnProps = { chatId: number; threadId: number; messageListType: MessageListType; + isReady?: boolean; }; type StateProps = { @@ -106,6 +107,7 @@ const MiddleHeader: FC = ({ chatId, threadId, messageListType, + isReady, pinnedMessageIds, messagesById, canUnpin, @@ -143,10 +145,10 @@ const MiddleHeader: FC = ({ const topMessageTitle = topMessageSender ? getSenderTitle(lang, topMessageSender) : undefined; useEffect(() => { - if (threadId === MAIN_THREAD_ID && lastSyncTime) { + if (threadId === MAIN_THREAD_ID && lastSyncTime && isReady) { loadPinnedMessages({ chatId }); } - }, [chatId, loadPinnedMessages, lastSyncTime, threadId]); + }, [chatId, loadPinnedMessages, lastSyncTime, threadId, isReady]); // Reset pinned index when switching chats and pinning/unpinning useEffect(() => { diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index 86b3c29ad..583537c62 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -244,6 +244,7 @@ const AttachmentModal: FC = ({ /> = ({ shouldSchedule, canScheduleUntilOnline, onDropHide, + isReady, editingMessage, chatId, threadId, @@ -225,15 +227,13 @@ const Composer: FC = ({ }, [chatId]); useEffect(() => { - if (chatId && lastSyncTime && threadId === MAIN_THREAD_ID) { + if (chatId && lastSyncTime && threadId === MAIN_THREAD_ID && isReady) { loadScheduledHistory(); } - }, [chatId, loadScheduledHistory, lastSyncTime, threadId]); + }, [isReady, chatId, loadScheduledHistory, lastSyncTime, threadId]); useLayoutEffect(() => { - if (!appendixRef.current) { - return; - } + if (!appendixRef.current) return; appendixRef.current.innerHTML = APPENDIX; }, []); @@ -303,6 +303,7 @@ const Composer: FC = ({ Boolean(shouldSuggestStickers && allowedAttachmentOptions.canSendStickers && !attachments.length), html, stickersForEmoji, + !isReady, ); const { isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji, @@ -314,6 +315,7 @@ const Composer: FC = ({ setHtml, baseEmojiKeywords, emojiKeywords, + !isReady, ); const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => { @@ -608,6 +610,8 @@ const Composer: FC = ({ }, [isRightColumnShown, closeSymbolMenu]); useEffect(() => { + if (!isReady) return; + if (isSelectModeActive) { disableHover(); } else { @@ -615,7 +619,7 @@ const Composer: FC = ({ enableHover(); }, SELECT_MODE_TRANSITION_MS); } - }, [isSelectModeActive, enableHover, disableHover]); + }, [isSelectModeActive, enableHover, disableHover, isReady]); const mainButtonHandler = useCallback(() => { switch (mainButtonState) { @@ -687,7 +691,7 @@ const Composer: FC = ({ return (
- {allowedAttachmentOptions.canAttachMedia && ( + {allowedAttachmentOptions.canAttachMedia && isReady && ( = ({ > - + {!isSymbolMenuLoaded && } ) : ( = ({ id, + isAttachmentModalInput, editableInputId, html, placeholder, @@ -101,8 +103,9 @@ const MessageInput: FC = ({ const [selectedRange, setSelectedRange] = useState(); useEffect(() => { + if (!isAttachmentModalInput) return; updateInputHeight(false); - }, []); + }, [isAttachmentModalInput]); useLayoutEffectWithPrevDeps(([prevHtml]) => { if (html !== inputRef.current!.innerHTML) { diff --git a/src/components/middle/composer/SymbolMenu.scss b/src/components/middle/composer/SymbolMenu.scss index 3f4a3329b..866e100f7 100644 --- a/src/components/middle/composer/SymbolMenu.scss +++ b/src/components/middle/composer/SymbolMenu.scss @@ -19,6 +19,14 @@ transform: translate3d(0, calc(var(--symbol-menu-height) + var(--symbol-menu-footer-height)), 0); } } + + body.animation-level-0 & { + transition: none; + } + + &:not(.middle-column-open) { + transform: translate3d(100vw, 0, 0) !important; + } } &-main { diff --git a/src/components/middle/composer/SymbolMenu.tsx b/src/components/middle/composer/SymbolMenu.tsx index 8b16b0e4b..40782c48a 100644 --- a/src/components/middle/composer/SymbolMenu.tsx +++ b/src/components/middle/composer/SymbolMenu.tsx @@ -1,6 +1,7 @@ import React, { FC, memo, useCallback, useEffect, useLayoutEffect, useRef, useState, } from '../../../lib/teact/teact'; +import { withGlobal } from '../../../lib/teact/teactn'; import { ApiSticker, ApiVideo } from '../../../api/types'; @@ -38,10 +39,14 @@ export type OwnProps = { addRecentEmoji: AnyToVoidFunction; }; +type StateProps = { + isLeftColumnShown: boolean; +}; + let isActivated = false; -const SymbolMenu: FC = ({ - isOpen, allowedAttachmentOptions, +const SymbolMenu: FC = ({ + isOpen, allowedAttachmentOptions, isLeftColumnShown, onLoad, onClose, onEmojiSelect, onStickerSelect, onGifSelect, onRemoveSymbol, onSearchOpen, addRecentEmoji, @@ -188,6 +193,7 @@ const SymbolMenu: FC = ({ const className = buildClassName( 'SymbolMenu mobile-menu', transitionClassNames, + !isLeftColumnShown && 'middle-column-open', ); return ( @@ -216,4 +222,10 @@ const SymbolMenu: FC = ({ ); }; -export default memo(SymbolMenu); +export default memo(withGlobal( + (global): StateProps => { + return { + isLeftColumnShown: global.isLeftColumnShown, + }; + }, +)(SymbolMenu)); diff --git a/src/components/middle/composer/hooks/useEmojiTooltip.ts b/src/components/middle/composer/hooks/useEmojiTooltip.ts index 43f15e5fe..e214abf4d 100644 --- a/src/components/middle/composer/hooks/useEmojiTooltip.ts +++ b/src/components/middle/composer/hooks/useEmojiTooltip.ts @@ -36,6 +36,7 @@ export default function useEmojiTooltip( onUpdateHtml: (html: string) => void, baseEmojiKeywords?: Record, emojiKeywords?: Record, + isDisabled = false, ) { const [isOpen, markIsOpen, unmarkIsOpen] = useFlag(); @@ -60,6 +61,7 @@ export default function useEmojiTooltip( // Initialize data on first render. useEffect(() => { + if (isDisabled) return; const exec = () => { setById(emojiData.emojis); }; @@ -70,10 +72,10 @@ export default function useEmojiTooltip( ensureEmojiData() .then(exec); } - }, []); + }, [isDisabled]); useEffect(() => { - if (!byId) { + if (!byId || isDisabled) { return; } @@ -107,7 +109,7 @@ export default function useEmojiTooltip( }, {} as Record); setByName(emojisByName); setNames(Object.keys(emojisByName)); - }, [baseEmojiKeywords, byId, emojiKeywords]); + }, [isDisabled, baseEmojiKeywords, byId, emojiKeywords]); useEffect(() => { if (!isAllowed || !html || !byId || !keywords || !keywords.length) { diff --git a/src/components/middle/composer/hooks/useStickerTooltip.ts b/src/components/middle/composer/hooks/useStickerTooltip.ts index 1f85f4466..6b0e4baf2 100644 --- a/src/components/middle/composer/hooks/useStickerTooltip.ts +++ b/src/components/middle/composer/hooks/useStickerTooltip.ts @@ -11,6 +11,7 @@ export default function useStickerTooltip( isAllowed: boolean, html: string, stickers?: ApiSticker[], + isDisabled = false, ) { const { loadStickersForEmoji, clearStickersForEmoji } = getDispatch(); const isSingleEmoji = ( @@ -20,6 +21,8 @@ export default function useStickerTooltip( const hasStickers = Boolean(stickers) && isSingleEmoji; useEffect(() => { + if (isDisabled) return; + if (isAllowed && isSingleEmoji) { loadStickersForEmoji({ emoji: html }); } else if (hasStickers || !isSingleEmoji) { @@ -27,7 +30,7 @@ export default function useStickerTooltip( } // We omit `hasStickers` here to prevent re-fetching after manually closing tooltip (via ). // eslint-disable-next-line react-hooks/exhaustive-deps - }, [html, isSingleEmoji, clearStickersForEmoji, loadStickersForEmoji, isAllowed]); + }, [html, isSingleEmoji, clearStickersForEmoji, loadStickersForEmoji, isAllowed, isDisabled]); return { isStickerTooltipOpen: hasStickers, diff --git a/src/global/types.ts b/src/global/types.ts index e454086ce..b10a624e4 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -416,7 +416,7 @@ export type ActionTypes = ( 'joinChannel' | 'leaveChannel' | 'deleteChannel' | 'toggleChatPinned' | 'toggleChatArchived' | 'toggleChatUnread' | 'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' | 'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' | - 'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | + 'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' | // messages 'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' | 'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' | diff --git a/src/modules/actions/ui/chats.ts b/src/modules/actions/ui/chats.ts index b41950e00..93768ad56 100644 --- a/src/modules/actions/ui/chats.ts +++ b/src/modules/actions/ui/chats.ts @@ -54,3 +54,23 @@ addReducer('resetChatCreation', (global) => { chatCreation: undefined, }; }); + +addReducer('openNextChat', (global, actions, payload) => { + const { targetIndexDelta, orderedIds } = payload; + + const { chatId } = selectCurrentMessageList(global) || {}; + + if (!chatId) { + actions.openChat({ id: orderedIds[0] }); + return; + } + + const position = orderedIds.indexOf(chatId); + + if (position === -1) { + return; + } + const nextId = orderedIds[position + targetIndexDelta]; + + actions.openChat({ id: nextId }); +}); diff --git a/src/modules/helpers/messages.ts b/src/modules/helpers/messages.ts index 4877b3330..807359fd1 100644 --- a/src/modules/helpers/messages.ts +++ b/src/modules/helpers/messages.ts @@ -10,6 +10,7 @@ import { getChatTitle } from './chats'; const CONTENT_NOT_SUPPORTED = 'The message is not supported on this version of Telegram'; const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i'); +const TRUNCATED_SUMMARY_LENGTH = 80; export function getMessageKey(message: ApiMessage) { const { chatId, id } = message; @@ -32,16 +33,18 @@ export function getMessageSummaryText(lang: LangFn, message: ApiMessage, noEmoji text, photo, video, audio, voice, document, sticker, contact, poll, invoice, } = message.content; + const truncatedText = text && text.text.substr(0, TRUNCATED_SUMMARY_LENGTH); + if (message.groupedId) { - return `${noEmoji ? '' : '🖼 '}${text ? text.text : lang('lng_in_dlg_album')}`; + return `${noEmoji ? '' : '🖼 '}${truncatedText || lang('lng_in_dlg_album')}`; } if (photo) { - return `${noEmoji ? '' : '🖼 '}${text ? text.text : lang('AttachPhoto')}`; + return `${noEmoji ? '' : '🖼 '}${truncatedText || lang('AttachPhoto')}`; } if (video) { - return `${noEmoji ? '' : '📹 '}${text ? text.text : lang(video.isGif ? 'AttachGif' : 'AttachVideo')}`; + return `${noEmoji ? '' : '📹 '}${truncatedText || lang(video.isGif ? 'AttachGif' : 'AttachVideo')}`; } if (sticker) { @@ -53,11 +56,11 @@ export function getMessageSummaryText(lang: LangFn, message: ApiMessage, noEmoji } if (voice) { - return `${noEmoji ? '' : '🎤 '}${text ? text.text : lang('AttachAudio')}`; + return `${noEmoji ? '' : '🎤 '}${truncatedText || lang('AttachAudio')}`; } if (document) { - return `${noEmoji ? '' : '📎 '}${text ? text.text : document.fileName}`; + return `${noEmoji ? '' : '📎 '}${truncatedText || document.fileName}`; } if (contact) { @@ -73,7 +76,7 @@ export function getMessageSummaryText(lang: LangFn, message: ApiMessage, noEmoji } if (text) { - return text.text; + return truncatedText; } return CONTENT_NOT_SUPPORTED; diff --git a/src/modules/reducers/chats.ts b/src/modules/reducers/chats.ts index 3982cb18a..ba6d4c5f9 100644 --- a/src/modules/reducers/chats.ts +++ b/src/modules/reducers/chats.ts @@ -47,9 +47,10 @@ export function replaceChats(global: GlobalState, newById: Record, photo?: ApiPhoto, -): GlobalState { +): ApiChat { const { byId } = global.chats; const chat = byId[chatId]; const shouldOmitMinInfo = chatUpdate.isMin && chat && !chat.isMin; @@ -60,9 +61,19 @@ export function updateChat( }; if (!updatedChat.id || !updatedChat.type) { - return global; + return updatedChat; } + return updatedChat; +} + +export function updateChat( + global: GlobalState, chatId: number, chatUpdate: Partial, photo?: ApiPhoto, +): GlobalState { + const { byId } = global.chats; + + const updatedChat = getUpdatedChat(global, chatId, chatUpdate, photo); + return replaceChats(global, { ...byId, [chatId]: updatedChat, @@ -70,8 +81,17 @@ export function updateChat( } export function updateChats(global: GlobalState, updatedById: Record): GlobalState { - Object.keys(updatedById).forEach((id) => { - global = updateChat(global, Number(id), updatedById[Number(id)]); + const updatedChats = Object.keys(updatedById).map(Number).reduce>((acc, id) => { + const updatedChat = getUpdatedChat(global, id, updatedById[id]); + if (updatedChat) { + acc[id] = updatedChat; + } + return acc; + }, {}); + + global = replaceChats(global, { + ...global.chats.byId, + ...updatedChats, }); return global; @@ -80,10 +100,19 @@ export function updateChats(global: GlobalState, updatedById: Record): GlobalState { const { byId } = global.chats; - Object.keys(addedById).map(Number).forEach((id) => { + const addedChats = Object.keys(addedById).map(Number).reduce>((acc, id) => { if (!byId[id] || (byId[id].isMin && !addedById[id].isMin)) { - global = updateChat(global, id, addedById[id]); + const updatedChat = getUpdatedChat(global, id, addedById[id]); + if (updatedChat) { + acc[id] = updatedChat; + } } + return acc; + }, {}); + + global = replaceChats(global, { + ...global.chats.byId, + ...addedChats, }); return global; diff --git a/src/modules/reducers/users.ts b/src/modules/reducers/users.ts index 575317f1f..94d1f6419 100644 --- a/src/modules/reducers/users.ts +++ b/src/modules/reducers/users.ts @@ -13,29 +13,54 @@ export function replaceUsers(global: GlobalState, newById: Record): GlobalState { + +// @optimization Don't spread/unspread global for each element, do it in a batch +function getUpdatedUser(global: GlobalState, userId: number, userUpdate: Partial): ApiUser { const { byId } = global.users; - const { hash, userIds: contactUserIds } = global.contactList || {}; const user = byId[userId]; const shouldOmitMinInfo = userUpdate.isMin && user && !user.isMin; + const updatedUser = { ...user, ...(shouldOmitMinInfo ? omit(userUpdate, ['isMin', 'accessHash']) : userUpdate), }; if (!updatedUser.id || !updatedUser.type) { - return global; + return user; } - if (updatedUser.isContact && (contactUserIds && !contactUserIds.includes(userId))) { - global = { - ...global, - contactList: { - hash: hash || 0, - userIds: [userId, ...contactUserIds], - }, - }; - } + return updatedUser; +} + +function updateContactList(global: GlobalState, updatedUsers: ApiUser[]): GlobalState { + const { hash, userIds: contactUserIds } = global.contactList || {}; + + if (!contactUserIds) return global; + + const newContactUserIds = updatedUsers + .filter((user) => user && user.isContact && !contactUserIds.includes(user.id)) + .map((user) => user.id); + + if (newContactUserIds.length === 0) return global; + + return { + ...global, + contactList: { + hash: hash || 0, + userIds: [ + ...newContactUserIds, + ...contactUserIds, + ], + }, + }; +} + +export function updateUser(global: GlobalState, userId: number, userUpdate: Partial): GlobalState { + const { byId } = global.users; + + const updatedUser = getUpdatedUser(global, userId, userUpdate); + + global = updateContactList(global, [updatedUser]); return replaceUsers(global, { ...byId, @@ -43,9 +68,21 @@ export function updateUser(global: GlobalState, userId: number, userUpdate: Part }); } + export function updateUsers(global: GlobalState, updatedById: Record): GlobalState { - Object.keys(updatedById).map(Number).forEach((id) => { - global = updateUser(global, id, updatedById[id]); + const updatedUsers = Object.keys(updatedById).map(Number).reduce>((acc, id) => { + const updatedUser = getUpdatedUser(global, id, updatedById[id]); + if (updatedUser) { + acc[id] = updatedUser; + } + return acc; + }, {}); + + global = updateContactList(global, Object.values(updatedUsers)); + + global = replaceUsers(global, { + ...global.users.byId, + ...updatedUsers, }); return global; @@ -54,10 +91,22 @@ export function updateUsers(global: GlobalState, updatedById: Record): GlobalState { const { byId } = global.users; - Object.keys(addedById).map(Number).forEach((id) => { + + const addedUsers = Object.keys(addedById).map(Number).reduce>((acc, id) => { if (!byId[id] || (byId[id].isMin && !addedById[id].isMin)) { - global = updateUser(global, id, addedById[id]); + const updatedUser = getUpdatedUser(global, id, addedById[id]); + if (updatedUser) { + acc[id] = updatedUser; + } } + return acc; + }, {}); + + global = updateContactList(global, Object.values(addedUsers)); + + global = replaceUsers(global, { + ...global.users.byId, + ...addedUsers, }); return global;