History support and animation when navigating between chats (#1289)

This commit is contained in:
Alexander Zinchuk 2021-08-03 01:31:08 +03:00
parent e69ea676f3
commit 7574b6445b
21 changed files with 247 additions and 117 deletions

View File

@ -167,7 +167,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
}, [animationLevel, orderDiff, animationType]);
const handleClick = useCallback(() => {
openChat({ id: chatId });
openChat({ id: chatId, shouldReplaceHistory: true });
if (isSelected && messageListType === 'thread') {
focusLastMessage();

View File

@ -154,7 +154,7 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
if (!digit) return;
if (digit === SAVED_MESSAGES_HOTKEY) {
openChat({ id: currentUserId });
openChat({ id: currentUserId, shouldReplaceHistory: true });
return;
}

View File

@ -173,7 +173,7 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
const position = Number(digit) - 1;
if (position > orderedIds.length - 1) return;
openChat({ id: orderedIds[position] });
openChat({ id: orderedIds[position], shouldReplaceHistory: true });
}
if (e.altKey) {

View File

@ -51,7 +51,7 @@ const ContactList: FC<OwnProps & StateProps & DispatchProps> = ({
const handleClick = useCallback(
(id: number) => {
openChat({ id });
openChat({ id, shouldReplaceHistory: true });
},
[openChat],
);

View File

@ -138,7 +138,7 @@ const LeftMainHeader: FC<OwnProps & StateProps & DispatchProps> = ({
}, [searchQuery, onSearchQuery]);
const handleSelectSaved = useCallback(() => {
openChat({ id: currentUserId });
openChat({ id: currentUserId, shouldReplaceHistory: true });
}, [currentUserId, openChat]);
const handleDarkModeToggle = useCallback((e: React.SyntheticEvent<HTMLElement>) => {

View File

@ -80,7 +80,7 @@ const ChatResults: FC<OwnProps & StateProps & DispatchProps> = ({
const handleChatClick = useCallback(
(id: number) => {
openChat({ id });
openChat({ id, shouldReplaceHistory: true });
if (id !== currentUserId) {
addRecentlyFoundChatId({ id });

View File

@ -60,7 +60,7 @@ const RecentContacts: FC<OwnProps & StateProps & DispatchProps> = ({
const handleClick = useCallback(
(id: number) => {
openChat({ id });
openChat({ id, shouldReplaceHistory: true });
onReset();
setTimeout(() => {
addRecentlyFoundChatId({ id });

View File

@ -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;

View File

@ -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<GlobalActions, (
@ -103,7 +104,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
messageListType,
isPrivate,
isPinnedMessageList,
isScheduledMessageList,
messageLists,
canPost,
currentUserBannedRights,
defaultBannedRights,
@ -119,8 +120,8 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
isMobileSearchActive,
isSelectModeActive,
animationLevel,
originChatId,
shouldSkipHistoryAnimations,
currentTransitionKey,
openChat,
unpinAllMessages,
loadUser,
@ -260,26 +261,12 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
);
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<StateProps & DispatchProps> = ({
/>
<Transition
name={shouldSkipHistoryAnimations ? 'none' : animationLevel === ANIMATION_LEVEL_MAX ? 'slide' : 'fade'}
activeKey={renderingMessageListType === 'thread' && renderingThreadId === MAIN_THREAD_ID ? 1 : 2}
activeKey={currentTransitionKey}
shouldCleanup
>
{() => (
@ -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, [

View File

@ -36,7 +36,6 @@ import {
selectAllowedMessageActions,
selectIsRightColumnShown,
selectThreadTopMessageId,
selectThreadOriginChat,
selectThreadInfo,
selectChatMessages,
selectPinnedIds,
@ -88,7 +87,6 @@ type StateProps = {
isRightColumnShown?: boolean;
audioMessage?: ApiMessage;
chatsById?: Record<number, ApiChat>;
originChatId: number;
messagesCount?: number;
isChatWithSelf?: boolean;
isChatWithBot?: boolean;
@ -96,11 +94,12 @@ type StateProps = {
notifySettings: NotifySettings;
notifyExceptions?: Record<number, NotifyException>;
shouldSkipHistoryAnimations?: boolean;
currentTransitionKey: number;
};
type DispatchProps = Pick<GlobalActions, (
'openChatWithInfo' | 'pinMessage' | 'focusMessage' | 'openChat' | 'loadPinnedMessages' | 'toggleLeftColumn' |
'exitMessageSelectMode'
'openChatWithInfo' | 'pinMessage' | 'focusMessage' | 'openChat' | 'openPreviousChat' | 'loadPinnedMessages' |
'toggleLeftColumn' | 'exitMessageSelectMode'
)>;
const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
@ -119,7 +118,6 @@ const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
audioMessage,
chat,
chatsById,
originChatId,
messagesCount,
isChatWithSelf,
isChatWithBot,
@ -127,10 +125,12 @@ const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
notifySettings,
notifyExceptions,
shouldSkipHistoryAnimations,
currentTransitionKey,
openChatWithInfo,
pinMessage,
focusMessage,
openChat,
openPreviousChat,
loadPinnedMessages,
toggleLeftColumn,
exitMessageSelectMode,
@ -194,7 +194,8 @@ const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
function renderMainThreadInfo() {
return (
<>
{isLeftColumnHideable && renderBackButton(shouldShowCloseButton, unreadCount)}
{(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, unreadCount)}
<div className="chat-info-wrapper" onClick={handleHeaderClick}>
{isChatPrivate(chatId) ? (
<PrivateChatInfo
@ -390,7 +391,7 @@ const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
<div className="MiddleHeader" ref={componentRef}>
<Transition
name={shouldSkipHistoryAnimations ? 'none' : 'slide-fade'}
activeKey={messageListType === 'thread' ? threadId : 1}
activeKey={currentTransitionKey}
>
{renderInfo}
</Transition>
@ -439,8 +440,6 @@ export default memo(withGlobal<OwnProps>(
? 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<OwnProps>(
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<OwnProps>(
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<OwnProps>(
'pinMessage',
'focusMessage',
'openChat',
'openPreviousChat',
'loadPinnedMessages',
'toggleLeftColumn',
'exitMessageSelectMode',

View File

@ -64,7 +64,17 @@ const Modal: FC<OwnProps & StateProps> = ({
: 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);

View File

@ -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',

View File

@ -33,6 +33,7 @@ export const INITIAL_STATE: GlobalState = {
messages: {
byChatId: {},
messageLists: [],
},
scheduledMessages: {

View File

@ -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<number, ApiMessage>;
threadsById: Record<number, Thread>;
}>;
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' |

View File

@ -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<boolean>(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),
};
}

View File

@ -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 });
});

View File

@ -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,
};
});

View File

@ -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<number, ApiMessage>;
threadsById: Record<number, Thread>;
@ -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,
},
};
}

View File

@ -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];
}

View File

@ -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;

19
src/util/routing.ts Normal file
View File

@ -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,
};
};