[Perf] Reduce unneeded renders in various components

This commit is contained in:
Alexander Zinchuk 2022-01-24 04:41:43 +01:00
parent a9e699e82f
commit 07ac02b201
21 changed files with 194 additions and 161 deletions

View File

@ -1,7 +1,7 @@
import React, {
FC, memo, useCallback, useLayoutEffect, useMemo, useRef,
} from '../../../lib/teact/teact';
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
import { getDispatch, getGlobal, withGlobal } from '../../../lib/teact/teactn';
import useLang, { LangFn } from '../../../hooks/useLang';
@ -68,7 +68,6 @@ type StateProps = {
user?: ApiUser;
userStatus?: ApiUserStatus;
actionTargetUserIds?: string[];
usersById?: Record<string, ApiUser>;
actionTargetMessage?: ApiMessage;
actionTargetChatId?: string;
lastMessageSender?: ApiUser;
@ -95,7 +94,6 @@ const Chat: FC<OwnProps & StateProps> = ({
user,
userStatus,
actionTargetUserIds,
usersById,
lastMessageSender,
lastMessageOutgoingStatus,
actionTargetMessage,
@ -132,10 +130,14 @@ const Chat: FC<OwnProps & StateProps> = ({
const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage));
const actionTargetUsers = useMemo(() => {
return actionTargetUserIds
? actionTargetUserIds.map((userId) => usersById?.[userId]).filter<ApiUser>(Boolean as any)
: undefined;
}, [actionTargetUserIds, usersById]);
if (!actionTargetUserIds) {
return undefined;
}
// No need for expensive global updates on users, so we avoid them
const usersById = getGlobal().users.byId;
return actionTargetUserIds.map((userId) => usersById[userId]).filter<ApiUser>(Boolean as any);
}, [actionTargetUserIds]);
// Sets animation excess values when `orderDiff` changes and then resets excess values to animate.
useLayoutEffect(() => {
@ -360,7 +362,6 @@ export default memo(withGlobal<OwnProps>(
: undefined;
const { targetUserIds: actionTargetUserIds, targetChatId: actionTargetChatId } = lastMessageAction || {};
const privateChatUserId = getPrivateChatUserId(chat);
const { byId: usersById } = global.users;
const {
chatId: currentChatId,
threadId: currentThreadId,
@ -386,7 +387,6 @@ export default memo(withGlobal<OwnProps>(
user: selectUser(global, privateChatUserId),
userStatus: selectUserStatus(global, privateChatUserId),
}),
...(actionTargetUserIds && { usersById }),
};
},
)(Chat));

View File

@ -85,7 +85,6 @@ type StateProps = {
isLeftColumnShown?: boolean;
isRightColumnShown?: boolean;
audioMessage?: ApiMessage;
chatsById?: Record<string, ApiChat>;
messagesCount?: number;
isChatWithSelf?: boolean;
isChatWithBot?: boolean;
@ -110,7 +109,6 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
isRightColumnShown,
audioMessage,
chat,
chatsById,
messagesCount,
isChatWithSelf,
isChatWithBot,
@ -229,12 +227,12 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
]);
const unreadCount = useMemo(() => {
if (!isLeftColumnHideable || !chatsById) {
if (!isLeftColumnHideable) {
return undefined;
}
return selectCountNotMutedUnread(getGlobal()) || undefined;
}, [isLeftColumnHideable, chatsById]);
}, [isLeftColumnHideable]);
const canToolsCollideWithChatInfo = (
windowWidth >= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN
@ -445,7 +443,6 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, messageListType }): StateProps => {
const { isLeftColumnShown, lastSyncTime, shouldSkipHistoryAnimations } = global;
const { byId: chatsById } = global.chats;
const chat = selectChat(global, chatId);
const { typingStatus } = chat || {};
@ -474,7 +471,6 @@ export default memo(withGlobal<OwnProps>(
isSelectModeActive: selectIsInSelectMode(global),
audioMessage,
chat,
chatsById,
messagesCount,
isChatWithSelf: selectIsChatWithSelf(global, chatId),
isChatWithBot: chat && selectIsChatWithBot(global, chat),

View File

@ -3,7 +3,6 @@ import React, { FC, memo, useCallback } from '../../../lib/teact/teact';
import { CONTENT_TYPES_WITH_PREVIEW } from '../../../config';
import { IS_TOUCH_ENV } from '../../../util/environment';
import { openSystemFilesDialog } from '../../../util/systemFilesDialog';
import { IAllowedAttachmentOptions } from '../../../modules/helpers';
import useMouseInside from '../../../hooks/useMouseInside';
import useLang from '../../../hooks/useLang';
@ -14,14 +13,15 @@ import './AttachMenu.scss';
export type OwnProps = {
isOpen: boolean;
allowedAttachmentOptions: IAllowedAttachmentOptions;
canAttachMedia: boolean;
canAttachPolls: boolean;
onFileSelect: (files: File[], isQuick: boolean) => void;
onPollCreate: () => void;
onClose: () => void;
};
const AttachMenu: FC<OwnProps> = ({
isOpen, allowedAttachmentOptions, onFileSelect, onPollCreate, onClose,
isOpen, canAttachMedia, canAttachPolls, onFileSelect, onPollCreate, onClose,
}) => {
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose);
@ -46,8 +46,6 @@ const AttachMenu: FC<OwnProps> = ({
const lang = useLang();
const { canAttachMedia, canAttachPolls } = allowedAttachmentOptions;
return (
<Menu
isOpen={isOpen}

View File

@ -2,7 +2,7 @@ import React, {
FC, memo, useCallback, useEffect, useRef,
} from '../../../lib/teact/teact';
import { ApiAttachment, ApiChatMember, ApiUser } from '../../../api/types';
import { ApiAttachment, ApiChatMember } from '../../../api/types';
import {
CONTENT_TYPES_WITH_PREVIEW,
@ -17,6 +17,7 @@ import useMentionTooltip from './hooks/useMentionTooltip';
import useEmojiTooltip from './hooks/useEmojiTooltip';
import useLang from '../../../hooks/useLang';
import useFlag from '../../../hooks/useFlag';
import { useStateRef } from '../../../hooks/useStateRef';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
@ -35,7 +36,6 @@ export type OwnProps = {
isReady?: boolean;
currentUserId?: string;
groupChatMembers?: ApiChatMember[];
usersById?: Record<string, ApiUser>;
recentEmojis: string[];
baseEmojiKeywords?: Record<string, string[]>;
emojiKeywords?: Record<string, string[]>;
@ -56,7 +56,6 @@ const AttachmentModal: FC<OwnProps> = ({
isReady,
currentUserId,
groupChatMembers,
usersById,
recentEmojis,
baseEmojiKeywords,
emojiKeywords,
@ -66,8 +65,8 @@ const AttachmentModal: FC<OwnProps> = ({
onFileAppend,
onClear,
}) => {
// eslint-disable-next-line no-null/no-null
const hideTimeoutRef = useRef<number>(null);
const captionRef = useStateRef(caption);
const hideTimeoutRef = useRef<number>();
const prevAttachments = usePrevious(attachments);
const renderingAttachments = attachments.length ? attachments : prevAttachments;
const isOpen = Boolean(attachments.length);
@ -79,7 +78,7 @@ const AttachmentModal: FC<OwnProps> = ({
isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers,
} = useMentionTooltip(
isOpen,
caption,
captionRef,
onCaptionUpdate,
EDITABLE_INPUT_MODAL_ID,
groupChatMembers,
@ -90,7 +89,7 @@ const AttachmentModal: FC<OwnProps> = ({
isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji,
} = useEmojiTooltip(
isOpen,
caption,
captionRef,
recentEmojis,
EDITABLE_INPUT_MODAL_ID,
onCaptionUpdate,
@ -150,6 +149,7 @@ const AttachmentModal: FC<OwnProps> = ({
if (hideTimeoutRef.current) {
window.clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = undefined;
}
}
@ -238,7 +238,6 @@ const AttachmentModal: FC<OwnProps> = ({
onClose={closeMentionTooltip}
onInsertUserName={insertMention}
filteredUsers={mentionFilteredUsers}
usersById={usersById}
/>
<EmojiTooltip
isOpen={isEmojiTooltipOpen}

View File

@ -25,6 +25,7 @@ import {
BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_ID, REPLIES_USER_ID, SCHEDULED_WHEN_ONLINE, SEND_MESSAGE_ACTION_INTERVAL,
} from '../../../config';
import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import {
selectChat,
selectIsRightColumnShown,
@ -37,6 +38,7 @@ import {
selectChatBot,
selectChatUser,
selectChatMessage,
selectUser,
selectUserStatus,
} from '../../../modules/selectors';
import {
@ -67,6 +69,7 @@ import useLang from '../../../hooks/useLang';
import useSendMessageAction from '../../../hooks/useSendMessageAction';
import useInterval from '../../../hooks/useInterval';
import useOnChange from '../../../hooks/useOnChange';
import { useStateRef } from '../../../hooks/useStateRef';
import useVoiceRecording from './hooks/useVoiceRecording';
import useClipboardPaste from './hooks/useClipboardPaste';
import useDraft from './hooks/useDraft';
@ -80,6 +83,8 @@ import DeleteMessageModal from '../../common/DeleteMessageModal.async';
import Button from '../../ui/Button';
import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
import Spinner from '../../ui/Spinner';
import CalendarModal from '../../common/CalendarModal.async';
import Avatar from '../../common/Avatar';
import AttachMenu from './AttachMenu.async';
import SymbolMenu from './SymbolMenu.async';
import InlineBotTooltip from './InlineBotTooltip.async';
@ -96,10 +101,7 @@ import BotCommandMenu from './BotCommandMenu.async';
import PollModal from './PollModal.async';
import DropArea, { DropAreaState } from './DropArea.async';
import WebPagePreview from './WebPagePreview';
import Portal from '../../ui/Portal';
import CalendarModal from '../../common/CalendarModal.async';
import SendAsMenu from './SendAsMenu.async';
import Avatar from '../../common/Avatar';
import './Composer.scss';
@ -131,7 +133,6 @@ type StateProps =
stickersForEmoji?: ApiSticker[];
groupChatMembers?: ApiChatMember[];
currentUserId?: string;
usersById?: Record<string, ApiUser>;
recentEmojis: string[];
lastSyncTime?: number;
contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled'];
@ -195,7 +196,6 @@ const Composer: FC<OwnProps & StateProps> = ({
groupChatMembers,
topInlineBotIds,
currentUserId,
usersById,
lastSyncTime,
contentToBeScheduled,
shouldSuggestStickers,
@ -231,6 +231,7 @@ const Composer: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line no-null/no-null
const appendixRef = useRef<HTMLDivElement>(null);
const [html, setHtml] = useState<string>('');
const htmlRef = useStateRef(html);
const lastMessageSendTimeSeconds = useRef<number>();
const prevDropAreaState = usePrevious(dropAreaState);
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
@ -241,12 +242,6 @@ const Composer: FC<OwnProps & StateProps> = ({
const sendAsIds = chat?.sendAsIds;
const sendMessageAction = useSendMessageAction(chatId, threadId);
// Cache for frequently updated state
const htmlRef = useRef<string>(html);
useEffect(() => {
htmlRef.current = html;
}, [html]);
useEffect(() => {
lastMessageSendTimeSeconds.current = undefined;
}, [chatId]);
@ -323,7 +318,7 @@ const Composer: FC<OwnProps & StateProps> = ({
isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers,
} = useMentionTooltip(
!attachments.length,
html,
htmlRef,
setHtml,
undefined,
groupChatMembers,
@ -365,15 +360,15 @@ const Composer: FC<OwnProps & StateProps> = ({
handleContextMenuHide,
} = useContextMenuHandlers(mainButtonRef, !(mainButtonState === MainButtonState.Send && canShowCustomSendMenu));
const allowedAttachmentOptions = useMemo(() => {
return getAllowedAttachmentOptions(chat, isChatWithBot);
}, [chat, isChatWithBot]);
const {
canSendStickers, canSendGifs, canAttachMedia, canAttachPolls, canAttachEmbedLinks,
} = useMemo(() => getAllowedAttachmentOptions(chat, isChatWithBot), [chat, isChatWithBot]);
const isAdmin = chat && isChatAdmin(chat);
const slowMode = getChatSlowModeOptions(chat);
const { isStickerTooltipOpen, closeStickerTooltip } = useStickerTooltip(
Boolean(shouldSuggestStickers && allowedAttachmentOptions.canSendStickers && !attachments.length),
Boolean(shouldSuggestStickers && canSendStickers && !attachments.length),
html,
stickersForEmoji,
!isReady,
@ -381,8 +376,8 @@ const Composer: FC<OwnProps & StateProps> = ({
const {
isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji,
} = useEmojiTooltip(
Boolean(shouldSuggestStickers && allowedAttachmentOptions.canSendStickers && !attachments.length),
html,
Boolean(shouldSuggestStickers && canSendStickers && !attachments.length),
htmlRef,
recentEmojis,
undefined,
setHtml,
@ -413,7 +408,7 @@ const Composer: FC<OwnProps & StateProps> = ({
requestAnimationFrame(() => {
focusEditableElement(messageInput);
});
}, []);
}, [htmlRef]);
const removeSymbol = useCallback(() => {
const selection = window.getSelection()!;
@ -427,13 +422,13 @@ const Composer: FC<OwnProps & StateProps> = ({
}
setHtml(deleteLastCharacterOutsideSelection(htmlRef.current!));
}, []);
}, [htmlRef]);
const resetComposer = useCallback((shouldPreserveInput = false) => {
if (!shouldPreserveInput) {
setHtml('');
}
setAttachments([]);
setAttachments(MEMO_EMPTY_ARRAY);
closeStickerTooltip();
closeCalendar();
setScheduledMessageArgs(undefined);
@ -459,7 +454,7 @@ const Composer: FC<OwnProps & StateProps> = ({
}, [chatId, resetComposer, stopRecordingVoiceRef]);
const handleEditComplete = useEditing(htmlRef, setHtml, editingMessage, resetComposer, openDeleteModal);
useDraft(draft, chatId, threadId, html, htmlRef, setHtml, editingMessage);
useDraft(draft, chatId, threadId, htmlRef, setHtml, editingMessage);
useClipboardPaste(insertTextAndUpdateCursor, setAttachments, editingMessage);
const handleFileSelect = useCallback(async (files: File[], isQuick: boolean) => {
@ -474,7 +469,7 @@ const Composer: FC<OwnProps & StateProps> = ({
}, [attachments]);
const handleClearAttachment = useCallback(() => {
setAttachments([]);
setAttachments(MEMO_EMPTY_ARRAY);
}, []);
const handleSend = useCallback(async (isSilent = false, scheduledAt?: number) => {
@ -580,7 +575,7 @@ const Composer: FC<OwnProps & StateProps> = ({
});
}, [
connectionState, attachments, activeVoiceRecording, isForwarding, clearDraft, chatId, serverTimeOffset,
resetComposer, stopRecordingVoice, showDialog, slowMode, isAdmin, sendMessage, forwardMessages, lang,
resetComposer, stopRecordingVoice, showDialog, slowMode, isAdmin, sendMessage, forwardMessages, lang, htmlRef,
]);
const handleActivateBotCommandMenu = useCallback(() => {
@ -791,8 +786,7 @@ const Composer: FC<OwnProps & StateProps> = ({
activeVoiceRecording, openCalendar, pauseRecordingVoice,
]);
const areVoiceMessagesNotAllowed = mainButtonState === MainButtonState.Record
&& !allowedAttachmentOptions.canAttachMedia;
const areVoiceMessagesNotAllowed = mainButtonState === MainButtonState.Record && !canAttachMedia;
const prevEditedMessage = usePrevious(editingMessage, true);
const renderedEditedMessage = editingMessage || prevEditedMessage;
@ -836,15 +830,13 @@ const Composer: FC<OwnProps & StateProps> = ({
return (
<div className={className}>
{allowedAttachmentOptions.canAttachMedia && isReady && (
<Portal containerId="#middle-column-portals">
<DropArea
isOpen={dropAreaState !== DropAreaState.None}
withQuick={[dropAreaState, prevDropAreaState].includes(DropAreaState.QuickFile)}
onHide={onDropHide}
onFileSelect={handleFileSelect}
/>
</Portal>
{canAttachMedia && isReady && (
<DropArea
isOpen={dropAreaState !== DropAreaState.None}
withQuick={dropAreaState === DropAreaState.QuickFile || prevDropAreaState === DropAreaState.QuickFile}
onHide={onDropHide}
onFileSelect={handleFileSelect}
/>
)}
<AttachmentModal
chatId={chatId}
@ -853,7 +845,6 @@ const Composer: FC<OwnProps & StateProps> = ({
caption={attachments.length ? html : ''}
groupChatMembers={groupChatMembers}
currentUserId={currentUserId}
usersById={usersById}
recentEmojis={recentEmojis}
isReady={isReady}
onCaptionUpdate={setHtml}
@ -889,12 +880,10 @@ const Composer: FC<OwnProps & StateProps> = ({
onClose={closeMentionTooltip}
onInsertUserName={insertMention}
filteredUsers={mentionFilteredUsers}
usersById={usersById}
/>
<InlineBotTooltip
isOpen={isInlineBotTooltipOpen}
botId={inlineBotId}
allowedAttachmentOptions={allowedAttachmentOptions}
isGallery={isInlineBotTooltipGallery}
inlineBotResults={inlineBotResults}
switchPm={inlineBotSwitchPm}
@ -916,7 +905,7 @@ const Composer: FC<OwnProps & StateProps> = ({
chatId={chatId}
threadId={threadId}
messageText={!attachments.length ? html : ''}
disabled={!allowedAttachmentOptions.canAttachEmbedLinks}
disabled={!canAttachEmbedLinks}
/>
<div className="message-input-wrapper">
{isChatWithBot && botCommands !== false && !activeVoiceRecording && !editingMessage && (
@ -1044,7 +1033,8 @@ const Composer: FC<OwnProps & StateProps> = ({
/>
<AttachMenu
isOpen={isAttachMenuOpen}
allowedAttachmentOptions={allowedAttachmentOptions}
canAttachMedia={canAttachMedia}
canAttachPolls={canAttachPolls}
onFileSelect={handleFileSelect}
onPollCreate={openPollModal}
onClose={closeAttachMenu}
@ -1067,7 +1057,8 @@ const Composer: FC<OwnProps & StateProps> = ({
chatId={chatId}
threadId={threadId}
isOpen={isSymbolMenuOpen}
allowedAttachmentOptions={allowedAttachmentOptions}
canSendGifs={canSendGifs}
canSendStickers={canSendStickers}
onLoad={onSymbolMenuLoadingComplete}
onClose={closeSymbolMenu}
onEmojiSelect={insertTextAndUpdateCursor}
@ -1145,12 +1136,10 @@ export default memo(withGlobal<OwnProps>(
const emojiKeywords = language !== BASE_EMOJI_KEYWORD_LANG ? global.emojiKeywords[language] : undefined;
const botKeyboardMessageId = messageWithActualBotKeyboard ? messageWithActualBotKeyboard.id : undefined;
const keyboardMessage = botKeyboardMessageId ? selectChatMessage(global, chatId, botKeyboardMessageId) : undefined;
const usersById = global.users.byId;
const chatsById = global.chats.byId;
const { currentUserId } = global;
const sendAsId = chat?.fullInfo ? chat?.fullInfo?.sendAsId || currentUserId : undefined;
const sendAsUser = sendAsId ? usersById?.[sendAsId] : undefined;
const sendAsChat = !sendAsUser && sendAsId ? chatsById?.[sendAsId] : undefined;
const sendAsUser = sendAsId ? selectUser(global, sendAsId) : undefined;
const sendAsChat = !sendAsUser && sendAsId ? selectChat(global, sendAsId) : undefined;
return {
editingMessage: selectEditingMessage(global, chatId, threadId, messageListType),
@ -1179,7 +1168,6 @@ export default memo(withGlobal<OwnProps>(
groupChatMembers: chat?.fullInfo?.members,
topInlineBotIds: global.topInlineBots?.userIds,
currentUserId,
usersById,
lastSyncTime: global.lastSyncTime,
contentToBeScheduled: global.messages.contentToBeScheduled,
shouldSuggestStickers,

View File

@ -8,6 +8,7 @@ import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
import usePrevious from '../../../hooks/usePrevious';
import Portal from '../../ui/Portal';
import DropTarget from './DropTarget';
import './DropArea.scss';
@ -84,10 +85,12 @@ const DropArea: FC<OwnProps> = ({
);
return (
<div className={className} onDragLeave={handleDragLeave} onDragOver={handleDragOver} onDrop={onHide}>
<DropTarget onFileSelect={handleFilesDrop} />
{(withQuick || prevWithQuick) && <DropTarget onFileSelect={handleQuickFilesDrop} isQuick />}
</div>
<Portal containerId="#middle-column-portals">
<div className={className} onDragLeave={handleDragLeave} onDragOver={handleDragOver} onDrop={onHide}>
<DropTarget onFileSelect={handleFilesDrop} />
{(withQuick || prevWithQuick) && <DropTarget onFileSelect={handleQuickFilesDrop} isQuick />}
</div>
</Portal>
);
};

View File

@ -3,7 +3,6 @@ import React, {
} from '../../../lib/teact/teact';
import { ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm } from '../../../api/types';
import { IAllowedAttachmentOptions } from '../../../modules/helpers';
import { LoadMoreDirection } from '../../../types';
import { IS_TOUCH_ENV } from '../../../util/environment';
@ -32,7 +31,6 @@ export type OwnProps = {
isOpen: boolean;
botId?: string;
isGallery?: boolean;
allowedAttachmentOptions: IAllowedAttachmentOptions;
inlineBotResults?: (ApiBotInlineResult | ApiBotInlineMediaResult)[];
switchPm?: ApiBotInlineSwitchPm;
onSelectResult: (inlineResult: ApiBotInlineMediaResult | ApiBotInlineResult) => void;

View File

@ -1,13 +1,14 @@
import React, {
FC, useCallback, useEffect, useRef, memo,
} from '../../../lib/teact/teact';
import usePrevious from '../../../hooks/usePrevious';
import { getGlobal } from '../../../lib/teact/teactn';
import { ApiUser } from '../../../api/types';
import useShowTransition from '../../../hooks/useShowTransition';
import buildClassName from '../../../util/buildClassName';
import setTooltipItemVisible from '../../../util/setTooltipItemVisible';
import usePrevious from '../../../hooks/usePrevious';
import useShowTransition from '../../../hooks/useShowTransition';
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation';
import ListItem from '../../ui/ListItem';
@ -20,14 +21,12 @@ export type OwnProps = {
onClose: () => void;
onInsertUserName: (user: ApiUser, forceFocus?: boolean) => void;
filteredUsers?: ApiUser[];
usersById?: Record<string, ApiUser>;
};
const MentionTooltip: FC<OwnProps> = ({
isOpen,
onClose,
onInsertUserName,
usersById,
filteredUsers,
}) => {
// eslint-disable-next-line no-null/no-null
@ -35,13 +34,15 @@ const MentionTooltip: FC<OwnProps> = ({
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
const handleUserSelect = useCallback((userId: string, forceFocus = false) => {
const user = usersById?.[userId];
// No need for expensive global updates on users, so we avoid them
const usersById = getGlobal().users.byId;
const user = usersById[userId];
if (!user) {
return;
}
onInsertUserName(user, forceFocus);
}, [usersById, onInsertUserName]);
}, [onInsertUserName]);
const handleSelectMention = useCallback((member: ApiUser) => {
handleUserSelect(member.id, true);

View File

@ -1,4 +1,4 @@
import React, { FC } from '../../../lib/teact/teact';
import React, { FC, memo } from '../../../lib/teact/teact';
import { OwnProps } from './StickerTooltip';
import { Bundles } from '../../../util/moduleLoader';
@ -12,4 +12,4 @@ const StickerTooltipAsync: FC<OwnProps> = (props) => {
return StickerTooltip ? <StickerTooltip {...props} /> : undefined;
};
export default StickerTooltipAsync;
export default memo(StickerTooltipAsync);

View File

@ -5,7 +5,6 @@ import { withGlobal } from '../../../lib/teact/teactn';
import { ApiSticker, ApiVideo } from '../../../api/types';
import { IAllowedAttachmentOptions } from '../../../modules/helpers';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../util/environment';
import { fastRaf } from '../../../util/schedulers';
import buildClassName from '../../../util/buildClassName';
@ -30,7 +29,8 @@ export type OwnProps = {
chatId: string;
threadId?: number;
isOpen: boolean;
allowedAttachmentOptions: IAllowedAttachmentOptions;
canSendStickers: boolean;
canSendGifs: boolean;
onLoad: () => void;
onClose: () => void;
onEmojiSelect: (emoji: string) => void;
@ -51,7 +51,8 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
chatId,
threadId,
isOpen,
allowedAttachmentOptions,
canSendStickers,
canSendGifs,
isLeftColumnShown,
onLoad,
onClose,
@ -131,8 +132,6 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
const lang = useLang();
const { canSendStickers, canSendGifs } = allowedAttachmentOptions;
function renderContent(isActive: boolean, isFrom: boolean) {
switch (activeTab) {
case SymbolMenuTabs.Emoji:

View File

@ -21,7 +21,6 @@ export default (
draft: ApiFormattedText | undefined,
chatId: string,
threadId: number,
html: string,
htmlRef: { current: string },
setHtml: (html: string) => void,
editedMessage: ApiMessage | undefined,
@ -29,8 +28,9 @@ export default (
const { saveDraft, clearDraft } = getDispatch();
const updateDraft = useCallback((draftChatId: string, draftThreadId: number) => {
if (htmlRef.current.length && !editedMessage) {
saveDraft({ chatId: draftChatId, threadId: draftThreadId, draft: parseMessageInput(htmlRef.current!) });
const currentHtml = htmlRef.current;
if (currentHtml.length && !editedMessage) {
saveDraft({ chatId: draftChatId, threadId: draftThreadId, draft: parseMessageInput(currentHtml!) });
} else {
clearDraft({ chatId: draftChatId, threadId: draftThreadId });
}
@ -75,6 +75,7 @@ export default (
}
}, [chatId, threadId, draft, setHtml, updateDraft, prevChatId, prevThreadId]);
const html = htmlRef.current;
// Update draft when input changes
const prevHtml = usePrevious(html);
useEffect(() => {

View File

@ -43,7 +43,7 @@ try {
export default function useEmojiTooltip(
isAllowed: boolean,
html: string,
htmlRef: { current: string },
recentEmojiIds: string[],
inputId = EDITABLE_INPUT_ID,
onUpdateHtml: (html: string) => void,
@ -71,6 +71,7 @@ export default function useEmojiTooltip(
}
}, [isDisabled]);
const html = htmlRef.current;
useEffect(() => {
if (!isAllowed || !html || !byId || isDisabled) {
unmarkIsOpen();
@ -111,9 +112,10 @@ export default function useEmojiTooltip(
]);
const insertEmoji = useCallback((textEmoji: string, isForce?: boolean) => {
const atIndex = html.lastIndexOf(':', isForce ? html.lastIndexOf(':') - 1 : undefined);
const currentHtml = htmlRef.current;
const atIndex = currentHtml.lastIndexOf(':', isForce ? currentHtml.lastIndexOf(':') - 1 : undefined);
if (atIndex !== -1) {
onUpdateHtml(`${html.substr(0, atIndex)}${textEmoji}`);
onUpdateHtml(`${currentHtml.substr(0, atIndex)}${textEmoji}`);
const messageInput = document.getElementById(inputId)!;
requestAnimationFrame(() => {
focusEditableElement(messageInput, true);
@ -121,7 +123,7 @@ export default function useEmojiTooltip(
}
unmarkIsOpen();
}, [html, inputId, onUpdateHtml, unmarkIsOpen]);
}, [htmlRef, inputId, onUpdateHtml, unmarkIsOpen]);
useEffect(() => {
if (isOpen && shouldForceInsertEmoji && filteredEmojis.length) {

View File

@ -8,6 +8,12 @@ import useDebouncedMemo from '../../../../hooks/useDebouncedMemo';
const DEBOUNCE_MS = 300;
const INLINE_BOT_QUERY_REGEXP = /^@([a-z0-9_]{1,32})[\u00A0\u0020]+(.*)/i;
const HAS_NEW_LINE = /^@([a-z0-9_]{1,32})[\u00A0\u0020]+\n{2,}/i;
const MEMO_NO_RESULT = {
username: '',
query: '',
canShowHelp: false,
usernameLowered: '',
};
const tempEl = document.createElement('div');
@ -81,12 +87,7 @@ function parseBotQuery(html: string) {
const text = getPlainText(html);
const result = text.match(INLINE_BOT_QUERY_REGEXP);
if (!result) {
return {
username: '',
query: '',
canShowHelp: false,
usernameLowered: '',
};
return MEMO_NO_RESULT;
}
return {

View File

@ -24,7 +24,7 @@ try {
export default function useMentionTooltip(
canSuggestMembers: boolean | undefined,
html: string,
htmlRef: { current: string },
onUpdateHtml: (html: string) => void,
inputId: string = EDITABLE_INPUT_ID,
groupChatMembers?: ApiChatMember[],
@ -62,6 +62,7 @@ export default function useMentionTooltip(
});
}, [currentUserId, groupChatMembers, topInlineBotIds]);
const html = htmlRef.current;
useEffect(() => {
if (!canSuggestMembers || !html.length) {
unmarkIsOpen();
@ -76,7 +77,7 @@ export default function useMentionTooltip(
} else {
unmarkIsOpen();
}
}, [canSuggestMembers, html, updateFilteredUsers, markIsOpen, unmarkIsOpen]);
}, [canSuggestMembers, updateFilteredUsers, markIsOpen, unmarkIsOpen, html]);
useEffect(() => {
if (usersToMention?.length) {
@ -101,9 +102,10 @@ export default function useMentionTooltip(
dir="auto"
>${getUserFirstOrLastName(user)}</a>`;
const atIndex = html.lastIndexOf('@');
const currentHtml = htmlRef.current;
const atIndex = currentHtml.lastIndexOf('@');
if (atIndex !== -1) {
onUpdateHtml(`${html.substr(0, atIndex)}${insertedHtml}&nbsp;`);
onUpdateHtml(`${currentHtml.substr(0, atIndex)}${insertedHtml}&nbsp;`);
const messageInput = document.getElementById(inputId)!;
requestAnimationFrame(() => {
focusEditableElement(messageInput, forceFocus);
@ -111,7 +113,7 @@ export default function useMentionTooltip(
}
unmarkIsOpen();
}, [html, inputId, onUpdateHtml, unmarkIsOpen]);
}, [htmlRef, inputId, onUpdateHtml, unmarkIsOpen]);
return {
isMentionTooltipOpen: isOpen,

View File

@ -45,29 +45,27 @@ interface OwnProps {
onSecondaryIconClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}
const ListItem: FC<OwnProps> = (props) => {
const {
ref,
buttonRef,
icon,
secondaryIcon,
className,
style,
children,
disabled,
ripple,
narrow,
inactive,
focus,
destructive,
multiline,
isStatic,
contextActions,
onMouseDown,
onClick,
onSecondaryIconClick,
} = props;
const ListItem: FC<OwnProps> = ({
ref,
buttonRef,
icon,
secondaryIcon,
className,
style,
children,
disabled,
ripple,
narrow,
inactive,
focus,
destructive,
multiline,
isStatic,
contextActions,
onMouseDown,
onClick,
onSecondaryIconClick,
}) => {
// eslint-disable-next-line no-null/no-null
let containerRef = useRef<HTMLDivElement>(null);
if (ref) {

View File

@ -12,10 +12,13 @@ export default <B extends Bundles, M extends BundleModules<B>>(
const module = getModuleFromMemory(bundleName, moduleName);
const forceUpdate = useForceUpdate();
if (autoUpdate) {
// Use effect and cleanup for listener removal
addLoadListener(forceUpdate);
}
useEffect(() => {
if (!autoUpdate) {
return undefined;
}
return addLoadListener(forceUpdate);
}, [autoUpdate, forceUpdate]);
useEffect(() => {
if (!noLoad && !module) {

13
src/hooks/useStateRef.ts Normal file
View File

@ -0,0 +1,13 @@
import { useEffect, useRef } from '../lib/teact/teact';
// Allows to use state value as "silent" dependency in hooks (not causing updates).
// Useful for state values that update frequently (such as controlled input value).
export function useStateRef<T>(value: T) {
const ref = useRef<T>(value);
useEffect(() => {
ref.current = value;
}, [value]);
return ref;
}

View File

@ -100,6 +100,7 @@ const Fragment = Symbol('Fragment');
const DEBUG_RENDER_THRESHOLD = 7;
const DEBUG_EFFECT_THRESHOLD = 7;
const DEBUG_SILENT_RENDERS_FOR = new Set(['TeactMemoWrapper', 'TeactNContainer', 'Button', 'ListItem', 'MenuItem']);
let renderingInstance: ComponentInstance;
@ -276,7 +277,7 @@ export function renderComponent(componentInstance: ComponentInstance) {
}
if (DEBUG_MORE) {
if (componentName !== 'TeactMemoWrapper' && componentName !== 'TeactNContainer') {
if (!DEBUG_SILENT_RENDERS_FOR.has(componentName)) {
// eslint-disable-next-line no-console
console.log(`[Teact] Render ${componentName}`);
}

View File

@ -5,6 +5,7 @@ import {
} from '../../../api/types';
import { unique } from '../../../util/iteratees';
import { areDeepEqual } from '../../../util/areDeepEqual';
import {
updateChat,
deleteChatMessages,
@ -468,14 +469,17 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
}
case 'updateMessageReactions': {
setGlobal(updateChatMessage(
global,
update.chatId,
update.id,
{
reactions: update.reactions,
},
));
const { chatId, id, reactions } = update;
const message = selectChatMessage(global, chatId, id);
const currentReactions = message?.reactions;
// `updateMessageReactions` happens with an interval so we try to avoid redundant global state updates
if (currentReactions && areDeepEqual(reactions, currentReactions)) {
return;
}
setGlobal(updateChatMessage(global, chatId, id, { reactions: update.reactions }));
break;
}
}

35
src/util/areDeepEqual.ts Normal file
View File

@ -0,0 +1,35 @@
export function areDeepEqual<T extends any>(value1: T, value2: T): boolean {
const type1 = typeof value1;
const type2 = typeof value2;
if (type1 !== type2) {
return false;
}
if (type1 !== 'object') {
return value1 === value2;
}
const isArray1 = Array.isArray(value1);
const isArray2 = Array.isArray(value2);
if (isArray1 !== isArray2) {
return false;
}
if (isArray1) {
const array1 = value1 as any[];
const array2 = value2 as any[];
if (array1.length !== array2.length) {
return false;
}
return array1.every((member1, i) => areDeepEqual(member1, array2[i]));
}
const object1 = value1 as AnyLiteral;
const object2 = value1 as AnyLiteral;
const keys1 = Object.keys(object1);
return keys1.every((key1) => areDeepEqual(object1[key1], object2[key1]));
}

View File

@ -1,4 +1,5 @@
import { DEBUG } from '../config';
import { createCallbackManager } from './callbacks';
export enum Bundles {
Auth,
@ -23,6 +24,8 @@ export type BundleModules<B extends keyof ImportedBundles> = keyof ImportedBundl
const LOAD_PROMISES: Partial<BundlePromises> = {};
const MEMORY_CACHE: Partial<ImportedBundles> = {};
const { addCallback, runCallbacks } = createCallbackManager();
export async function loadModule<B extends Bundles, M extends BundleModules<B>>(bundleName: B, moduleName: M) {
if (!LOAD_PROMISES[bundleName]) {
switch (bundleName) {
@ -45,7 +48,7 @@ export async function loadModule<B extends Bundles, M extends BundleModules<B>>(
break;
}
(LOAD_PROMISES[bundleName] as Promise<ImportedBundles[B]>).then(handleBundleLoad);
(LOAD_PROMISES[bundleName] as Promise<ImportedBundles[B]>).then(runCallbacks);
}
const bundle = (await LOAD_PROMISES[bundleName]) as unknown as ImportedBundles[B];
@ -67,16 +70,4 @@ export function getModuleFromMemory<B extends Bundles, M extends BundleModules<B
return bundle[moduleName];
}
const listeners: NoneToVoidFunction[] = [];
export function addLoadListener(listener: NoneToVoidFunction) {
if (!listeners.includes(listener)) {
listeners.push(listener);
}
}
function handleBundleLoad() {
listeners.forEach((listener) => {
listener();
});
}
export const addLoadListener = addCallback;