From b4f283d207a1411ea4e6c9ba6533885123733782 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 10 Jan 2023 02:07:51 +0100 Subject: [PATCH] Topic management: Display icon picker (#2247) --- src/api/gramjs/methods/chats.ts | 2 +- src/api/gramjs/methods/index.ts | 2 +- src/api/gramjs/methods/symbols.ts | 15 ++ src/bundles/extra.ts | 2 + src/components/common/GroupChatInfo.tsx | 8 +- src/components/common/ProfileInfo.tsx | 10 +- src/components/common/StickerButton.scss | 4 + src/components/left/main/Chat.tsx | 10 +- src/components/main/Main.tsx | 4 +- src/components/middle/HeaderMenuContainer.tsx | 23 ++- .../middle/composer/CustomEmojiPicker.tsx | 32 ++++- .../middle/composer/StickerPicker.scss | 12 +- src/components/middle/composer/StickerSet.tsx | 136 ++++++++++++------ src/components/right/CreateTopic.async.tsx | 16 +++ src/components/right/CreateTopic.tsx | 69 +++++++-- src/components/right/EditTopic.async.tsx | 16 +++ src/components/right/EditTopic.tsx | 104 ++++++++++---- src/components/right/ManageTopic.module.scss | 42 ++++-- src/components/right/RightColumn.tsx | 4 +- src/config.ts | 1 + src/global/actions/api/chats.ts | 25 +++- src/global/actions/api/symbols.ts | 18 +++ src/global/actions/api/sync.ts | 12 +- src/global/actions/apiUpdaters/messages.ts | 16 ++- src/global/actions/ui/chats.ts | 14 +- src/global/actions/ui/misc.ts | 13 +- src/global/reducers/messages.ts | 5 +- src/global/selectors/messages.ts | 9 ++ src/global/types.ts | 4 + 29 files changed, 474 insertions(+), 154 deletions(-) create mode 100644 src/components/right/CreateTopic.async.tsx create mode 100644 src/components/right/EditTopic.async.tsx diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index a6269fbcb..5cfc4e292 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -1516,7 +1516,7 @@ export function editTopic({ channel: buildInputPeer(id, accessHash), topicId, title, - iconEmojiId: iconEmojiId ? BigInt(iconEmojiId) : undefined, + iconEmojiId: BigInt(iconEmojiId || '0'), closed: isClosed, hidden: isHidden, }), true); diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 585d1e683..9b01d0d70 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -43,7 +43,7 @@ export { faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet, searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects, removeRecentSticker, clearRecentStickers, fetchCustomEmoji, fetchPremiumGifts, fetchCustomEmojiSets, - fetchFeaturedEmojiStickers, fetchGenericEmojiEffects, + fetchFeaturedEmojiStickers, fetchGenericEmojiEffects, fetchDefaultTopicIcons, } from './symbols'; export { diff --git a/src/api/gramjs/methods/symbols.ts b/src/api/gramjs/methods/symbols.ts index 8b384e42d..1f8f48882 100644 --- a/src/api/gramjs/methods/symbols.ts +++ b/src/api/gramjs/methods/symbols.ts @@ -248,6 +248,21 @@ export async function fetchPremiumGifts() { }; } +export async function fetchDefaultTopicIcons() { + const result = await invokeRequest(new GramJs.messages.GetStickerSet({ + stickerset: new GramJs.InputStickerSetEmojiDefaultTopicIcons(), + })); + + if (!(result instanceof GramJs.messages.StickerSet)) { + return undefined; + } + + return { + set: buildStickerSet(result.set), + stickers: processStickerResult(result.documents), + }; +} + export async function searchStickers({ query, hash = '0' }: { query: string; hash?: string }) { const result = await invokeRequest(new GramJs.messages.SearchStickerSets({ q: query, diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 8bf2882c3..7caec368e 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -76,6 +76,8 @@ export { default as GifSearch } from '../components/right/GifSearch'; export { default as Statistics } from '../components/right/statistics/Statistics'; export { default as MessageStatistics } from '../components/right/statistics/MessageStatistics'; export { default as PollResults } from '../components/right/PollResults'; +export { default as CreateTopic } from '../components/right/CreateTopic'; +export { default as EditTopic } from '../components/right/EditTopic'; export { default as Management } from '../components/right/management/Management'; diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx index 82ece7b1d..906f4d545 100644 --- a/src/components/common/GroupChatInfo.tsx +++ b/src/components/common/GroupChatInfo.tsx @@ -20,7 +20,7 @@ import { isChatSuperGroup, } from '../../global/helpers'; import { - selectChat, selectChatMessages, selectChatOnlineCount, selectThreadInfo, + selectChat, selectChatMessages, selectChatOnlineCount, selectThreadInfo, selectThreadMessagesCount, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import renderText from './helpers/renderText'; @@ -61,6 +61,7 @@ type StateProps = onlineCount?: number; areMessagesLoaded: boolean; animationLevel: AnimationLevel; + messagesCount?: number; } & Pick; @@ -85,6 +86,7 @@ const GroupChatInfo: FC = ({ animationLevel, lastSyncTime, topic, + messagesCount, onClick, }) => { const { @@ -148,7 +150,7 @@ const GroupChatInfo: FC = ({ if (isTopic) { return ( - {threadInfo?.messagesCount ? lang('messages', threadInfo.messagesCount, 'i') : renderText(chat.title)} + {messagesCount ? lang('messages', messagesCount, 'i') : renderText(chat.title)} ); } @@ -227,6 +229,7 @@ export default memo(withGlobal( const onlineCount = chat ? selectChatOnlineCount(global, chat) : undefined; const areMessagesLoaded = Boolean(selectChatMessages(global, chatId)); const topic = threadId ? chat?.topics?.[threadId] : undefined; + const messagesCount = topic && selectThreadMessagesCount(global, chatId, threadId!); return { lastSyncTime, @@ -236,6 +239,7 @@ export default memo(withGlobal( topic, areMessagesLoaded, animationLevel: global.settings.byKey.animationLevel, + messagesCount, }; }, )(GroupChatInfo)); diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index 733537491..b3a27c3e8 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -11,11 +11,10 @@ import type { GlobalState } from '../../global/types'; import type { AnimationLevel } from '../../types'; import { MediaViewerOrigin } from '../../types'; -import { GENERAL_TOPIC_ID } from '../../config'; import { IS_TOUCH_ENV } from '../../util/environment'; import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { - selectChat, selectCurrentMessageList, selectThreadInfo, selectUser, selectUserStatus, + selectChat, selectCurrentMessageList, selectThreadMessagesCount, selectUser, selectUserStatus, } from '../../global/selectors'; import { getUserStatus, isChatChannel, isUserOnline } from '../../global/helpers'; import { captureEvents, SwipeDirection } from '../../util/captureEvents'; @@ -187,9 +186,7 @@ const ProfileInfo: FC = ({ />

{renderText(topic!.title)}

- {messagesCount && messagesCount > 1 - ? lang('Chat.Title.Topic', messagesCount + (topic!.id === GENERAL_TOPIC_ID ? 1 : -1), 'i') - : lang('lng_forum_no_messages')} + {messagesCount ? lang('Chat.Title.Topic', messagesCount, 'i') : lang('lng_forum_no_messages')}

); @@ -308,7 +305,6 @@ export default memo(withGlobal( const { mediaId, avatarOwnerId } = global.mediaViewer; const isForum = chat?.isForum; const { threadId: currentTopicId } = selectCurrentMessageList(global) || {}; - const threadInfo = currentTopicId ? selectThreadInfo(global, userId, currentTopicId) : undefined; const topic = isForum && currentTopicId ? chat?.topics?.[currentTopicId] : undefined; return { @@ -323,7 +319,7 @@ export default memo(withGlobal( avatarOwnerId, ...(topic && { topic, - messagesCount: threadInfo?.messagesCount, + messagesCount: selectThreadMessagesCount(global, userId, currentTopicId!), }), }; }, diff --git a/src/components/common/StickerButton.scss b/src/components/common/StickerButton.scss index 2f2151cd1..f0a8187cf 100644 --- a/src/components/common/StickerButton.scss +++ b/src/components/common/StickerButton.scss @@ -44,12 +44,16 @@ right: 0; width: 1.25rem; height: 1.25rem; + display: flex; justify-content: center; align-items: center; + border-radius: 50%; color: white; background: var(--premium-gradient); + + z-index: 1; } &.interactive { diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index b212ac0e1..396c71151 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -124,7 +124,6 @@ const Chat: FC = ({ }) => { const { openChat, - openForumPanel, focusLastMessage, loadTopics, } = getActions(); @@ -156,19 +155,12 @@ const Chat: FC = ({ }); const handleClick = useCallback(() => { - if (isForum) { - openForumPanel({ chatId }); - return; - } - openChat({ id: chatId, shouldReplaceHistory: true }, { forceOnHeavyAnimation: true }); if (isSelected && canScrollDown) { focusLastMessage(); } - }, [ - isForum, openChat, chatId, isSelected, canScrollDown, openForumPanel, focusLastMessage, - ]); + }, [openChat, chatId, isSelected, canScrollDown, focusLastMessage]); const handleDragEnter = useCallback((e) => { e.preventDefault(); diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 50502b280..8dac14322 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -180,6 +180,7 @@ const Main: FC = ({ loadAvailableReactions, loadStickerSets, loadPremiumGifts, + loadDefaultTopicIcons, loadAddedStickers, loadFavoriteStickers, ensureTimeFormat, @@ -222,12 +223,13 @@ const Main: FC = ({ loadAttachBots(); loadContactList(); loadPremiumGifts(); + loadDefaultTopicIcons(); checkAppVersion(); } }, [ lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings, loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachBots, loadContactList, - loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, + loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, loadDefaultTopicIcons, ]); // Language-based API calls diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx index e99cb5e63..33c3a1e7f 100644 --- a/src/components/middle/HeaderMenuContainer.tsx +++ b/src/components/middle/HeaderMenuContainer.tsx @@ -27,8 +27,8 @@ import { getCanAddContact, isChatChannel, isChatGroup, - getHasAdminRight, getCanManageTopic, + isUserRightBanned, } from '../../global/helpers'; import useShowTransition from '../../hooks/useShowTransition'; import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation'; @@ -91,6 +91,7 @@ type StateProps = { isPrivate?: boolean; isMuted?: boolean; isTopic?: boolean; + isForum?: boolean; canAddContact?: boolean; canReportChat?: boolean; canDeleteChat?: boolean; @@ -113,6 +114,7 @@ const HeaderMenuContainer: FC = ({ botCommands, withForumActions, isTopic, + isForum, isChatInfoShown, canStartBot, canRestartBot, @@ -157,6 +159,7 @@ const HeaderMenuContainer: FC = ({ openChatWithInfo, openCreateTopicPanel, openEditTopicPanel, + openChat, } = getActions(); const [isMenuOpen, setIsMenuOpen] = useState(true); @@ -166,7 +169,7 @@ const HeaderMenuContainer: FC = ({ useShowTransition(isOpen, onCloseAnimationEnd, undefined, false); const isViewGroupInfoShown = usePrevDuringAnimation( - (!isChatInfoShown && (withForumActions || isTopic)) ? true : undefined, CLOSE_MENU_ANIMATION_DURATION, + (!isChatInfoShown && isForum) ? true : undefined, CLOSE_MENU_ANIMATION_DURATION, ); const handleReport = useCallback(() => { @@ -222,6 +225,11 @@ const HeaderMenuContainer: FC = ({ closeMenu(); }, [openEditTopicPanel, chatId, threadId, closeMenu]); + const handleViewAsTopicsClick = useCallback(() => { + openChat({ id: undefined }); + closeMenu(); + }, [closeMenu, openChat]); + const handleEnterVoiceChatClick = useCallback(() => { if (canCreateVoiceChat) { // TODO show popup to schedule @@ -352,6 +360,14 @@ const HeaderMenuContainer: FC = ({ {lang('lng_forum_topic_edit')} )} + {IS_SINGLE_COLUMN_LAYOUT && !withForumActions && isForum && !isTopic && ( + + {lang('Chat.ContextViewAsTopics')} + + )} {withForumActions && Boolean(pendingJoinRequests) && ( ( ); const topic = chat?.topics?.[threadId]; - const canCreateTopic = chat.isForum && (chat.isCreator || getHasAdminRight(chat, 'manageTopics')); + const canCreateTopic = chat.isForum && !isUserRightBanned(chat, 'manageTopics'); const canEditTopic = topic && getCanManageTopic(chat, topic); return { @@ -546,6 +562,7 @@ export default memo(withGlobal( isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)), isPrivate, isTopic: chat?.isForum && !isMainThread, + isForum: chat?.isForum, canAddContact, canReportChat, canDeleteChat: getCanDeleteChat(chat), diff --git a/src/components/middle/composer/CustomEmojiPicker.tsx b/src/components/middle/composer/CustomEmojiPicker.tsx index d6ddf71a7..b8dbafb7b 100644 --- a/src/components/middle/composer/CustomEmojiPicker.tsx +++ b/src/components/middle/composer/CustomEmojiPicker.tsx @@ -4,7 +4,7 @@ import React, { } from '../../../lib/teact/teact'; import { getGlobal, withGlobal } from '../../../global'; -import type { ApiStickerSet, ApiSticker, ApiChat } from '../../../api/types'; +import type { ApiStickerSet, ApiSticker } from '../../../api/types'; import type { StickerSetOrRecent } from '../../../types'; import { @@ -41,17 +41,18 @@ import StickerSetCover from './StickerSetCover'; import './StickerPicker.scss'; type OwnProps = { - chatId: string; - className: string; + chatId?: string; + className?: string; loadAndPlay: boolean; + withDefaultTopicIcons?: boolean; onCustomEmojiSelect: (sticker: ApiSticker) => void; }; type StateProps = { - chat?: ApiChat; stickerSetsById: Record; addedCustomEmojiIds?: string[]; recentCustomEmoji: ApiSticker[]; + defaultTopicIconsId?: string; featuredCustomEmojiIds?: string[]; canAnimate?: boolean; isSavedMessages?: boolean; @@ -74,6 +75,8 @@ const CustomEmojiPicker: FC = ({ canAnimate, isSavedMessages, isCurrentUserPremium, + withDefaultTopicIcons, + defaultTopicIconsId, onCustomEmojiSelect, }) => { // eslint-disable-next-line no-null/no-null @@ -124,7 +127,16 @@ const CustomEmojiPicker: FC = ({ const defaultSets = []; - if (recentCustomEmoji.length) { + if (withDefaultTopicIcons) { + const defaultTopicIconsPack = stickerSetsById[defaultTopicIconsId!]; + if (defaultTopicIconsPack.stickers?.length) { + defaultSets.push({ + ...defaultTopicIconsPack, + id: RECENT_SYMBOL_SET_ID, + title: lang('RecentStickers'), + }); + } + } else if (recentCustomEmoji.length) { defaultSets.push({ id: RECENT_SYMBOL_SET_ID, title: lang('RecentStickers'), @@ -144,7 +156,10 @@ const CustomEmojiPicker: FC = ({ ...existingAddedSetIds, ...featuredSetIds, ]; - }, [addedCustomEmojiIds, featuredCustomEmojiIds, lang, recentCustomEmoji, stickerSetsById]); + }, [ + addedCustomEmojiIds, defaultTopicIconsId, featuredCustomEmojiIds, lang, recentCustomEmoji, stickerSetsById, + withDefaultTopicIcons, + ]); const noPopulatedSets = useMemo(() => ( areAddedLoaded @@ -280,6 +295,8 @@ const CustomEmojiPicker: FC = ({ observeIntersection={observeIntersection} shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1} isSavedMessages={isSavedMessages} + shouldHideRecentHeader={withDefaultTopicIcons} + withDefaultTopicIcon={withDefaultTopicIcons} isCustomEmojiPicker isCurrentUserPremium={isCurrentUserPremium} onStickerSelect={handleEmojiSelect} @@ -296,7 +313,7 @@ export default memo(withGlobal( setsById, } = global.stickers; - const isSavedMessages = selectIsChatWithSelf(global, chatId); + const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId)); const recentCustomEmoji = Object.values(pickTruthy(global.customEmojis.byId, global.recentCustomEmojis)); @@ -308,6 +325,7 @@ export default memo(withGlobal( isCurrentUserPremium: selectIsCurrentUserPremium(global), recentCustomEmoji, featuredCustomEmojiIds: global.customEmojis.featuredIds, + defaultTopicIconsId: global.defaultTopicIconsId, }; }, )(CustomEmojiPicker)); diff --git a/src/components/middle/composer/StickerPicker.scss b/src/components/middle/composer/StickerPicker.scss index b7054d1b8..c41e85ff2 100644 --- a/src/components/middle/composer/StickerPicker.scss +++ b/src/components/middle/composer/StickerPicker.scss @@ -5,11 +5,7 @@ position: relative; height: calc(100% - 3rem); overflow-y: auto; - padding: 0.5rem; - - @media (max-width: 600px) { - padding: 0.5rem 0.25rem; - } + padding: 0.5rem 0.25rem; } &-header { @@ -92,12 +88,8 @@ } .symbol-set-container { - width: 25rem; + width: 100%; line-height: 0; - - @media (max-width: 600px) { - width: 100%; - } } .sticker-set-button { diff --git a/src/components/middle/composer/StickerSet.tsx b/src/components/middle/composer/StickerSet.tsx index 0be81a9dc..6eebb4367 100644 --- a/src/components/middle/composer/StickerSet.tsx +++ b/src/components/middle/composer/StickerSet.tsx @@ -1,5 +1,5 @@ import React, { - memo, useCallback, useMemo, useRef, + memo, useCallback, useLayoutEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; import { getActions, getGlobal } from '../../../global'; @@ -10,21 +10,24 @@ import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import { useOnIntersect } from '../../../hooks/useIntersectionObserver'; import { + DEFAULT_TOPIC_ICON_STICKER_ID, EMOJI_SIZE_PICKER, FAVORITE_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, STICKER_SIZE_PICKER, } from '../../../config'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; -import windowSize from '../../../util/windowSize'; import buildClassName from '../../../util/buildClassName'; import { selectIsAlwaysHighPriorityEmoji, selectIsSetPremium } from '../../../global/selectors'; import useLang from '../../../hooks/useLang'; import useFlag from '../../../hooks/useFlag'; import useMediaTransition from '../../../hooks/useMediaTransition'; +import { useResizeObserver } from '../../../hooks/useResizeObserver'; import StickerButton from '../../common/StickerButton'; import ConfirmDialog from '../../ui/ConfirmDialog'; import Button from '../../ui/Button'; +import grey from '../../../assets/icons/forumTopic/grey.svg'; + type OwnProps = { stickerSet: StickerSetOrRecent; loadAndPlay: boolean; @@ -34,6 +37,8 @@ type OwnProps = { isSavedMessages?: boolean; isCurrentUserPremium?: boolean; isCustomEmojiPicker?: boolean; + shouldHideRecentHeader?: boolean; + withDefaultTopicIcon?: boolean; observeIntersection: ObserveFn; onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void; onStickerUnfave?: (sticker: ApiSticker) => void; @@ -41,11 +46,11 @@ type OwnProps = { onStickerRemoveRecent?: (sticker: ApiSticker) => void; }; -const STICKERS_PER_ROW_ON_DESKTOP = 5; -const EMOJI_PER_ROW_ON_DESKTOP = 8; const STICKER_MARGIN = IS_SINGLE_COLUMN_LAYOUT ? 8 : 16; const EMOJI_MARGIN = IS_SINGLE_COLUMN_LAYOUT ? 8 : 10; -const MOBILE_CONTAINER_PADDING = 8; +const CONTAINER_PADDING = 8; + +const ITEMS_PER_ROW_FALLBACK = 8; const StickerSet: FC = ({ stickerSet, @@ -56,6 +61,8 @@ const StickerSet: FC = ({ isSavedMessages, isCurrentUserPremium, isCustomEmojiPicker, + shouldHideRecentHeader, + withDefaultTopicIcon, observeIntersection, onStickerSelect, onStickerUnfave, @@ -75,11 +82,13 @@ const StickerSet: FC = ({ // eslint-disable-next-line no-null/no-null const sharedCanvasRef = useRef(null); // eslint-disable-next-line no-null/no-null - const sharedCanvas2Ref = useRef(null); + const sharedCanvasHqRef = useRef(null); const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag(); const lang = useLang(); + const [itemsPerRow, setItemsPerRow] = useState(ITEMS_PER_ROW_FALLBACK); + useOnIntersect(ref, observeIntersection); const transitionClassNames = useMediaTransition(shouldRender); @@ -110,21 +119,48 @@ const StickerSet: FC = ({ } }, [isCurrentUserPremium, isPremiumSet, openPremiumModal, stickerSet, toggleStickerSet]); - const isLocked = !isSavedMessages && !isRecent && isEmoji && !isCurrentUserPremium - && stickerSet.stickers?.some(({ isFree }) => !isFree); + const handleDefaultTopicIconClick = useCallback(() => { + onStickerSelect?.({ + id: DEFAULT_TOPIC_ICON_STICKER_ID, + isLottie: false, + isVideo: false, + stickerSetInfo: { + shortName: 'dummy', + }, + } satisfies ApiSticker); + }, [onStickerSelect]); + const itemSize = isEmoji ? EMOJI_SIZE_PICKER : STICKER_SIZE_PICKER; - const itemsPerRow = isEmoji ? EMOJI_PER_ROW_ON_DESKTOP : STICKERS_PER_ROW_ON_DESKTOP; const margin = isEmoji ? EMOJI_MARGIN : STICKER_MARGIN; - const stickersPerRow = IS_SINGLE_COLUMN_LAYOUT - ? Math.floor((windowSize.get().width - MOBILE_CONTAINER_PADDING) / (itemSize + margin)) - : itemsPerRow; + const calculateItemsPerRow = useCallback((width: number) => { + if (!width) return ITEMS_PER_ROW_FALLBACK; + + return Math.floor((width - CONTAINER_PADDING) / (itemSize + margin)); + }, [itemSize, margin]); + + const handleResize = useCallback((entry: ResizeObserverEntry) => { + setItemsPerRow(calculateItemsPerRow(entry.contentRect.width)); + }, [calculateItemsPerRow]); + useResizeObserver(ref, handleResize); + + useLayoutEffect(() => { + if (!ref.current) return; + setItemsPerRow(calculateItemsPerRow(ref.current.clientWidth)); + }, [calculateItemsPerRow]); + + const isLocked = !isSavedMessages && !isRecent && isEmoji && !isCurrentUserPremium + && stickerSet.stickers?.some(({ isFree }) => !isFree); const canCut = !stickerSet.installedDate && stickerSet.id !== RECENT_SYMBOL_SET_ID; const [isCut, , expand] = useFlag(canCut); - const itemsBeforeCutout = stickersPerRow * 3 - 1; - const heightWhenCut = Math.ceil(Math.min(itemsBeforeCutout, stickerSet.count) / stickersPerRow) * (itemSize + margin); - const height = isCut ? heightWhenCut : Math.ceil(stickerSet.count / stickersPerRow) * (itemSize + margin); + const itemsBeforeCutout = itemsPerRow * 3 - 1; + const totalItemsCount = withDefaultTopicIcon ? stickerSet.count + 1 : stickerSet.count; + + const heightWhenCut = Math.ceil(Math.min(itemsBeforeCutout, totalItemsCount) / itemsPerRow) * (itemSize + margin); + const height = isCut ? heightWhenCut : Math.ceil(totalItemsCount / itemsPerRow) * (itemSize + margin); + + const shouldHideHeader = isRecent && shouldHideRecentHeader; const favoriteStickerIdsSet = useMemo(() => ( favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined @@ -139,27 +175,29 @@ const StickerSet: FC = ({ buildClassName('symbol-set', isLocked && 'symbol-set-locked') } > -
-

- {isLocked && } - {stickerSet.title} -

- {isRecent && ( - - )} - {!isRecent && isEmoji && !stickerSet.installedDate && ( - - )} -
+ {!shouldHideHeader && ( +
+

+ {isLocked && } + {stickerSet.title} +

+ {isRecent && ( + + )} + {!isRecent && isEmoji && !stickerSet.installedDate && ( + + )} +
+ )}
= ({ className="shared-canvas" style={canCut ? `height: ${heightWhenCut}px;` : undefined} /> - {(isRecent || isFavorite || canCut) && } + {(isRecent || isFavorite || canCut) && } + {withDefaultTopicIcon && ( + + )} {shouldRender && stickerSet.stickers && stickerSet.stickers .slice(0, isCut ? itemsBeforeCutout : stickerSet.stickers.length) .map((sticker, i) => { const isHqEmoji = (isRecent || isFavorite) && selectIsAlwaysHighPriorityEmoji(getGlobal(), sticker.stickerSetInfo); const canvasRef = (canCut && i >= itemsBeforeCutout) || isHqEmoji - ? sharedCanvas2Ref + ? sharedCanvasHqRef : sharedCanvasRef; return ( @@ -198,9 +246,15 @@ const StickerSet: FC = ({ /> ); })} - {isCut && stickerSet.count > itemsBeforeCutout && ( - )}
diff --git a/src/components/right/CreateTopic.async.tsx b/src/components/right/CreateTopic.async.tsx new file mode 100644 index 000000000..55161aace --- /dev/null +++ b/src/components/right/CreateTopic.async.tsx @@ -0,0 +1,16 @@ +import React, { memo } from '../../lib/teact/teact'; +import type { FC } from '../../lib/teact/teact'; +import type { OwnProps } from './CreateTopic'; +import { Bundles } from '../../util/moduleLoader'; + +import useModuleLoader from '../../hooks/useModuleLoader'; +import Loading from '../ui/Loading'; + +const CreateTopicAsync: FC = (props) => { + const CreateTopic = useModuleLoader(Bundles.Extra, 'CreateTopic'); + + // eslint-disable-next-line react/jsx-props-no-spreading + return CreateTopic ? : ; +}; + +export default memo(CreateTopicAsync); diff --git a/src/components/right/CreateTopic.tsx b/src/components/right/CreateTopic.tsx index 190f2152a..e38a61b79 100644 --- a/src/components/right/CreateTopic.tsx +++ b/src/components/right/CreateTopic.tsx @@ -4,10 +4,11 @@ import React, { import { getActions, withGlobal } from '../../global'; import type { FC } from '../../lib/teact/teact'; -import type { ApiChat } from '../../api/types'; +import type { ApiChat, ApiSticker } from '../../api/types'; import type { GlobalState } from '../../global/types'; -import { selectChat } from '../../global/selectors'; +import { DEFAULT_TOPIC_ICON_STICKER_ID } from '../../config'; +import { selectChat, selectIsCurrentUserPremium } from '../../global/selectors'; import { getTopicColors } from '../../util/forumColors'; import cycleRestrict from '../../util/cycleRestrict'; import buildClassName from '../../util/buildClassName'; @@ -20,12 +21,14 @@ import TopicIcon from '../common/TopicIcon'; import InputText from '../ui/InputText'; import FloatingActionButton from '../ui/FloatingActionButton'; import Spinner from '../ui/Spinner'; +import CustomEmojiPicker from '../middle/composer/CustomEmojiPicker'; +import Transition from '../ui/Transition'; import styles from './ManageTopic.module.scss'; const ICON_SIZE = 5 * REM; -type OwnProps = { +export type OwnProps = { isActive: boolean; onClose: NoneToVoidFunction; }; @@ -33,17 +36,20 @@ type OwnProps = { type StateProps = { chat?: ApiChat; createTopicPanel?: GlobalState['createTopicPanel']; + isCurrentUserPremium?: boolean; }; const CreateTopic: FC = ({ isActive, chat, createTopicPanel, + isCurrentUserPremium, onClose, }) => { - const { createTopic, closeCreateTopicPanel } = getActions(); + const { createTopic, openPremiumModal } = getActions(); const [title, setTitle] = useState(''); const [iconColorIndex, setIconColorIndex] = useState(0); + const [iconEmojiId, setIconEmojiId] = useState(undefined); const lang = useLang(); const isTouched = Boolean(title); @@ -67,17 +73,32 @@ const CreateTopic: FC = ({ chatId: chat!.id, title, iconColor: getTopicColors()[iconColorIndex], + iconEmojiId, }); - closeCreateTopicPanel(); - }, [chat, closeCreateTopicPanel, createTopic, iconColorIndex, title]); + }, [chat, createTopic, iconColorIndex, iconEmojiId, title]); + + const handleCustomEmojiSelect = useCallback((emoji: ApiSticker) => { + if (!emoji.isFree && !isCurrentUserPremium) { + openPremiumModal({ initialSection: 'animated_emoji' }); + return; + } + + if (emoji.id === DEFAULT_TOPIC_ICON_STICKER_ID) { + setIconEmojiId(undefined); + return; + } + + setIconEmojiId(emoji.id); + }, [isCurrentUserPremium, openPremiumModal]); const dummyTopic = useMemo(() => { return { id: 0, title, iconColor: getTopicColors()[iconColorIndex], + iconEmojiId, }; - }, [iconColorIndex, title]); + }, [iconColorIndex, iconEmojiId, title]); if (!chat?.isForum) { return undefined; @@ -85,15 +106,24 @@ const CreateTopic: FC = ({ return (
-
-
+
+
{lang('CreateTopicTitle')} - + + + = ({ teactExperimentControlled />
+
+ +
= (props) => { + const EditTopic = useModuleLoader(Bundles.Extra, 'EditTopic'); + + // eslint-disable-next-line react/jsx-props-no-spreading + return EditTopic ? : ; +}; + +export default memo(EditTopicAsync); diff --git a/src/components/right/EditTopic.tsx b/src/components/right/EditTopic.tsx index fe4b065e8..5c620a4cd 100644 --- a/src/components/right/EditTopic.tsx +++ b/src/components/right/EditTopic.tsx @@ -4,10 +4,11 @@ import React, { import { getActions, withGlobal } from '../../global'; import type { FC } from '../../lib/teact/teact'; -import type { ApiChat, ApiTopic } from '../../api/types'; +import type { ApiChat, ApiSticker, ApiTopic } from '../../api/types'; import type { GlobalState } from '../../global/types'; -import { selectChat } from '../../global/selectors'; +import { DEFAULT_TOPIC_ICON_STICKER_ID, GENERAL_TOPIC_ID } from '../../config'; +import { selectChat, selectIsCurrentUserPremium } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { REM } from '../common/helpers/mediaDimensions'; @@ -19,12 +20,14 @@ import InputText from '../ui/InputText'; import FloatingActionButton from '../ui/FloatingActionButton'; import Spinner from '../ui/Spinner'; import Loading from '../ui/Loading'; +import CustomEmojiPicker from '../middle/composer/CustomEmojiPicker'; +import Transition from '../ui/Transition'; import styles from './ManageTopic.module.scss'; const ICON_SIZE = 5 * REM; -type OwnProps = { +export type OwnProps = { isActive: boolean; onClose: NoneToVoidFunction; }; @@ -33,6 +36,7 @@ type StateProps = { chat?: ApiChat; topic?: ApiTopic; editTopicPanel?: GlobalState['editTopicPanel']; + isCurrentUserPremium?: boolean; }; const EditTopic: FC = ({ @@ -40,14 +44,16 @@ const EditTopic: FC = ({ chat, topic, editTopicPanel, + isCurrentUserPremium, onClose, }) => { - const { editTopic, closeEditTopicPanel } = getActions(); + const { editTopic, openPremiumModal } = getActions(); const [title, setTitle] = useState(''); - const [isTouched, setIsTouched] = useState(false); + const [iconEmojiId, setIconEmojiId] = useState(undefined); const lang = useLang(); const isLoading = Boolean(editTopicPanel?.isLoading); + const isGeneral = topic?.id === GENERAL_TOPIC_ID; useHistoryBack({ isActive, @@ -55,33 +61,51 @@ const EditTopic: FC = ({ }); useEffect(() => { - if (topic?.title) { + if (topic?.title || topic?.iconEmojiId) { setTitle(topic.title); - setIsTouched(false); + setIconEmojiId(topic.iconEmojiId); } - }, [topic?.title]); + }, [topic]); + + const isTouched = useMemo(() => { + return title !== topic?.title || iconEmojiId !== topic?.iconEmojiId; + }, [iconEmojiId, title, topic?.iconEmojiId, topic?.title]); const handleTitleChange = useCallback((e: React.ChangeEvent) => { const newTitle = e.target.value; setTitle(newTitle); - setIsTouched(newTitle !== topic?.title); - }, [topic?.title]); + }, []); const handleEditTopic = useCallback(() => { editTopic({ chatId: chat!.id, title, topicId: topic!.id, + iconEmojiId, }); - closeEditTopicPanel(); - }, [chat, closeEditTopicPanel, editTopic, title, topic]); + }, [chat, editTopic, iconEmojiId, title, topic]); + + const handleCustomEmojiSelect = useCallback((emoji: ApiSticker) => { + if (!emoji.isFree && !isCurrentUserPremium) { + openPremiumModal({ initialSection: 'animated_emoji' }); + return; + } + + if (emoji.id === DEFAULT_TOPIC_ICON_STICKER_ID) { + setIconEmojiId(undefined); + return; + } + + setIconEmojiId(emoji.id); + }, [isCurrentUserPremium, openPremiumModal]); const dummyTopic = useMemo(() => { return { ...topic!, title, + iconEmojiId, }; - }, [title, topic]); + }, [iconEmojiId, title, topic]); if (!chat?.isForum) { return undefined; @@ -89,24 +113,45 @@ const EditTopic: FC = ({ return (
-
+
{!topic && } {topic && ( -
- {lang('CreateTopicTitle')} - - -
+ <> +
+ {lang('CreateTopicTitle')} + + + + +
+ {!isGeneral && ( +
+ +
+ )} + )}
]+alt="([^"]+)"(?![^>]*data-document-id)[^>]*>/gm; export const BASE_EMOJI_KEYWORD_LANG = 'en'; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 048f347c9..16b5974b6 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -1373,13 +1373,16 @@ addActionHandler('loadTopics', async (global, actions, payload) => { }); addActionHandler('loadTopicById', async (global, actions, payload) => { - const { chatId, topicId } = payload; + const { chatId, topicId, shouldCloseChatOnError } = payload; const chat = selectChat(global, chatId); if (!chat) return; const result = await callApi('fetchTopicById', { chat, topicId }); if (!result) { + if (shouldCloseChatOnError) { + actions.openChat({ id: undefined }); + } return; } @@ -1413,7 +1416,9 @@ addActionHandler('toggleForum', async (global, actions, payload) => { }); addActionHandler('createTopic', async (global, actions, payload) => { - const { chatId, title, iconColor } = payload; + const { + chatId, title, iconColor, iconEmojiId, + } = payload; const chat = selectChat(global, chatId); if (!chat) return; @@ -1425,10 +1430,13 @@ addActionHandler('createTopic', async (global, actions, payload) => { }, }); - const topicId = await callApi('createTopic', { chat, title, iconColor }); + const topicId = await callApi('createTopic', { + chat, title, iconColor, iconEmojiId, + }); if (topicId) { actions.openChat({ id: chatId, threadId: topicId, shouldReplaceHistory: true }); } + actions.closeCreateTopicPanel(); }); addActionHandler('deleteTopic', async (global, actions, payload) => { @@ -1451,12 +1459,23 @@ addActionHandler('editTopic', async (global, actions, payload) => { const topic = chat?.topics?.[topicId]; if (!chat || !topic) return; + setGlobal({ + ...global, + editTopicPanel: { + chatId, + topicId, + isLoading: true, + }, + }); + const result = await callApi('editTopic', { chat, topicId, ...rest }); if (!result) return; global = getGlobal(); global = updateTopic(global, chatId, topicId, rest); setGlobal(global); + + actions.closeEditTopicPanel(); }); addActionHandler('toggleTopicPinned', (global, actions, payload) => { diff --git a/src/global/actions/api/symbols.ts b/src/global/actions/api/symbols.ts index fb3e92d69..476cc6aa6 100644 --- a/src/global/actions/api/symbols.ts +++ b/src/global/actions/api/symbols.ts @@ -198,6 +198,24 @@ addActionHandler('loadPremiumGifts', async () => { }); }); +addActionHandler('loadDefaultTopicIcons', async (global) => { + const stickerSet = await callApi('fetchDefaultTopicIcons'); + if (!stickerSet) { + return; + } + global = getGlobal(); + + const { set, stickers } = stickerSet; + + const fullSet = { ...set, stickers }; + + global = updateStickerSet(global, fullSet.id, fullSet); + setGlobal({ + ...global, + defaultTopicIconsId: fullSet.id, + }); +}); + addActionHandler('loadStickers', (global, actions, payload) => { const { stickerSetInfo } = payload; const cachedSet = selectStickerSet(global, stickerSetInfo); diff --git a/src/global/actions/api/sync.ts b/src/global/actions/api/sync.ts index 7afef9c40..4acec1856 100644 --- a/src/global/actions/api/sync.ts +++ b/src/global/actions/api/sync.ts @@ -104,9 +104,6 @@ async function loadAndReplaceMessages() { const currentChat = activeCurrentChatId ? global.chats.byId[activeCurrentChatId] : undefined; if (activeCurrentChatId && currentChat) { - if (currentChat.isForum) { - getActions().loadTopics({ chatId: activeCurrentChatId, force: true }); - } const result = await loadTopMessages(currentChat, activeThreadId, threadInfo?.lastReadInboxMessageId); global = getGlobal(); const { chatId: newCurrentChatId } = selectCurrentMessageList(global) || {}; @@ -162,6 +159,15 @@ async function loadAndReplaceMessages() { setGlobal(global); + if (currentChat?.isForum) { + getActions().loadTopics({ chatId: activeCurrentChatId!, force: true }); + if (currentThreadId && currentThreadId !== MAIN_THREAD_ID) { + getActions().loadTopicById({ + chatId: activeCurrentChatId!, topicId: currentThreadId, shouldCloseChatOnError: true, + }); + } + } + const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer; if (audioChatId && audioMessageId && !selectChatMessage(global, audioChatId, audioMessageId)) { getActions().closeAudioPlayer(); diff --git a/src/global/actions/apiUpdaters/messages.ts b/src/global/actions/apiUpdaters/messages.ts index 668e85982..7cd2d6c7d 100644 --- a/src/global/actions/apiUpdaters/messages.ts +++ b/src/global/actions/apiUpdaters/messages.ts @@ -23,6 +23,7 @@ import { deleteChatScheduledMessages, updateThreadUnreadFromForwardedMessage, updateTopic, + deleteTopic, } from '../../reducers'; import { selectChatMessage, @@ -376,7 +377,7 @@ addActionHandler('apiUpdate', (global, actions, update) => { case 'deleteMessages': { const { ids, chatId } = update; - deleteMessages(chatId, ids, actions, global); + deleteMessages(global, chatId, ids, actions); break; } @@ -402,7 +403,7 @@ addActionHandler('apiUpdate', (global, actions, update) => { if (chatMessages) { const ids = Object.keys(chatMessages.byId).map(Number); - deleteMessages(chatId, ids, actions, getGlobal()); + deleteMessages(global, chatId, ids, actions); } else { actions.requestChatUpdate({ chatId }); } @@ -734,7 +735,7 @@ function updateListedAndViewportIds(global: GlobalState, actions: GlobalActions, global = replaceThreadParam(global, chatId, threadInfo.threadId, 'threadInfo', { ...threadInfo, lastMessageId: message.id, - messagesCount: threadInfo.messagesCount + 1, + messagesCount: (threadInfo.messagesCount || 0) + 1, }); } @@ -808,10 +809,13 @@ function findLastMessage(global: GlobalState, chatId: string) { return undefined; } -function deleteMessages(chatId: string | undefined, ids: number[], actions: GlobalActions, global: GlobalState) { +function deleteMessages(global: GlobalState, chatId: string | undefined, ids: number[], actions: GlobalActions) { // Channel update if (chatId) { + const chat = selectChat(global, chatId); + if (!chat) return; + ids.forEach((id) => { global = updateChatMessage(global, chatId, id, { isDeleting: true, @@ -821,6 +825,10 @@ function deleteMessages(chatId: string | undefined, ids: number[], actions: Glob if (newLastMessage) { global = updateChatLastMessage(global, chatId, newLastMessage, true); } + + if (chat.topics?.[id]) { + global = deleteTopic(global, chatId, id); + } }); actions.requestChatUpdate({ chatId }); diff --git a/src/global/actions/ui/chats.ts b/src/global/actions/ui/chats.ts index 91ebe75ef..630e8e527 100644 --- a/src/global/actions/ui/chats.ts +++ b/src/global/actions/ui/chats.ts @@ -5,13 +5,13 @@ import { MAIN_THREAD_ID } from '../../../api/types'; import { exitMessageSelectMode, replaceThreadParam, updateCurrentMessageList, } from '../../reducers'; -import { selectCurrentMessageList } from '../../selectors'; +import { selectChat, selectCurrentMessageList } from '../../selectors'; import { closeLocalTextSearch } from './localSearch'; addActionHandler('openChat', (global, actions, payload) => { const { id, - threadId = MAIN_THREAD_ID, + threadId, type = 'thread', shouldReplaceHistory = false, } = payload; @@ -35,7 +35,7 @@ addActionHandler('openChat', (global, actions, payload) => { || currentMessageList.type !== type )) { if (id) { - global = replaceThreadParam(global, id, threadId, 'replyStack', []); + global = replaceThreadParam(global, id, threadId || MAIN_THREAD_ID, 'replyStack', []); } global = exitMessageSelectMode(global); @@ -54,10 +54,16 @@ addActionHandler('openChat', (global, actions, payload) => { }; } - if (id !== global.forumPanelChatId) { + if (id && id !== global.forumPanelChatId) { actions.closeForumPanel(); } + if (id && !threadId) { + const chat = selectChat(global, id); + // Prevent chat opening on forum click + if (chat?.isForum) return global; + } + return updateCurrentMessageList(global, id, threadId, type, shouldReplaceHistory); }); diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index 5decea4d4..478e40d5c 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -1,11 +1,14 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index'; import type { ApiError, ApiNotification } from '../../../api/types'; +import { MAIN_THREAD_ID } from '../../../api/types'; import { APP_VERSION, DEBUG, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT } from '../../../config'; import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT } from '../../../util/environment'; import getReadableErrorText from '../../../util/getReadableErrorText'; -import { selectChatMessage, selectCurrentMessageList, selectIsTrustedBot } from '../../selectors'; +import { + selectChatMessage, selectCurrentChat, selectCurrentMessageList, selectIsTrustedBot, +} from '../../selectors'; import generateIdFor from '../../../util/generateIdFor'; import { unique } from '../../../util/iteratees'; @@ -443,6 +446,10 @@ addActionHandler('updateLastRenderedCustomEmojis', (global, actions, payload) => addActionHandler('openCreateTopicPanel', (global, actions, payload) => { const { chatId } = payload; + // Topic panel can be opened only if there is a selected chat + const currentChat = selectCurrentChat(global); + if (!currentChat) actions.openChat({ id: chatId, threadId: MAIN_THREAD_ID }); + return { ...global, createTopicPanel: { @@ -461,6 +468,10 @@ addActionHandler('closeCreateTopicPanel', (global) => { addActionHandler('openEditTopicPanel', (global, actions, payload) => { const { chatId, topicId } = payload; + // Topic panel can be opened only if there is a selected chat + const currentChat = selectCurrentChat(global); + if (!currentChat) actions.openChat({ id: chatId }); + return { ...global, editTopicPanel: { diff --git a/src/global/reducers/messages.ts b/src/global/reducers/messages.ts index 3b8e2a2da..051a944bc 100644 --- a/src/global/reducers/messages.ts +++ b/src/global/reducers/messages.ts @@ -250,8 +250,7 @@ export function deleteChatMessages( global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'pinnedIds', mainPinnedIds); if (threadInfo && newMessageCount !== undefined) { - global = replaceThreadParam(global, chatId, threadId, 'threadInfo', { - ...threadInfo, + global = updateThreadInfo(global, chatId, threadId, { messagesCount: newMessageCount, }); } @@ -581,7 +580,7 @@ export function updateThreadUnreadFromForwardedMessage( global = replaceThreadParam(global, chatId, channelPostId, 'threadInfo', { ...threadInfoOld, lastMessageId, - messagesCount: threadInfoOld.messagesCount + (isDeleting ? -1 : 1), + messagesCount: (threadInfoOld.messagesCount || 0) + (isDeleting ? -1 : 1), }); } } diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index d0d4a244e..b8585d354 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -173,6 +173,15 @@ export function selectReplyStack(global: GlobalState, chatId: string, threadId: return selectThreadParam(global, chatId, threadId, 'replyStack'); } +export function selectThreadMessagesCount(global: GlobalState, chatId: string, threadId: number) { + const chat = selectChat(global, chatId); + const threadInfo = selectThreadInfo(global, chatId, threadId); + if (!chat || !threadInfo || threadInfo.messagesCount === undefined) return undefined; + // In forum topics first message is ignored, but not in General + if (chat.isForum && threadId !== GENERAL_TOPIC_ID) return threadInfo.messagesCount - 1; + return threadInfo.messagesCount; +} + export function selectThreadOriginChat(global: GlobalState, chatId: string, threadId: number) { if (threadId === MAIN_THREAD_ID) { return selectChat(global, chatId); diff --git a/src/global/types.ts b/src/global/types.ts index bdbadf005..47b995944 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -349,6 +349,7 @@ export type GlobalState = { animatedEmojiEffects?: ApiStickerSet; genericEmojiEffects?: ApiStickerSet; premiumGifts?: ApiStickerSet; + defaultTopicIconsId?: string; emojiKeywords: Partial>; gifs: { @@ -1305,6 +1306,7 @@ export interface ActionPayloads { }; loadPremiumGifts: never; + loadDefaultTopicIcons: never; loadPremiumStickers: { hash?: string; }; @@ -1335,6 +1337,7 @@ export interface ActionPayloads { chatId: string; title: string; iconColor?: number; + iconEmojiId?: string; }; loadTopics: { chatId: string; @@ -1343,6 +1346,7 @@ export interface ActionPayloads { loadTopicById: { chatId: string; topicId: number; + shouldCloseChatOnError?: boolean; }; deleteTopic: {