[Perf] Reduce unneeded renders in various components
This commit is contained in:
parent
a9e699e82f
commit
07ac02b201
@ -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));
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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} `);
|
onUpdateHtml(`${currentHtml.substr(0, atIndex)}${insertedHtml} `);
|
||||||
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,
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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
13
src/hooks/useStateRef.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
35
src/util/areDeepEqual.ts
Normal 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]));
|
||||||
|
}
|
||||||
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user