[Perf] Various fixes for chat opening animation on Android (#1234)

This commit is contained in:
Alexander Zinchuk 2021-07-13 17:31:34 +03:00
parent 0a594a84e1
commit fa8e750433
24 changed files with 246 additions and 126 deletions

View File

@ -1,5 +1,4 @@
.MessageOutgoingStatus {
position: relative;
width: 1.19rem;
height: 1.19rem;
overflow: hidden;

View File

@ -1,5 +1,4 @@
.Badge-transition {
transform: scale(1);
opacity: 1;
transition: transform .3s cubic-bezier(0.34, 1.56, 0.64, 1);

View File

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

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
folderId,
orderDiff,
animationType,
isSelected,
isPinned,
chat,
isMuted,
@ -101,6 +100,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
draft,
messageListType,
animationLevel,
isSelected,
lastSyncTime,
openChat,
focusLastMessage,
@ -324,7 +324,11 @@ export default memo(withGlobal<OwnProps>(
: 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<OwnProps>(
draft: selectDraft(global, chatId, MAIN_THREAD_ID),
messageListType,
animationLevel: global.settings.byKey.animationLevel,
isSelected: chatId === currentChatId && currentThreadId === MAIN_THREAD_ID,
lastSyncTime: global.lastSyncTime,
};
},

View File

@ -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<number, ApiUser>;
chatFolder?: ApiChatFolder;
listIds?: number[];
currentChatId?: number;
orderedPinnedIds?: number[];
lastSyncTime?: number;
isInDiscussionThread?: boolean;
notifySettings: NotifySettings;
notifyExceptions?: Record<number, NotifyException>;
};
type DispatchProps = Pick<GlobalActions, 'loadMoreChats' | 'preloadTopChatMessages' | 'openChat'>;
type DispatchProps = Pick<GlobalActions, 'loadMoreChats' | 'preloadTopChatMessages' | 'openChat' | 'openNextChat'>;
enum FolderTypeToListType {
'all' = 'active',
@ -60,15 +58,14 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
chatId={id}
isPinned
folderId={folderId}
isSelected={id === currentChatId && !isInDiscussionThread}
animationType={getAnimationType(id)}
orderDiff={orderDiffById[id]}
// @ts-ignore
@ -153,7 +149,6 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps>(
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<OwnProps>(
} : {
chatFolder,
}),
isInDiscussionThread: currentThreadId !== MAIN_THREAD_ID,
notifySettings: selectNotifySettings(global),
notifyExceptions: selectNotifyExceptions(global),
};
@ -263,5 +241,6 @@ export default memo(withGlobal<OwnProps>(
'loadMoreChats',
'preloadTopChatMessages',
'openChat',
'openNextChat',
]),
)(ChatList));

View File

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

View File

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

View File

@ -113,11 +113,6 @@ const Main: FC<StateProps & DispatchProps> = ({
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) {

View File

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

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
isChatLoaded,
isChannelChat,
canPost,
isReady,
isChatWithSelf,
messageIds,
messagesById,
@ -331,9 +333,12 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
// 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<OwnProps & StateProps & DispatchProps> = ({
// Remember scroll position before repositioning it
useOnChange(() => {
if (!messageIds || !listItemElementsRef.current) {
if (!messageIds || !listItemElementsRef.current || !isReady) {
return;
}
@ -370,7 +375,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
isSelectModeActive && 'select-mode-active',
hasFocusing && 'has-focusing',
isScrolled && 'scrolled',
!isReady && 'is-animating',
);
return (

View File

@ -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<GlobalActions, 'openChat' | 'unpinAllMessages' | 'loadUser' |
'closeLocalTextSearch' | 'exitMessageSelectMode'>;
type DispatchProps = Pick<GlobalActions, (
'openChat' | 'unpinAllMessages' | 'loadUser' | 'closeLocalTextSearch' | 'exitMessageSelectMode'
)>;
const CLOSE_ANIMATION_DURATION = IS_SINGLE_COLUMN_LAYOUT ? 450 + ANIMATION_END_DELAY : undefined;
@ -129,6 +131,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
const [isFabShown, setIsFabShown] = useState<boolean | undefined>();
const [isNotchShown, setIsNotchShown] = useState<boolean | undefined>();
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<StateProps & DispatchProps> = ({
setIsNotchShown(undefined);
}, [chatId]);
useEffect(() => {
if (animationLevel === ANIMATION_LEVEL_MIN) {
setIsReady(true);
}
}, [animationLevel]);
const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
if (e.propertyName === 'transform' && e.target === e.currentTarget) {
setIsReady(Boolean(chatId));
}
};
useEffect(() => {
if (isPrivate) {
loadUser({ userId: chatId });
@ -263,6 +278,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
<div
id="MiddleColumn"
className={className}
onTransitionEnd={handleTransitionEnd}
// @ts-ignore teact-feature
style={`
--composer-hidden-scale: ${composerHiddenScale};
@ -290,6 +306,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
chatId={renderingChatId}
threadId={renderingThreadId}
messageListType={renderingMessageListType}
isReady={isReady}
/>
<Transition
name={shouldSkipHistoryAnimations ? 'none' : animationLevel === ANIMATION_LEVEL_MAX ? 'slide' : 'fade'}
@ -307,6 +324,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
hasTools={renderingHasTools}
onFabToggle={setIsFabShown}
onNotchToggle={setIsNotchShown}
isReady={isReady}
/>
<div className={footerClassName}>
{renderingCanPost && (
@ -316,6 +334,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
messageListType={renderingMessageListType}
dropAreaState={dropAreaState}
onDropHide={handleHideDropArea}
isReady={isReady}
/>
)}
{isPinnedMessageList && (

View File

@ -73,6 +73,7 @@ type OwnProps = {
chatId: number;
threadId: number;
messageListType: MessageListType;
isReady?: boolean;
};
type StateProps = {
@ -106,6 +107,7 @@ const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
chatId,
threadId,
messageListType,
isReady,
pinnedMessageIds,
messagesById,
canUnpin,
@ -143,10 +145,10 @@ const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
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(() => {

View File

@ -244,6 +244,7 @@ const AttachmentModal: FC<OwnProps> = ({
/>
<MessageInput
id="caption-input-text"
isAttachmentModalInput
html={caption}
editableInputId={EDITABLE_INPUT_MODAL_ID}
placeholder={lang('Caption')}

View File

@ -94,6 +94,7 @@ type OwnProps = {
messageListType: MessageListType;
dropAreaState: string;
onDropHide: NoneToVoidFunction;
isReady: boolean;
};
type StateProps = {
@ -156,6 +157,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
shouldSchedule,
canScheduleUntilOnline,
onDropHide,
isReady,
editingMessage,
chatId,
threadId,
@ -225,15 +227,13 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
}, [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<OwnProps & StateProps & DispatchProps> = ({
Boolean(shouldSuggestStickers && allowedAttachmentOptions.canSendStickers && !attachments.length),
html,
stickersForEmoji,
!isReady,
);
const {
isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji,
@ -314,6 +315,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
setHtml,
baseEmojiKeywords,
emojiKeywords,
!isReady,
);
const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => {
@ -608,6 +610,8 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
}, [isRightColumnShown, closeSymbolMenu]);
useEffect(() => {
if (!isReady) return;
if (isSelectModeActive) {
disableHover();
} else {
@ -615,7 +619,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
enableHover();
}, SELECT_MODE_TRANSITION_MS);
}
}, [isSelectModeActive, enableHover, disableHover]);
}, [isSelectModeActive, enableHover, disableHover, isReady]);
const mainButtonHandler = useCallback(() => {
switch (mainButtonState) {
@ -687,7 +691,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
return (
<div className={className}>
{allowedAttachmentOptions.canAttachMedia && (
{allowedAttachmentOptions.canAttachMedia && isReady && (
<Portal containerId="#middle-column-portals">
<DropArea
isOpen={dropAreaState !== DropAreaState.None}
@ -763,7 +767,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
>
<i className="icon-smile" />
<i className="icon-keyboard" />
<Spinner color="gray" />
{!isSymbolMenuLoaded && <Spinner color="gray" />}
</Button>
) : (
<ResponsiveHoverButton

View File

@ -32,6 +32,7 @@ const TRANSITION_DURATION_FACTOR = 50;
type OwnProps = {
id: string;
isAttachmentModalInput?: boolean;
editableInputId?: string;
html: string;
placeholder: string;
@ -73,6 +74,7 @@ function clearSelection() {
const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
id,
isAttachmentModalInput,
editableInputId,
html,
placeholder,
@ -101,8 +103,9 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
const [selectedRange, setSelectedRange] = useState<Range>();
useEffect(() => {
if (!isAttachmentModalInput) return;
updateInputHeight(false);
}, []);
}, [isAttachmentModalInput]);
useLayoutEffectWithPrevDeps(([prevHtml]) => {
if (html !== inputRef.current!.innerHTML) {

View File

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

View File

@ -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<OwnProps> = ({
isOpen, allowedAttachmentOptions,
const SymbolMenu: FC<OwnProps & StateProps> = ({
isOpen, allowedAttachmentOptions, isLeftColumnShown,
onLoad, onClose,
onEmojiSelect, onStickerSelect, onGifSelect,
onRemoveSymbol, onSearchOpen, addRecentEmoji,
@ -188,6 +193,7 @@ const SymbolMenu: FC<OwnProps> = ({
const className = buildClassName(
'SymbolMenu mobile-menu',
transitionClassNames,
!isLeftColumnShown && 'middle-column-open',
);
return (
@ -216,4 +222,10 @@ const SymbolMenu: FC<OwnProps> = ({
);
};
export default memo(SymbolMenu);
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
return {
isLeftColumnShown: global.isLeftColumnShown,
};
},
)(SymbolMenu));

View File

@ -36,6 +36,7 @@ export default function useEmojiTooltip(
onUpdateHtml: (html: string) => void,
baseEmojiKeywords?: Record<string, string[]>,
emojiKeywords?: Record<string, string[]>,
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<string, Emoji[]>);
setByName(emojisByName);
setNames(Object.keys(emojisByName));
}, [baseEmojiKeywords, byId, emojiKeywords]);
}, [isDisabled, baseEmojiKeywords, byId, emojiKeywords]);
useEffect(() => {
if (!isAllowed || !html || !byId || !keywords || !keywords.length) {

View File

@ -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 <Esc>).
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [html, isSingleEmoji, clearStickersForEmoji, loadStickersForEmoji, isAllowed]);
}, [html, isSingleEmoji, clearStickersForEmoji, loadStickersForEmoji, isAllowed, isDisabled]);
return {
isStickerTooltipOpen: hasStickers,

View File

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

View File

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

View File

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

View File

@ -47,9 +47,10 @@ export function replaceChats(global: GlobalState, newById: Record<number, ApiCha
};
}
export function updateChat(
// @optimization Don't spread/unspread global for each element, do it in a batch
function getUpdatedChat(
global: GlobalState, chatId: number, chatUpdate: Partial<ApiChat>, 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<ApiChat>, 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<number, ApiChat>): GlobalState {
Object.keys(updatedById).forEach((id) => {
global = updateChat(global, Number(id), updatedById[Number(id)]);
const updatedChats = Object.keys(updatedById).map(Number).reduce<Record<number, ApiChat>>((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<number, Api
// @optimization Allows to avoid redundant updates which cause a lot of renders
export function addChats(global: GlobalState, addedById: Record<number, ApiChat>): GlobalState {
const { byId } = global.chats;
Object.keys(addedById).map(Number).forEach((id) => {
const addedChats = Object.keys(addedById).map(Number).reduce<Record<number, ApiChat>>((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;

View File

@ -13,29 +13,54 @@ export function replaceUsers(global: GlobalState, newById: Record<number, ApiUse
},
};
}
export function updateUser(global: GlobalState, userId: number, userUpdate: Partial<ApiUser>): GlobalState {
// @optimization Don't spread/unspread global for each element, do it in a batch
function getUpdatedUser(global: GlobalState, userId: number, userUpdate: Partial<ApiUser>): 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<ApiUser>): 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<number, ApiUser>): GlobalState {
Object.keys(updatedById).map(Number).forEach((id) => {
global = updateUser(global, id, updatedById[id]);
const updatedUsers = Object.keys(updatedById).map(Number).reduce<Record<number, ApiUser>>((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<number, Api
// @optimization Allows to avoid redundant updates which cause a lot of renders
export function addUsers(global: GlobalState, addedById: Record<number, ApiUser>): GlobalState {
const { byId } = global.users;
Object.keys(addedById).map(Number).forEach((id) => {
const addedUsers = Object.keys(addedById).map(Number).reduce<Record<number, ApiUser>>((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;