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

View File

@ -85,7 +85,6 @@ type StateProps = {
isLeftColumnShown?: boolean; isLeftColumnShown?: boolean;
isRightColumnShown?: boolean; isRightColumnShown?: boolean;
audioMessage?: ApiMessage; audioMessage?: ApiMessage;
chatsById?: Record<string, ApiChat>;
messagesCount?: number; messagesCount?: number;
isChatWithSelf?: boolean; isChatWithSelf?: boolean;
isChatWithBot?: boolean; isChatWithBot?: boolean;
@ -110,7 +109,6 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
isRightColumnShown, isRightColumnShown,
audioMessage, audioMessage,
chat, chat,
chatsById,
messagesCount, messagesCount,
isChatWithSelf, isChatWithSelf,
isChatWithBot, isChatWithBot,
@ -229,12 +227,12 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
]); ]);
const unreadCount = useMemo(() => { const unreadCount = useMemo(() => {
if (!isLeftColumnHideable || !chatsById) { if (!isLeftColumnHideable) {
return undefined; return undefined;
} }
return selectCountNotMutedUnread(getGlobal()) || undefined; return selectCountNotMutedUnread(getGlobal()) || undefined;
}, [isLeftColumnHideable, chatsById]); }, [isLeftColumnHideable]);
const canToolsCollideWithChatInfo = ( const canToolsCollideWithChatInfo = (
windowWidth >= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN windowWidth >= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN
@ -445,7 +443,6 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, messageListType }): StateProps => { (global, { chatId, threadId, messageListType }): StateProps => {
const { isLeftColumnShown, lastSyncTime, shouldSkipHistoryAnimations } = global; const { isLeftColumnShown, lastSyncTime, shouldSkipHistoryAnimations } = global;
const { byId: chatsById } = global.chats;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const { typingStatus } = chat || {}; const { typingStatus } = chat || {};
@ -474,7 +471,6 @@ export default memo(withGlobal<OwnProps>(
isSelectModeActive: selectIsInSelectMode(global), isSelectModeActive: selectIsInSelectMode(global),
audioMessage, audioMessage,
chat, chat,
chatsById,
messagesCount, messagesCount,
isChatWithSelf: selectIsChatWithSelf(global, chatId), isChatWithSelf: selectIsChatWithSelf(global, chatId),
isChatWithBot: chat && selectIsChatWithBot(global, chat), 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 { CONTENT_TYPES_WITH_PREVIEW } from '../../../config';
import { IS_TOUCH_ENV } from '../../../util/environment'; import { IS_TOUCH_ENV } from '../../../util/environment';
import { openSystemFilesDialog } from '../../../util/systemFilesDialog'; import { openSystemFilesDialog } from '../../../util/systemFilesDialog';
import { IAllowedAttachmentOptions } from '../../../modules/helpers';
import useMouseInside from '../../../hooks/useMouseInside'; import useMouseInside from '../../../hooks/useMouseInside';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
@ -14,14 +13,15 @@ import './AttachMenu.scss';
export type OwnProps = { export type OwnProps = {
isOpen: boolean; isOpen: boolean;
allowedAttachmentOptions: IAllowedAttachmentOptions; canAttachMedia: boolean;
canAttachPolls: boolean;
onFileSelect: (files: File[], isQuick: boolean) => void; onFileSelect: (files: File[], isQuick: boolean) => void;
onPollCreate: () => void; onPollCreate: () => void;
onClose: () => void; onClose: () => void;
}; };
const AttachMenu: FC<OwnProps> = ({ const AttachMenu: FC<OwnProps> = ({
isOpen, allowedAttachmentOptions, onFileSelect, onPollCreate, onClose, isOpen, canAttachMedia, canAttachPolls, onFileSelect, onPollCreate, onClose,
}) => { }) => {
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose); const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose);
@ -46,8 +46,6 @@ const AttachMenu: FC<OwnProps> = ({
const lang = useLang(); const lang = useLang();
const { canAttachMedia, canAttachPolls } = allowedAttachmentOptions;
return ( return (
<Menu <Menu
isOpen={isOpen} isOpen={isOpen}

View File

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

View File

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

View File

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

View File

@ -1,13 +1,14 @@
import React, { import React, {
FC, useCallback, useEffect, useRef, memo, FC, useCallback, useEffect, useRef, memo,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import usePrevious from '../../../hooks/usePrevious'; import { getGlobal } from '../../../lib/teact/teactn';
import { ApiUser } from '../../../api/types'; import { ApiUser } from '../../../api/types';
import useShowTransition from '../../../hooks/useShowTransition';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import setTooltipItemVisible from '../../../util/setTooltipItemVisible'; import setTooltipItemVisible from '../../../util/setTooltipItemVisible';
import usePrevious from '../../../hooks/usePrevious';
import useShowTransition from '../../../hooks/useShowTransition';
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'; import { useKeyboardNavigation } from './hooks/useKeyboardNavigation';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
@ -20,14 +21,12 @@ export type OwnProps = {
onClose: () => void; onClose: () => void;
onInsertUserName: (user: ApiUser, forceFocus?: boolean) => void; onInsertUserName: (user: ApiUser, forceFocus?: boolean) => void;
filteredUsers?: ApiUser[]; filteredUsers?: ApiUser[];
usersById?: Record<string, ApiUser>;
}; };
const MentionTooltip: FC<OwnProps> = ({ const MentionTooltip: FC<OwnProps> = ({
isOpen, isOpen,
onClose, onClose,
onInsertUserName, onInsertUserName,
usersById,
filteredUsers, filteredUsers,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // 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 { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
const handleUserSelect = useCallback((userId: string, forceFocus = 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) { if (!user) {
return; return;
} }
onInsertUserName(user, forceFocus); onInsertUserName(user, forceFocus);
}, [usersById, onInsertUserName]); }, [onInsertUserName]);
const handleSelectMention = useCallback((member: ApiUser) => { const handleSelectMention = useCallback((member: ApiUser) => {
handleUserSelect(member.id, true); 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 { OwnProps } from './StickerTooltip';
import { Bundles } from '../../../util/moduleLoader'; import { Bundles } from '../../../util/moduleLoader';
@ -12,4 +12,4 @@ const StickerTooltipAsync: FC<OwnProps> = (props) => {
return StickerTooltip ? <StickerTooltip {...props} /> : undefined; 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 { ApiSticker, ApiVideo } from '../../../api/types';
import { IAllowedAttachmentOptions } from '../../../modules/helpers';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../util/environment'; import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../util/environment';
import { fastRaf } from '../../../util/schedulers'; import { fastRaf } from '../../../util/schedulers';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -30,7 +29,8 @@ export type OwnProps = {
chatId: string; chatId: string;
threadId?: number; threadId?: number;
isOpen: boolean; isOpen: boolean;
allowedAttachmentOptions: IAllowedAttachmentOptions; canSendStickers: boolean;
canSendGifs: boolean;
onLoad: () => void; onLoad: () => void;
onClose: () => void; onClose: () => void;
onEmojiSelect: (emoji: string) => void; onEmojiSelect: (emoji: string) => void;
@ -51,7 +51,8 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
chatId, chatId,
threadId, threadId,
isOpen, isOpen,
allowedAttachmentOptions, canSendStickers,
canSendGifs,
isLeftColumnShown, isLeftColumnShown,
onLoad, onLoad,
onClose, onClose,
@ -131,8 +132,6 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
const lang = useLang(); const lang = useLang();
const { canSendStickers, canSendGifs } = allowedAttachmentOptions;
function renderContent(isActive: boolean, isFrom: boolean) { function renderContent(isActive: boolean, isFrom: boolean) {
switch (activeTab) { switch (activeTab) {
case SymbolMenuTabs.Emoji: case SymbolMenuTabs.Emoji:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,10 +12,13 @@ export default <B extends Bundles, M extends BundleModules<B>>(
const module = getModuleFromMemory(bundleName, moduleName); const module = getModuleFromMemory(bundleName, moduleName);
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
if (autoUpdate) { useEffect(() => {
// Use effect and cleanup for listener removal if (!autoUpdate) {
addLoadListener(forceUpdate); return undefined;
} }
return addLoadListener(forceUpdate);
}, [autoUpdate, forceUpdate]);
useEffect(() => { useEffect(() => {
if (!noLoad && !module) { 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_RENDER_THRESHOLD = 7;
const DEBUG_EFFECT_THRESHOLD = 7; const DEBUG_EFFECT_THRESHOLD = 7;
const DEBUG_SILENT_RENDERS_FOR = new Set(['TeactMemoWrapper', 'TeactNContainer', 'Button', 'ListItem', 'MenuItem']);
let renderingInstance: ComponentInstance; let renderingInstance: ComponentInstance;
@ -276,7 +277,7 @@ export function renderComponent(componentInstance: ComponentInstance) {
} }
if (DEBUG_MORE) { if (DEBUG_MORE) {
if (componentName !== 'TeactMemoWrapper' && componentName !== 'TeactNContainer') { if (!DEBUG_SILENT_RENDERS_FOR.has(componentName)) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`[Teact] Render ${componentName}`); console.log(`[Teact] Render ${componentName}`);
} }

View File

@ -5,6 +5,7 @@ import {
} from '../../../api/types'; } from '../../../api/types';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import { areDeepEqual } from '../../../util/areDeepEqual';
import { import {
updateChat, updateChat,
deleteChatMessages, deleteChatMessages,
@ -468,14 +469,17 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
} }
case 'updateMessageReactions': { case 'updateMessageReactions': {
setGlobal(updateChatMessage( const { chatId, id, reactions } = update;
global, const message = selectChatMessage(global, chatId, id);
update.chatId, const currentReactions = message?.reactions;
update.id,
{ // `updateMessageReactions` happens with an interval so we try to avoid redundant global state updates
reactions: update.reactions, if (currentReactions && areDeepEqual(reactions, currentReactions)) {
}, return;
)); }
setGlobal(updateChatMessage(global, chatId, id, { reactions: update.reactions }));
break; 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 { DEBUG } from '../config';
import { createCallbackManager } from './callbacks';
export enum Bundles { export enum Bundles {
Auth, Auth,
@ -23,6 +24,8 @@ export type BundleModules<B extends keyof ImportedBundles> = keyof ImportedBundl
const LOAD_PROMISES: Partial<BundlePromises> = {}; const LOAD_PROMISES: Partial<BundlePromises> = {};
const MEMORY_CACHE: Partial<ImportedBundles> = {}; const MEMORY_CACHE: Partial<ImportedBundles> = {};
const { addCallback, runCallbacks } = createCallbackManager();
export async function loadModule<B extends Bundles, M extends BundleModules<B>>(bundleName: B, moduleName: M) { export async function loadModule<B extends Bundles, M extends BundleModules<B>>(bundleName: B, moduleName: M) {
if (!LOAD_PROMISES[bundleName]) { if (!LOAD_PROMISES[bundleName]) {
switch (bundleName) { switch (bundleName) {
@ -45,7 +48,7 @@ export async function loadModule<B extends Bundles, M extends BundleModules<B>>(
break; 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]; 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]; return bundle[moduleName];
} }
const listeners: NoneToVoidFunction[] = []; export const addLoadListener = addCallback;
export function addLoadListener(listener: NoneToVoidFunction) {
if (!listeners.includes(listener)) {
listeners.push(listener);
}
}
function handleBundleLoad() {
listeners.forEach((listener) => {
listener();
});
}