diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 673137325..bb2ff696e 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -167,7 +167,7 @@ const Chat: FC = ({ }, [animationLevel, orderDiff, animationType]); const handleClick = useCallback(() => { - openChat({ id: chatId }); + openChat({ id: chatId, shouldReplaceHistory: true }); if (isSelected && messageListType === 'thread') { focusLastMessage(); diff --git a/src/components/left/main/ChatFolders.tsx b/src/components/left/main/ChatFolders.tsx index 12b10767e..641c84277 100644 --- a/src/components/left/main/ChatFolders.tsx +++ b/src/components/left/main/ChatFolders.tsx @@ -154,7 +154,7 @@ const ChatFolders: FC = ({ if (!digit) return; if (digit === SAVED_MESSAGES_HOTKEY) { - openChat({ id: currentUserId }); + openChat({ id: currentUserId, shouldReplaceHistory: true }); return; } diff --git a/src/components/left/main/ChatList.tsx b/src/components/left/main/ChatList.tsx index a9c1e7a12..4b9f13eeb 100644 --- a/src/components/left/main/ChatList.tsx +++ b/src/components/left/main/ChatList.tsx @@ -173,7 +173,7 @@ const ChatList: FC = ({ const position = Number(digit) - 1; if (position > orderedIds.length - 1) return; - openChat({ id: orderedIds[position] }); + openChat({ id: orderedIds[position], shouldReplaceHistory: true }); } if (e.altKey) { diff --git a/src/components/left/main/ContactList.tsx b/src/components/left/main/ContactList.tsx index 53453cfa3..c5b16f8bb 100644 --- a/src/components/left/main/ContactList.tsx +++ b/src/components/left/main/ContactList.tsx @@ -51,7 +51,7 @@ const ContactList: FC = ({ const handleClick = useCallback( (id: number) => { - openChat({ id }); + openChat({ id, shouldReplaceHistory: true }); }, [openChat], ); diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index d5e4f004a..a12a1dbcb 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -138,7 +138,7 @@ const LeftMainHeader: FC = ({ }, [searchQuery, onSearchQuery]); const handleSelectSaved = useCallback(() => { - openChat({ id: currentUserId }); + openChat({ id: currentUserId, shouldReplaceHistory: true }); }, [currentUserId, openChat]); const handleDarkModeToggle = useCallback((e: React.SyntheticEvent) => { diff --git a/src/components/left/search/ChatResults.tsx b/src/components/left/search/ChatResults.tsx index 3e1cc89fd..3c0a6a335 100644 --- a/src/components/left/search/ChatResults.tsx +++ b/src/components/left/search/ChatResults.tsx @@ -80,7 +80,7 @@ const ChatResults: FC = ({ const handleChatClick = useCallback( (id: number) => { - openChat({ id }); + openChat({ id, shouldReplaceHistory: true }); if (id !== currentUserId) { addRecentlyFoundChatId({ id }); diff --git a/src/components/left/search/RecentContacts.tsx b/src/components/left/search/RecentContacts.tsx index bef24f393..b3f368416 100644 --- a/src/components/left/search/RecentContacts.tsx +++ b/src/components/left/search/RecentContacts.tsx @@ -60,7 +60,7 @@ const RecentContacts: FC = ({ const handleClick = useCallback( (id: number) => { - openChat({ id }); + openChat({ id, shouldReplaceHistory: true }); onReset(); setTimeout(() => { addRecentlyFoundChatId({ id }); diff --git a/src/components/middle/MiddleColumn.scss b/src/components/middle/MiddleColumn.scss index 6d48fb1a7..2d4004b89 100644 --- a/src/components/middle/MiddleColumn.scss +++ b/src/components/middle/MiddleColumn.scss @@ -82,6 +82,8 @@ } .messages-layout { + --slide-transition: 450ms cubic-bezier(0.25, 1, 0.5, 1); + display: flex; flex-direction: column; align-items: center; diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index ec3778408..8f39ae1e9 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -4,7 +4,7 @@ import React, { import { withGlobal } from '../../lib/teact/teactn'; import { ApiChatBannedRights, MAIN_THREAD_ID } from '../../api/types'; -import { GlobalActions, MessageListType } from '../../global/types'; +import { GlobalActions, MessageListType, MessageList as GlobalMessageList } from '../../global/types'; import { ThemeKey } from '../../types'; import { @@ -36,7 +36,6 @@ import { selectIsRightColumnShown, selectPinnedIds, selectTheme, - selectThreadOriginChat, } from '../../modules/selectors'; import { getCanPostInChat, getMessageSendingRestrictionReason, isChatPrivate } from '../../modules/helpers'; import captureEscKeyListener from '../../util/captureEscKeyListener'; @@ -48,6 +47,7 @@ import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation'; import calculateMiddleFooterTransforms from './helpers/calculateMiddleFooterTransforms'; import useLang from '../../hooks/useLang'; import useHistoryBack from '../../hooks/useHistoryBack'; +import { createMessageHash } from '../../util/routing'; import Transition from '../ui/Transition'; import MiddleHeader from './MiddleHeader'; @@ -83,8 +83,9 @@ type StateProps = { isMobileSearchActive?: boolean; isSelectModeActive?: boolean; animationLevel?: number; - originChatId?: number; shouldSkipHistoryAnimations?: boolean; + currentTransitionKey: number; + messageLists?: GlobalMessageList[]; }; type DispatchProps = Pick = ({ messageListType, isPrivate, isPinnedMessageList, - isScheduledMessageList, + messageLists, canPost, currentUserBannedRights, defaultBannedRights, @@ -119,8 +120,8 @@ const MiddleColumn: FC = ({ isMobileSearchActive, isSelectModeActive, animationLevel, - originChatId, shouldSkipHistoryAnimations, + currentTransitionKey, openChat, unpinAllMessages, loadUser, @@ -260,26 +261,12 @@ const MiddleColumn: FC = ({ ); const closeChat = () => { - if (renderingThreadId !== MAIN_THREAD_ID) { - openChat({ id: originChatId, threadId: MAIN_THREAD_ID }, true); - } else if (isPinnedMessageList || isScheduledMessageList) { - openChat({ id: chatId, type: 'thread' }); - } else { - openChat({ id: undefined }, true); - } + openChat({ id: undefined }, true); }; - useHistoryBack(renderingChatId && renderingThreadId, closeChat, openChat, { - id: chatId, - threadId: MAIN_THREAD_ID, - }); - - const isDiscussion = renderingChatId && renderingThreadId !== MAIN_THREAD_ID; - - useHistoryBack(isDiscussion || isPinnedMessageList || isScheduledMessageList, closeChat, openChat, { - id: chatId, - threadId: renderingThreadId, - }); + useHistoryBack(renderingChatId && renderingThreadId, + closeChat, undefined, undefined, undefined, + messageLists ? messageLists.map(createMessageHash) : []); useHistoryBack(isMobileSearchActive, closeLocalTextSearch); useHistoryBack(isSelectModeActive, exitMessageSelectMode); @@ -320,7 +307,7 @@ const MiddleColumn: FC = ({ /> {() => ( @@ -408,6 +395,7 @@ export default memo(withGlobal( isBlurred: isBackgroundBlurred, background: customBackground, backgroundColor, patternColor, } = global.settings.themes[theme] || {}; + const { messageLists } = global.messages; const currentMessageList = selectCurrentMessageList(global); const { isLeftColumnShown, chats: { listIds } } = global; @@ -422,6 +410,7 @@ export default memo(withGlobal( isMobileSearchActive: Boolean(IS_SINGLE_COLUMN_LAYOUT && selectCurrentTextSearch(global)), isSelectModeActive: selectIsInSelectMode(global), animationLevel: global.settings.byKey.animationLevel, + currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1), }; if (!currentMessageList || !listIds.active) { @@ -437,14 +426,12 @@ export default memo(withGlobal( const isBotNotStarted = selectIsChatBotNotStarted(global, chatId); const isPinnedMessageList = messageListType === 'pinned'; const isScheduledMessageList = messageListType === 'scheduled'; - const originChat = selectThreadOriginChat(global, chatId, threadId); return { ...state, chatId, threadId, messageListType, - originChatId: originChat ? originChat.id : chatId, isPrivate: isChatPrivate(chatId), canPost: !isPinnedMessageList && (!chat || canPost) && !isBotNotStarted, isPinnedMessageList, @@ -458,6 +445,7 @@ export default memo(withGlobal( ), pinnedMessagesCount: pinnedIds ? pinnedIds.length : 0, shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations, + messageLists, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index 526e71a42..bf99be81c 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -36,7 +36,6 @@ import { selectAllowedMessageActions, selectIsRightColumnShown, selectThreadTopMessageId, - selectThreadOriginChat, selectThreadInfo, selectChatMessages, selectPinnedIds, @@ -88,7 +87,6 @@ type StateProps = { isRightColumnShown?: boolean; audioMessage?: ApiMessage; chatsById?: Record; - originChatId: number; messagesCount?: number; isChatWithSelf?: boolean; isChatWithBot?: boolean; @@ -96,11 +94,12 @@ type StateProps = { notifySettings: NotifySettings; notifyExceptions?: Record; shouldSkipHistoryAnimations?: boolean; + currentTransitionKey: number; }; type DispatchProps = Pick; const MiddleHeader: FC = ({ @@ -119,7 +118,6 @@ const MiddleHeader: FC = ({ audioMessage, chat, chatsById, - originChatId, messagesCount, isChatWithSelf, isChatWithBot, @@ -127,10 +125,12 @@ const MiddleHeader: FC = ({ notifySettings, notifyExceptions, shouldSkipHistoryAnimations, + currentTransitionKey, openChatWithInfo, pinMessage, focusMessage, openChat, + openPreviousChat, loadPinnedMessages, toggleLeftColumn, exitMessageSelectMode, @@ -194,7 +194,8 @@ const MiddleHeader: FC = ({ messageInput.blur(); } } - if (threadId === MAIN_THREAD_ID && messageListType === 'thread') { + + if (threadId === MAIN_THREAD_ID && messageListType === 'thread' && currentTransitionKey === 0) { if (IS_SINGLE_COLUMN_LAYOUT || shouldShowCloseButton) { e.stopPropagation(); // Stop propagation to prevent chat re-opening on tablets openChat({ id: undefined }); @@ -209,10 +210,10 @@ const MiddleHeader: FC = ({ exitMessageSelectMode(); } - openChat({ id: originChatId, threadId: MAIN_THREAD_ID }); + openPreviousChat(); }, [ - openChat, originChatId, threadId, messageListType, toggleLeftColumn, isSelectModeActive, exitMessageSelectMode, - shouldShowCloseButton, + threadId, messageListType, currentTransitionKey, isSelectModeActive, openPreviousChat, shouldShowCloseButton, + openChat, toggleLeftColumn, exitMessageSelectMode, ]); const unreadCount = useMemo(() => { @@ -339,7 +340,7 @@ const MiddleHeader: FC = ({ function renderMainThreadInfo() { return ( <> - {isLeftColumnHideable && renderBackButton(shouldShowCloseButton, unreadCount)} + {(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, unreadCount)}
{isChatPrivate(chatId) ? ( = ({
{renderInfo} @@ -439,8 +440,6 @@ export default memo(withGlobal( ? selectChatMessage(global, audioChatId, audioMessageId) : undefined; - const originChat = selectThreadOriginChat(global, chatId, threadId); - let messagesCount: number | undefined; if (messageListType === 'pinned') { const pinnedIds = selectPinnedIds(global, chatId); @@ -463,7 +462,6 @@ export default memo(withGlobal( audioMessage, chat, chatsById, - originChatId: originChat ? originChat.id : chatId, messagesCount, isChatWithSelf: selectIsChatWithSelf(global, chatId), isChatWithBot: chat && selectIsChatWithBot(global, chat), @@ -471,6 +469,7 @@ export default memo(withGlobal( notifySettings: selectNotifySettings(global), notifyExceptions: selectNotifyExceptions(global), shouldSkipHistoryAnimations, + currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1), }; const messagesById = selectChatMessages(global, chatId); @@ -514,6 +513,7 @@ export default memo(withGlobal( 'pinMessage', 'focusMessage', 'openChat', + 'openPreviousChat', 'loadPinnedMessages', 'toggleLeftColumn', 'exitMessageSelectMode', diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index 27bbd4a8c..63f00875c 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -64,7 +64,17 @@ const Modal: FC = ({ : undefined), [isOpen, onClose, onEnter]); useEffect(() => (isOpen && modalRef.current ? trapFocus(modalRef.current) : undefined), [isOpen]); - useHistoryBack(isOpen, onClose); + const { forceClose } = useHistoryBack(isOpen, onClose); + + // For modals that are closed by unmounting without changing `isOpen` to `false` + useEffect(() => { + return () => { + if (isOpen) { + forceClose(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffectWithPrevDeps(([prevIsOpen]) => { document.body.classList.toggle('has-open-dialog', isOpen); diff --git a/src/global/cache.ts b/src/global/cache.ts index b1d0c4890..043bbd95e 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -229,7 +229,7 @@ function reduceMessages(global: GlobalState): GlobalState['messages'] { return { byChatId, - messageLists: !currentMessageList || IS_SINGLE_COLUMN_LAYOUT ? undefined : [{ + messageLists: !currentMessageList || IS_SINGLE_COLUMN_LAYOUT ? [] : [{ ...currentMessageList, threadId: MAIN_THREAD_ID, type: 'thread', diff --git a/src/global/initial.ts b/src/global/initial.ts index c333a59a7..387dce87a 100644 --- a/src/global/initial.ts +++ b/src/global/initial.ts @@ -33,6 +33,7 @@ export const INITIAL_STATE: GlobalState = { messages: { byChatId: {}, + messageLists: [], }, scheduledMessages: { diff --git a/src/global/types.ts b/src/global/types.ts index 33ecaabd9..3ce922430 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -46,6 +46,12 @@ import { export type MessageListType = 'thread' | 'pinned' | 'scheduled'; +export interface MessageList { + chatId: number; + threadId: number; + type: MessageListType; +} + export interface Thread { listedIds?: number[]; outlyingIds?: number[]; @@ -134,11 +140,7 @@ export type GlobalState = { byId: Record; threadsById: Record; }>; - messageLists?: { - chatId: number; - threadId: number; - type: MessageListType; - }[]; + messageLists: MessageList[]; contentToBeScheduled?: { gif?: ApiVideo; sticker?: ApiSticker; @@ -439,7 +441,7 @@ export type ActionTypes = ( 'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' | 'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' | 'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' | - 'addChatMembers' | 'deleteChatMember' | + 'addChatMembers' | 'deleteChatMember' | 'openPreviousChat' | // messages 'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' | 'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' | diff --git a/src/hooks/useHistoryBack.ts b/src/hooks/useHistoryBack.ts index 880574333..f18358985 100644 --- a/src/hooks/useHistoryBack.ts +++ b/src/hooks/useHistoryBack.ts @@ -1,8 +1,9 @@ -import { useEffect, useRef } from '../lib/teact/teact'; +import { useCallback, useEffect, useRef } from '../lib/teact/teact'; import { IS_IOS } from '../util/environment'; import usePrevious from './usePrevious'; import { getDispatch } from '../lib/teact/teactn'; +import { areSortedArraysEqual } from '../util/iteratees'; // Carefully selected by swiping and observing visual changes // TODO: may be different on other devices such as iPad, maybe take dpi into account? @@ -61,48 +62,19 @@ export default function useHistoryBack( onForward?: (state: any) => void, currentState?: any, shouldReplaceNext = false, + hashes?: string[], ) { const indexRef = useRef(-1); const isForward = useRef(false); const prevIsActive = usePrevious(isActive); const isClosed = useRef(true); + const indexHashRef = useRef<{ index: number; hash: string }[]>([]); + const prevHashes = usePrevious(hashes); + const isHashChangedFromEvent = useRef(false); - useEffect(() => { - const handlePopState = (event: PopStateEvent) => { - if (historyState.isHistoryAltered) { - setTimeout(() => { - historyState.isHistoryAltered = false; - }, 0); - return; - } - const { index: i } = event.state; - const index = i || 0; - - const prev = historyState.currentIndexes[historyState.currentIndexes.indexOf(indexRef.current) - 1]; - - if (historyState.isDisabled) return; - - if (!isClosed.current && (index === 0 || index === prev)) { - historyState.currentIndexes.splice(historyState.currentIndexes.indexOf(indexRef.current), 1); - - if (onBack) { - if (historyState.isEdge) { - getDispatch().disableHistoryAnimations(); - } - onBack(!historyState.isEdge); - isClosed.current = true; - } - } else if (index === indexRef.current && isClosed.current && onForward) { - isForward.current = true; - if (historyState.isEdge) { - getDispatch().disableHistoryAnimations(); - } - onForward(event.state.state); - } - }; - - if (!historyState.isDisabled && prevIsActive !== isActive) { - if (isActive) { + const handleChange = useCallback((isForceClose = false) => { + if (!hashes) { + if (isActive && !isForceClose) { isClosed.current = false; if (isForward.current) { @@ -126,6 +98,7 @@ export default function useHistoryBack( state: currentState, }, ''); + indexRef.current = index; if (shouldReplaceNext) { @@ -133,8 +106,10 @@ export default function useHistoryBack( } }, 0); } - } else if (!isClosed.current) { - if (indexRef.current === historyState.currentIndex || !shouldReplaceNext) { + } + + if ((isForceClose || !isActive) && !isClosed.current) { + if ((indexRef.current === historyState.currentIndex || !shouldReplaceNext)) { historyState.isHistoryAltered = true; window.history.back(); @@ -146,9 +121,127 @@ export default function useHistoryBack( isClosed.current = true; } + } else { + const prev = prevHashes || []; + if (prev.length < hashes.length) { + const index = ++historyState.currentIndex; + historyState.currentIndexes.push(index); + + window.history.pushState({ + index, + state: currentState, + }, '', `#${hashes[hashes.length - 1]}`); + + indexHashRef.current.push({ + index, + hash: hashes[hashes.length - 1], + }); + } else { + const delta = prev.length - hashes.length; + if (isHashChangedFromEvent.current) { + isHashChangedFromEvent.current = false; + } else { + if (hashes.length !== indexHashRef.current.length) { + if (delta > 0) { + const last = indexHashRef.current[indexHashRef.current.length - delta - 1]; + let realDelta = delta; + if (last) { + const indexLast = historyState.currentIndexes.findIndex( + (l) => l === last.index, + ); + realDelta = historyState.currentIndexes.length - indexLast - 1; + } + historyState.isHistoryAltered = true; + window.history.go(-realDelta); + const removed = indexHashRef.current.splice(indexHashRef.current.length - delta - 1, delta); + removed.forEach(({ index }) => { + historyState.currentIndexes.splice(historyState.currentIndexes.indexOf(index), 1); + }); + } + } + + if (hashes.length > 0) { + setTimeout(() => { + const index = ++historyState.currentIndex; + historyState.currentIndexes[historyState.currentIndexes.length - 1] = index; + + window.history.replaceState({ + index, + state: currentState, + }, '', `#${hashes[hashes.length - 1]}`); + + indexHashRef.current[indexHashRef.current.length - 1] = { + index, + hash: hashes[hashes.length - 1], + }; + }, 0); + } + } + } + } + }, [currentState, hashes, isActive, prevHashes, shouldReplaceNext]); + + useEffect(() => { + const handlePopState = (event: PopStateEvent) => { + if (historyState.isHistoryAltered) { + setTimeout(() => { + historyState.isHistoryAltered = false; + }, 0); + return; + } + const { index: i } = event.state; + const index = i || 0; + try { + const currIndex = hashes ? indexHashRef.current[indexHashRef.current.length - 1].index : indexRef.current; + + const prev = historyState.currentIndexes[historyState.currentIndexes.indexOf(currIndex) - 1]; + + if (historyState.isDisabled) return; + + if ((!isClosed.current && (index === 0 || index === prev)) || (hashes && (index === 0 || index === prev))) { + if (hashes) { + isHashChangedFromEvent.current = true; + indexHashRef.current.pop(); + } + + historyState.currentIndexes.splice(historyState.currentIndexes.indexOf(currIndex), 1); + + if (onBack) { + if (historyState.isEdge) { + getDispatch() + .disableHistoryAnimations(); + } + onBack(!historyState.isEdge); + isClosed.current = true; + } + } else if (index === currIndex && isClosed.current && onForward && !hashes) { + isForward.current = true; + if (historyState.isEdge) { + getDispatch() + .disableHistoryAnimations(); + } + onForward(event.state.state); + } + } catch (e) { + // Forward navigation for hashed is not supported + } + }; + + const hasChanged = hashes + ? (!prevHashes || !areSortedArraysEqual(prevHashes, hashes)) + : prevIsActive !== isActive; + + if (!historyState.isDisabled && hasChanged) { + handleChange(); } window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); - }, [currentState, isActive, onBack, onForward, prevIsActive, shouldReplaceNext]); + }, [ + currentState, handleChange, hashes, isActive, onBack, onForward, prevHashes, prevIsActive, shouldReplaceNext, + ]); + + return { + forceClose: () => handleChange(true), + }; } diff --git a/src/modules/actions/ui/chats.ts b/src/modules/actions/ui/chats.ts index c7269728a..7af1bef64 100644 --- a/src/modules/actions/ui/chats.ts +++ b/src/modules/actions/ui/chats.ts @@ -8,7 +8,7 @@ import { closeLocalTextSearch } from './localSearch'; addReducer('openChat', (global, actions, payload) => { const { - id, threadId = -1, type = 'thread', + id, threadId = -1, type = 'thread', shouldReplaceHistory = false, } = payload!; const currentMessageList = selectCurrentMessageList(global); @@ -37,7 +37,11 @@ addReducer('openChat', (global, actions, payload) => { setGlobal(global); } - return updateCurrentMessageList(global, id, threadId, type); + return updateCurrentMessageList(global, id, threadId, type, shouldReplaceHistory); +}); + +addReducer('openPreviousChat', (global) => { + return updateCurrentMessageList(global, undefined); }); addReducer('openChatWithInfo', (global, actions, payload) => { @@ -80,5 +84,5 @@ addReducer('openNextChat', (global, actions, payload) => { } const nextId = orderedIds[position + targetIndexDelta]; - actions.openChat({ id: nextId }); + actions.openChat({ id: nextId, shouldReplaceHistory: true }); }); diff --git a/src/modules/actions/ui/misc.ts b/src/modules/actions/ui/misc.ts index 5516566f1..757644f79 100644 --- a/src/modules/actions/ui/misc.ts +++ b/src/modules/actions/ui/misc.ts @@ -58,16 +58,14 @@ addReducer('closeManagement', (global): GlobalState | undefined => { }; }); -addReducer('openChat', (global, actions, payload) => { +addReducer('openChat', (global) => { if (!IS_SINGLE_COLUMN_LAYOUT && !IS_TABLET_COLUMN_LAYOUT) { return undefined; } - const { id } = payload!; - return { ...global, - isLeftColumnShown: id === undefined, + isLeftColumnShown: global.messages.messageLists.length === 0, }; }); diff --git a/src/modules/reducers/messages.ts b/src/modules/reducers/messages.ts index d5006a42b..377e36052 100644 --- a/src/modules/reducers/messages.ts +++ b/src/modules/reducers/messages.ts @@ -1,4 +1,6 @@ -import { GlobalState, MessageListType, Thread } from '../../global/types'; +import { + GlobalState, MessageList, MessageListType, Thread, +} from '../../global/types'; import { ApiMessage, ApiThreadInfo, MAIN_THREAD_ID } from '../../api/types'; import { FocusDirection } from '../../types'; @@ -21,6 +23,8 @@ import { areSortedArraysEqual, omit, pickTruthy, unique, } from '../../util/iteratees'; +const TMP_CHAT_ID = -1; + type MessageStoreSections = { byId: Record; threadsById: Record; @@ -31,13 +35,30 @@ export function updateCurrentMessageList( chatId: number | undefined, threadId: number = MAIN_THREAD_ID, type: MessageListType = 'thread', + shouldReplaceHistory?: boolean, ): GlobalState { + const { messageLists } = global.messages; + let newMessageLists: MessageList[] = messageLists; + if (shouldReplaceHistory) { + newMessageLists = chatId ? [{ chatId, threadId, type }] : []; + } else if (chatId) { + const last = messageLists[messageLists.length - 1]; + if (!last || last.chatId !== chatId || last.threadId !== threadId || last.type !== type) { + if (last && last.chatId === TMP_CHAT_ID) { + newMessageLists = [...messageLists.slice(0, -1), { chatId, threadId, type }]; + } else { + newMessageLists = [...messageLists, { chatId, threadId, type }]; + } + } + } else { + newMessageLists = messageLists.slice(0, -1); + } + return { ...global, messages: { ...global.messages, - // TODO Support stack navigation - messageLists: chatId ? [{ chatId, threadId, type }] : undefined, + messageLists: newMessageLists, }, }; } diff --git a/src/modules/selectors/messages.ts b/src/modules/selectors/messages.ts index 5efac836c..73571c1b2 100644 --- a/src/modules/selectors/messages.ts +++ b/src/modules/selectors/messages.ts @@ -39,7 +39,7 @@ const MESSAGE_EDIT_ALLOWED_TIME_MS = 172800000; // 48 hours export function selectCurrentMessageList(global: GlobalState) { const { messageLists } = global.messages; - if (messageLists && messageLists.length) { + if (messageLists.length) { return messageLists[messageLists.length - 1]; } diff --git a/src/util/fastSmoothScroll.ts b/src/util/fastSmoothScroll.ts index f9f36ff5c..74b786f90 100644 --- a/src/util/fastSmoothScroll.ts +++ b/src/util/fastSmoothScroll.ts @@ -22,21 +22,13 @@ export default function fastSmoothScroll( forceDuration?: number, forceCurrentContainerHeight?: boolean, ) { + const scrollFrom = calculateScrollFrom(container, element, maxDistance, forceDirection); + if (forceDirection === FocusDirection.Static) { - let block!: ScrollLogicalPosition; - - if (position === 'centerOrTop') { - block = element.offsetHeight < container.offsetHeight ? 'center' : 'start'; - } else { - block = position; - } - - element.scrollIntoView({ block }); - + scrollWithJs(container, element, scrollFrom, position, margin, 0); return; } - const scrollFrom = calculateScrollFrom(container, element, maxDistance, forceDirection); if (getGlobal().settings.byKey.animationLevel === ANIMATION_LEVEL_MIN) { forceDuration = 0; diff --git a/src/util/routing.ts b/src/util/routing.ts new file mode 100644 index 000000000..ad3675976 --- /dev/null +++ b/src/util/routing.ts @@ -0,0 +1,19 @@ +import { MessageList, MessageListType } from '../global/types'; +import { MAIN_THREAD_ID } from '../api/types'; + +export const createMessageHash = (messageList: MessageList): string => ( + messageList.chatId.toString() + + (messageList.type !== 'thread' ? `_${messageList.type}` + : (messageList.threadId !== -1 ? `_${messageList.threadId}` : '')) +); + +export const parseMessageHash = (value: string): MessageList => { + const [chatId, typeOrThreadId] = value.split('_'); + const isType = ['thread', 'pinned', 'scheduled'].includes(typeOrThreadId); + + return { + chatId: Number(chatId), + type: !!typeOrThreadId && isType ? (typeOrThreadId as MessageListType) : 'thread', + threadId: !!typeOrThreadId && !isType ? Number(typeOrThreadId) : MAIN_THREAD_ID, + }; +};