From a185fd940776ee3e5cd361521ecd153d297d1819 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 20 Aug 2021 23:47:01 +0300 Subject: [PATCH] Composer: Add Bot Command Menu and Bot Command Tooltip (#1364) --- src/api/gramjs/apiBuilders/bots.ts | 6 +- src/api/gramjs/apiBuilders/chats.ts | 12 + src/api/gramjs/apiBuilders/users.ts | 13 +- src/api/gramjs/methods/bots.ts | 23 +- src/api/gramjs/methods/chats.ts | 7 + src/api/gramjs/methods/index.ts | 2 +- src/api/types/bots.ts | 6 + src/api/types/chats.ts | 2 + src/api/types/users.ts | 2 + src/assets/fonts/icomoon.woff | Bin 35792 -> 36176 bytes src/assets/fonts/icomoon.woff2 | Bin 16752 -> 16828 bytes src/bundles/extra.ts | 2 + src/components/middle/MessageList.tsx | 5 +- .../middle/composer/AttachmentModal.tsx | 5 +- .../middle/composer/BotCommand.scss | 27 ++ src/components/middle/composer/BotCommand.tsx | 47 +++ .../middle/composer/BotCommandMenu.async.tsx | 15 + .../middle/composer/BotCommandMenu.scss | 24 ++ .../middle/composer/BotCommandMenu.tsx | 63 ++++ .../composer/BotCommandTooltip.async.tsx | 15 + .../middle/composer/BotCommandTooltip.scss | 11 + .../middle/composer/BotCommandTooltip.tsx | 106 ++++++ src/components/middle/composer/Composer.scss | 17 +- src/components/middle/composer/Composer.tsx | 103 +++++- .../middle/composer/EmojiTooltip.tsx | 61 +--- .../middle/composer/InlineBotTooltip.tsx | 69 ++-- .../middle/composer/MentionTooltip.tsx | 60 +--- .../middle/composer/SymbolMenu.scss | 7 + .../composer/hooks/useBotCommandTooltip.ts | 67 ++++ .../composer/hooks/useKeyboardNavigation.ts | 66 ++++ .../composer/hooks/useMentionTooltip.ts | 3 - src/components/payment/PaymentModal.tsx | 1 - .../management/ManageChatPrivacyType.tsx | 2 +- src/config.ts | 1 + src/global/types.ts | 2 +- src/hooks/useMouseInside.ts | 8 +- src/lib/gramjs/client/auth.ts | 1 - src/lib/gramjs/tl/apiTl.js | 1 + src/lib/gramjs/tl/static/api.reduced.tl | 3 +- src/modules/actions/api/bots.ts | 20 +- src/modules/actions/apiUpdaters/initial.ts | 3 +- src/styles/Telegram T.json | 308 ++++++++++-------- src/styles/icons.scss | 10 +- src/util/getReadableErrorText.ts | 2 +- 44 files changed, 887 insertions(+), 321 deletions(-) create mode 100644 src/components/middle/composer/BotCommand.scss create mode 100644 src/components/middle/composer/BotCommand.tsx create mode 100644 src/components/middle/composer/BotCommandMenu.async.tsx create mode 100644 src/components/middle/composer/BotCommandMenu.scss create mode 100644 src/components/middle/composer/BotCommandMenu.tsx create mode 100644 src/components/middle/composer/BotCommandTooltip.async.tsx create mode 100644 src/components/middle/composer/BotCommandTooltip.scss create mode 100644 src/components/middle/composer/BotCommandTooltip.tsx create mode 100644 src/components/middle/composer/hooks/useBotCommandTooltip.ts create mode 100644 src/components/middle/composer/hooks/useKeyboardNavigation.ts diff --git a/src/api/gramjs/apiBuilders/bots.ts b/src/api/gramjs/apiBuilders/bots.ts index 692c45ae5..d86141fec 100644 --- a/src/api/gramjs/apiBuilders/bots.ts +++ b/src/api/gramjs/apiBuilders/bots.ts @@ -1,6 +1,6 @@ import { Api as GramJs } from '../../../lib/gramjs'; import { - ApiBotInlineMediaResult, ApiBotInlineResult, ApiInlineResultType, ApiWebDocument, + ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm, ApiInlineResultType, ApiWebDocument, } from '../../types'; import { pick } from '../../../util/iteratees'; @@ -46,6 +46,10 @@ export function buildApiBotInlineMediaResult( }; } +export function buildBotSwitchPm(switchPm?: GramJs.InlineBotSwitchPM) { + return switchPm ? pick(switchPm, ['text', 'startParam']) as ApiBotInlineSwitchPm : undefined; +} + function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDocument | undefined { return document ? pick(document, ['url', 'mimeType']) : undefined; } diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 8b219ded5..366ff0dc7 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -3,6 +3,7 @@ import { ApiChat, ApiChatAdminRights, ApiChatBannedRights, + ApiBotCommand, ApiChatFolder, ApiChatMember, ApiRestrictionReason, @@ -376,3 +377,14 @@ export function buildApiChatFolderFromSuggested({ description, }; } + +export function buildApiChatBotCommands(botInfos: GramJs.BotInfo[]) { + return botInfos.reduce((botCommands, botInfo) => { + botCommands = botCommands.concat(botInfo.commands.map((mtpCommand) => ({ + botId: botInfo.userId, + ...omitVirtualClassFields(mtpCommand), + }))); + + return botCommands; + }, [] as ApiBotCommand[]); +} diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 691f0c870..bd304df82 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -1,5 +1,7 @@ import { Api as GramJs } from '../../../lib/gramjs'; -import { ApiUser, ApiUserStatus, ApiUserType } from '../../types'; +import { + ApiBotCommand, ApiUser, ApiUserStatus, ApiUserType, +} from '../../types'; export function buildApiUserFromFull(mtpUserFull: GramJs.UserFull): ApiUser { const { @@ -14,6 +16,7 @@ export function buildApiUserFromFull(mtpUserFull: GramJs.UserFull): ApiUser { pinnedMessageId: pinnedMsgId, isBlocked: Boolean(blocked), ...(botInfo && { botDescription: botInfo.description }), + ...(botInfo && botInfo.commands.length && { botCommands: buildApiBotCommands(mtpUserFull.user.id, botInfo) }), }, }; } @@ -74,3 +77,11 @@ export function buildApiUserStatus(mtpStatus?: GramJs.TypeUserStatus): ApiUserSt return { type: 'userStatusLastMonth' }; } } + +function buildApiBotCommands(botId: number, botInfo: GramJs.BotInfo) { + return botInfo.commands.map(({ command, description }) => ({ + botId, + command, + description, + })) as ApiBotCommand[]; +} diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 3938d67e7..ee31d2963 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -1,15 +1,14 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; -import { ApiBotInlineSwitchPm, ApiChat, ApiUser } from '../../types'; +import { ApiChat, ApiUser } from '../../types'; import localDb from '../localDb'; import { invokeRequest } from './client'; import { buildInputPeer, calculateResultHash, generateRandomBigInt } from '../gramjsBuilders'; import { buildApiUser } from '../apiBuilders/users'; -import { buildApiBotInlineMediaResult, buildApiBotInlineResult } from '../apiBuilders/bots'; +import { buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm } from '../apiBuilders/bots'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; -import { pick } from '../../../util/iteratees'; export function init() { } @@ -92,7 +91,7 @@ export async function fetchInlineBotResults({ isGallery: Boolean(result.gallery), help: bot.botPlaceholder, nextOffset: getInlineBotResultsNextOffset(bot.username, result.nextOffset), - switchPm: buildSwitchPm(result.switchPm), + switchPm: buildBotSwitchPm(result.switchPm), users: result.users.map(buildApiUser).filter(Boolean as any), results: processInlineBotResult(String(result.queryId), result.results), }; @@ -118,8 +117,20 @@ export async function sendInlineBotResult({ }), true); } -function buildSwitchPm(switchPm?: GramJs.InlineBotSwitchPM) { - return switchPm ? pick(switchPm, ['text', 'startParam']) as ApiBotInlineSwitchPm : undefined; +export async function startBot({ + bot, startParam, +}: { + bot: ApiUser; + startParam?: string; +}) { + const randomId = generateRandomBigInt(); + + await invokeRequest(new GramJs.messages.StartBot({ + bot: buildInputPeer(bot.id, bot.accessHash), + peer: buildInputPeer(bot.id, bot.accessHash), + randomId, + startParam, + }), true); } function processInlineBotResult(queryId: string, results: GramJs.TypeBotInlineResult[]) { diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 2d4f963c5..e54330e66 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -22,6 +22,7 @@ import { getApiChatIdFromMtpPeer, buildApiChatFolder, buildApiChatFolderFromSuggested, + buildApiChatBotCommands, } from '../apiBuilders/chats'; import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages'; import { buildApiUser } from '../apiBuilders/users'; @@ -317,10 +318,12 @@ async function getFullChatInfo(chatId: number): Promise<{ about, participants, exportedInvite, + botInfo, } = result.fullChat; const members = buildChatMembers(participants); const adminMembers = members ? members.filter(({ isAdmin, isOwner }) => isAdmin || isOwner) : undefined; + const botCommands = botInfo ? buildApiChatBotCommands(botInfo) : undefined; return { fullInfo: { @@ -328,6 +331,7 @@ async function getFullChatInfo(chatId: number): Promise<{ members, adminMembers, canViewMembers: true, + botCommands, ...(exportedInvite && { inviteLink: exportedInvite.link, }), @@ -361,6 +365,7 @@ async function getFullChannelInfo( linkedChatId, hiddenPrehistory, call, + botInfo, } = result.fullChat; const inviteLink = exportedInvite instanceof GramJs.ChatInviteExported @@ -374,6 +379,7 @@ async function getFullChannelInfo( const { members: adminMembers, users: adminUsers } = ( canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'admin') ) || {}; + const botCommands = botInfo ? buildApiChatBotCommands(botInfo) : undefined; return { fullInfo: { @@ -395,6 +401,7 @@ async function getFullChannelInfo( adminMembers, groupCallId: call ? call.id.toString() : undefined, linkedChatId: linkedChatId ? getApiChatIdFromMtpPeer({ chatId: linkedChatId } as GramJs.TypePeer) : undefined, + botCommands, }, users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])], }; diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index c56ee47c7..7b4ced51d 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -55,7 +55,7 @@ export { } from './twoFaSettings'; export { - answerCallbackButton, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, sendInlineBotResult, + answerCallbackButton, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, sendInlineBotResult, startBot, } from './bots'; export { diff --git a/src/api/types/bots.ts b/src/api/types/bots.ts index 3b6a8ac4e..2ac6cf358 100644 --- a/src/api/types/bots.ts +++ b/src/api/types/bots.ts @@ -38,3 +38,9 @@ export interface ApiBotInlineSwitchPm { text: string; startParam: string; } + +export interface ApiBotCommand { + botId: number; + command: string; + description: string; +} diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 6643e573d..f662de452 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -1,4 +1,5 @@ import { ApiMessage, ApiPhoto } from './messages'; +import { ApiBotCommand } from './bots'; type ApiChatType = ( 'chatTypePrivate' | 'chatTypeSecret' | @@ -96,6 +97,7 @@ export interface ApiChatFullInfo { maxMessageId?: number; }; linkedChatId?: number; + botCommands?: ApiBotCommand[]; } export interface ApiChatMember { diff --git a/src/api/types/users.ts b/src/api/types/users.ts index b06128d45..4f9c15d3f 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -1,4 +1,5 @@ import { ApiPhoto } from './messages'; +import { ApiBotCommand } from './bots'; export interface ApiUser { id: number; @@ -28,6 +29,7 @@ export interface ApiUserFullInfo { commonChatsCount?: number; botDescription?: string; pinnedMessageId?: number; + botCommands?: ApiBotCommand[]; } export type ApiUserType = 'userTypeBot' | 'userTypeRegular' | 'userTypeDeleted' | 'userTypeUnknown'; diff --git a/src/assets/fonts/icomoon.woff b/src/assets/fonts/icomoon.woff index 9c9fd54c8565c64f5852e86525e661ade46b16c3..1c87e0b48404b941d15b2b0b0865980b3e0799dd 100755 GIT binary patch delta 620 zcmcaGo$10XCXsS~H#Y`G1|aASVBiMREE7ev>z5_xCKfO-FlGRSLO@tN>@s_LVlhZ; z50K9R#RBO$m1#h+BMb~}9w7YT*!9qi)Wj49h7KN}8Z!`uE(gZW-4f6ZpUbDX3VI_$0#ZyW~^w(XlkOS zY>J($3Xx^n)8*69AQfC|M2q~$*B}~#n!qQCAf+Etpn52Y6fJ`7C#1xic1d-CfXpCU+ zW}eE-!=MfH9?;E7Z0wADjPi_(dSYfkPuVhxfxRrxD8~ronwo(JoPnB)0vzA|tF!w?AW8a6k==*fH9%o$rZe`(`k;sD74 Zam1!h4n{7gJI*Q~%?z!Z`MM4<0sy?Ib-wYj1C^A%{5&djBMawY2Mt|eTWeND0)mi diff --git a/src/assets/fonts/icomoon.woff2 b/src/assets/fonts/icomoon.woff2 index ad4766f4ca2997e0032a83ce8fc094f76863c931..c8420c5c52aa2341fdfb6d60a1b365931fbcc5aa 100644 GIT binary patch literal 16828 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�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 U-1hSwTR2l=JeKy(wP9cY0E(@8;Q#;t diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 617187af4..9f85bb9a0 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -29,6 +29,8 @@ export { default as AttachmentModal } from '../components/middle/composer/Attach export { default as PollModal } from '../components/middle/composer/PollModal'; export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu'; export { default as AttachMenu } from '../components/middle/composer/AttachMenu'; +export { default as BotCommandTooltip } from '../components/middle/composer/BotCommandTooltip'; +export { default as BotCommandMenu } from '../components/middle/composer/BotCommandMenu'; export { default as MentionTooltip } from '../components/middle/composer/MentionTooltip'; export { default as StickerTooltip } from '../components/middle/composer/StickerTooltip'; export { default as CustomSendMenu } from '../components/middle/composer/CustomSendMenu'; diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 264fb2bf8..600656bdd 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -546,10 +546,9 @@ export default memo(withGlobal( && !messageIds && !chat.unreadCount && !focusingId && lastMessage && !lastMessage.groupedId ); - const bot = selectChatBot(global, chatId); + const chatBot = selectChatBot(global, chatId)!; let botDescription: string | undefined; if (selectIsChatBotNotStarted(global, chatId)) { - const chatBot = selectChatBot(global, chatId)!; if (chatBot.fullInfo) { botDescription = chatBot.fullInfo.botDescription || 'NoMessages'; } else { @@ -565,7 +564,7 @@ export default memo(withGlobal( isGroupChat: isChatGroup(chat), isCreator: chat.isCreator, isChatWithSelf: selectIsChatWithSelf(global, chatId), - isBot: Boolean(bot), + isBot: Boolean(chatBot), messageIds, messagesById, firstUnreadId: selectFirstUnreadId(global, chatId, threadId), diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index 738bd13d5..4a439e8c9 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -67,9 +67,7 @@ const AttachmentModal: FC = ({ const lang = useLang(); const { - isMentionTooltipOpen, mentionFilter, - closeMentionTooltip, insertMention, - mentionFilteredUsers, + isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers, } = useMentionTooltip( isOpen, caption, @@ -227,7 +225,6 @@ const AttachmentModal: FC = ({ void; +}; + +const BotCommand: FC = ({ + withAvatar, + focus, + botCommand, + bot, + onClick, +}) => { + return ( + onClick(botCommand)} + focus={focus} + > + {withAvatar && ( + + )} +
+ /{botCommand.command} + {renderText(botCommand.description)} +
+
+ ); +}; + +export default memo(BotCommand); diff --git a/src/components/middle/composer/BotCommandMenu.async.tsx b/src/components/middle/composer/BotCommandMenu.async.tsx new file mode 100644 index 000000000..dcc980429 --- /dev/null +++ b/src/components/middle/composer/BotCommandMenu.async.tsx @@ -0,0 +1,15 @@ +import React, { FC, memo } from '../../../lib/teact/teact'; +import { OwnProps } from './BotCommandMenu'; +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const BotCommandMenuAsync: FC = (props) => { + const { isOpen } = props; + const BotCommandMenu = useModuleLoader(Bundles.Extra, 'BotCommandMenu', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return BotCommandMenu ? : undefined; +}; + +export default memo(BotCommandMenuAsync); diff --git a/src/components/middle/composer/BotCommandMenu.scss b/src/components/middle/composer/BotCommandMenu.scss new file mode 100644 index 000000000..e2fc16edb --- /dev/null +++ b/src/components/middle/composer/BotCommandMenu.scss @@ -0,0 +1,24 @@ +.BotCommandMenu { + .menu-container { + width: calc(100% - 4rem); + max-width: 20rem; + max-height: 40vh; + overflow: auto; + flex-direction: column; + + @media (max-width: 600px) { + width: calc(100% - 3rem); + } + } + + .is-pointer-env & { + > .backdrop { + position: absolute; + top: -1rem; + left: 0; + right: auto; + width: 3.5rem; + height: 4.5rem; + } + } +} diff --git a/src/components/middle/composer/BotCommandMenu.tsx b/src/components/middle/composer/BotCommandMenu.tsx new file mode 100644 index 000000000..5bf96c3ee --- /dev/null +++ b/src/components/middle/composer/BotCommandMenu.tsx @@ -0,0 +1,63 @@ +import React, { FC, memo, useCallback } from '../../../lib/teact/teact'; +import { withGlobal } from '../../../lib/teact/teactn'; + +import { GlobalActions } from '../../../global/types'; +import { ApiBotCommand } from '../../../api/types'; + +import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../util/environment'; +import { pick } from '../../../util/iteratees'; +import useMouseInside from '../../../hooks/useMouseInside'; + +import Menu from '../../ui/Menu'; +import BotCommand from './BotCommand'; + +import './BotCommandMenu.scss'; + +export type OwnProps = { + isOpen: boolean; + botCommands: ApiBotCommand[]; + onClose: NoneToVoidFunction; +}; + +type DispatchProps = Pick; + +const BotCommandMenu: FC = ({ + isOpen, botCommands, onClose, sendBotCommand, +}) => { + const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose, undefined, IS_SINGLE_COLUMN_LAYOUT); + + const handleClick = useCallback((botCommand: ApiBotCommand) => { + sendBotCommand({ + command: `/${botCommand.command}`, + botId: botCommand.botId, + }); + onClose(); + }, [onClose, sendBotCommand]); + + return ( + + {botCommands.map((botCommand) => ( + + ))} + + ); +}; + +export default memo(withGlobal( + undefined, + (setGlobal, actions): DispatchProps => pick(actions, ['sendBotCommand']), +)(BotCommandMenu)); diff --git a/src/components/middle/composer/BotCommandTooltip.async.tsx b/src/components/middle/composer/BotCommandTooltip.async.tsx new file mode 100644 index 000000000..d6b41c73f --- /dev/null +++ b/src/components/middle/composer/BotCommandTooltip.async.tsx @@ -0,0 +1,15 @@ +import React, { FC, memo } from '../../../lib/teact/teact'; +import { OwnProps } from './BotCommandTooltip'; +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const BotCommandTooltipAsync: FC = (props) => { + const { isOpen } = props; + const BotCommandTooltip = useModuleLoader(Bundles.Extra, 'BotCommandTooltip', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return BotCommandTooltip ? : undefined; +}; + +export default memo(BotCommandTooltipAsync); diff --git a/src/components/middle/composer/BotCommandTooltip.scss b/src/components/middle/composer/BotCommandTooltip.scss new file mode 100644 index 000000000..8c65d4006 --- /dev/null +++ b/src/components/middle/composer/BotCommandTooltip.scss @@ -0,0 +1,11 @@ +.BotCommandTooltip { + width: calc(100% - 4rem); + max-width: 26rem; + flex-direction: column; + + z-index: -1; + + @media (max-width: 600px) { + width: calc(100% - 3rem); + } +} diff --git a/src/components/middle/composer/BotCommandTooltip.tsx b/src/components/middle/composer/BotCommandTooltip.tsx new file mode 100644 index 000000000..78250db70 --- /dev/null +++ b/src/components/middle/composer/BotCommandTooltip.tsx @@ -0,0 +1,106 @@ +import React, { + FC, useCallback, useEffect, useRef, memo, +} from '../../../lib/teact/teact'; +import { withGlobal } from '../../../lib/teact/teactn'; + +import { ApiBotCommand, ApiUser } from '../../../api/types'; +import { GlobalActions } from '../../../global/types'; + +import { pick } from '../../../util/iteratees'; +import buildClassName from '../../../util/buildClassName'; +import setTooltipItemVisible from '../../../util/setTooltipItemVisible'; +import useShowTransition from '../../../hooks/useShowTransition'; +import usePrevious from '../../../hooks/usePrevious'; +import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'; + +import BotCommand from './BotCommand'; + +import './BotCommandTooltip.scss'; + +export type OwnProps = { + isOpen: boolean; + withUsername?: boolean; + botCommands?: ApiBotCommand[]; + onClick: NoneToVoidFunction; + onClose: NoneToVoidFunction; +}; + +type StateProps = { + usersById: Record; +}; + +type DispatchProps = Pick; + +const BotCommandTooltip: FC = ({ + usersById, + isOpen, + withUsername, + botCommands, + onClick, + onClose, + sendBotCommand, +}) => { + // eslint-disable-next-line no-null/no-null + const containerRef = useRef(null); + const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); + + const handleSendCommand = useCallback(({ botId, command }: ApiBotCommand) => { + const bot = usersById[botId]; + sendBotCommand({ + command: `/${command}${withUsername && bot ? `@${bot.username}` : ''}`, + botId, + }); + onClick(); + }, [onClick, sendBotCommand, usersById, withUsername]); + + const selectedCommandIndex = useKeyboardNavigation({ + isActive: isOpen, + items: botCommands, + onSelect: handleSendCommand, + onClose, + }); + + useEffect(() => { + if (botCommands && !botCommands.length) { + onClose(); + } + }, [botCommands, onClose]); + + useEffect(() => { + setTooltipItemVisible('.chat-item-clickable', selectedCommandIndex, containerRef); + }, [selectedCommandIndex]); + + const prevCommands = usePrevious(botCommands && botCommands.length ? botCommands : undefined, shouldRender); + const renderedCommands = botCommands && !botCommands.length ? prevCommands : botCommands; + + if (!shouldRender || (renderedCommands && !renderedCommands.length)) { + return undefined; + } + + const className = buildClassName( + 'BotCommandTooltip composer-tooltip custom-scroll', + transitionClassNames, + ); + + return ( +
+ {renderedCommands && renderedCommands.map((chatBotCommand, index) => ( + + ))} +
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => ({ + usersById: global.users.byId, + }), + (setGlobal, actions): DispatchProps => pick(actions, ['sendBotCommand']), +)(BotCommandTooltip)); diff --git a/src/components/middle/composer/Composer.scss b/src/components/middle/composer/Composer.scss index a5c1612b4..f72c62c4b 100644 --- a/src/components/middle/composer/Composer.scss +++ b/src/components/middle/composer/Composer.scss @@ -192,13 +192,14 @@ > .Spinner { align-self: center; --spinner-size: 1.5rem; + margin-right: -.5rem; } > .Button { flex-shrink: 0; background: none !important; - width: 3.375rem; - height: 3.375rem; + width: 3.5rem; + height: 3.5rem; margin: 0; padding: 0.625rem; align-self: flex-end; @@ -210,7 +211,17 @@ } + .Button { - margin-left: -.25rem; + margin-left: -.625rem; + } + + &.bot-commands { + color: var(--color-primary) !important; + padding-right: 0; + + // SymbolMenu button should be accessible if BotCommandsMenu opened + body.is-touch-env &.activated + .Button.mobile-symbol-menu-button { + z-index: calc(var(--z-menu-backdrop) + 1); + } } &.scheduled-button i::after { diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index 67b1748ae..979c1c4b9 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -17,14 +17,16 @@ import { ApiChatMember, ApiUser, MAIN_THREAD_ID, + ApiBotCommand, } from '../../../api/types'; import { InlineBotSettings } from '../../../types'; -import { BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_ID, SCHEDULED_WHEN_ONLINE } from '../../../config'; +import { + BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_ID, REPLIES_USER_ID, SCHEDULED_WHEN_ONLINE, +} from '../../../config'; import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment'; import { selectChat, - selectIsChatWithBot, selectIsRightColumnShown, selectIsInSelectMode, selectNewestMessageWithBotKeyboardButtons, @@ -32,6 +34,7 @@ import { selectScheduledIds, selectEditingMessage, selectIsChatWithSelf, + selectChatBot, selectChatUser, selectChatMessage, } from '../../../modules/selectors'; @@ -50,8 +53,10 @@ import insertHtmlInSelection from '../../../util/insertHtmlInSelection'; import deleteLastCharacterOutsideSelection from '../../../util/deleteLastCharacterOutsideSelection'; import { pick } from '../../../util/iteratees'; import buildClassName from '../../../util/buildClassName'; +import windowSize from '../../../util/windowSize'; import { isSelectionInsideInput } from './helpers/selection'; import applyIosAutoCapitalizationFix from './helpers/applyIosAutoCapitalizationFix'; +import { getServerTime } from '../../../util/serverTime'; import useFlag from '../../../hooks/useFlag'; import useVoiceRecording from './hooks/useVoiceRecording'; @@ -65,8 +70,7 @@ import useMentionTooltip from './hooks/useMentionTooltip'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useLang from '../../../hooks/useLang'; import useInlineBotTooltip from './hooks/useInlineBotTooltip'; -import windowSize from '../../../util/windowSize'; -import { getServerTime } from '../../../util/serverTime'; +import useBotCommandTooltip from './hooks/useBotCommandTooltip'; import DeleteMessageModal from '../../common/DeleteMessageModal.async'; import Button from '../../ui/Button'; @@ -79,10 +83,12 @@ import MentionTooltip from './MentionTooltip.async'; import CustomSendMenu from './CustomSendMenu.async'; import StickerTooltip from './StickerTooltip.async'; import EmojiTooltip from './EmojiTooltip.async'; +import BotCommandTooltip from './BotCommandTooltip.async'; import BotKeyboardMenu from './BotKeyboardMenu'; import MessageInput from './MessageInput'; import ComposerEmbeddedMessage from './ComposerEmbeddedMessage'; import AttachmentModal from './AttachmentModal.async'; +import BotCommandMenu from './BotCommandMenu.async'; import PollModal from './PollModal.async'; import DropArea, { DropAreaState } from './DropArea.async'; import WebPagePreview from './WebPagePreview'; @@ -133,6 +139,8 @@ type StateProps = { topInlineBotIds?: number[]; isInlineBotLoading: boolean; inlineBots?: Record; + botCommands?: ApiBotCommand[] | false; + chatBotCommands?: ApiBotCommand[]; } & Pick; type DispatchProps = Pick = ({ recentEmojis, inlineBots, isInlineBotLoading, + botCommands, + chatBotCommands, sendMessage, editMessage, saveDraft, @@ -259,6 +269,7 @@ const Composer: FC = ({ const [attachments, setAttachments] = useState([]); const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag(); + const [isBotCommandMenuOpen, openBotCommandMenu, closeBotCommandMenu] = useFlag(); const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag(); const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag(); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); @@ -283,9 +294,7 @@ const Composer: FC = ({ const canShowCustomSendMenu = !shouldSchedule; const { - isMentionTooltipOpen, mentionFilter, - closeMentionTooltip, insertMention, - mentionFilteredUsers, + isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers, } = useMentionTooltip( !attachments.length, html, @@ -313,6 +322,17 @@ const Composer: FC = ({ inlineBots, ); + const { + isOpen: isBotCommandTooltipOpen, + close: closeBotCommandTooltip, + filteredBotCommands: botTooltipCommands, + } = useBotCommandTooltip( + Boolean((botCommands && botCommands.length) || (chatBotCommands && chatBotCommands.length)), + html, + botCommands, + chatBotCommands, + ); + const { isContextMenuOpen: isCustomSendMenuOpen, handleContextMenu, @@ -532,6 +552,16 @@ const Composer: FC = ({ resetComposer, stopRecordingVoice, showDialog, slowMode, isAdmin, sendMessage, forwardMessages, lang, ]); + const handleActivateBotCommandMenu = useCallback(() => { + closeSymbolMenu(); + openBotCommandMenu(); + }, [closeSymbolMenu, openBotCommandMenu]); + + const handleActivateSymbolMenu = useCallback(() => { + closeBotCommandMenu(); + openSymbolMenu(); + }, [closeBotCommandMenu, openSymbolMenu]); + const handleStickerSelect = useCallback((sticker: ApiSticker, shouldPreserveInput = false) => { sticker = { ...sticker, @@ -582,6 +612,13 @@ const Composer: FC = ({ }); }, [chatId, clearDraft, connectionState, resetComposer, sendInlineBotResult]); + const handleBotCommandSelect = useCallback(() => { + clearDraft({ chatId, localOnly: true }); + requestAnimationFrame(() => { + resetComposer(); + }); + }, [chatId, clearDraft, resetComposer]); + const handlePollSend = useCallback((poll: ApiNewPoll) => { if (shouldSchedule) { setScheduledMessageArgs({ poll }); @@ -598,7 +635,7 @@ const Composer: FC = ({ setScheduledMessageArgs({ isSilent: true }); openCalendar(); } else { - handleSend(true); + void handleSend(true); } }, [handleSend, openCalendar, shouldSchedule]); @@ -610,7 +647,7 @@ const Composer: FC = ({ + (isWhenOnline ? 0 : serverTimeOffset); if (!scheduledMessageArgs || Object.keys(restArgs).length === 0) { - handleSend(!!isSilent, scheduledAt); + void handleSend(!!isSilent, scheduledAt); } else { sendMessage({ ...scheduledMessageArgs, @@ -652,9 +689,10 @@ const Composer: FC = ({ messageInput.blur(); setTimeout(() => { + closeBotCommandMenu(); openSymbolMenu(); }, MOBILE_KEYBOARD_HIDE_DELAY_MS); - }, [openSymbolMenu]); + }, [openSymbolMenu, closeBotCommandMenu]); const handleAllScheduledClick = useCallback(() => { openChat({ id: chatId, threadId, type: 'scheduled' }); @@ -687,14 +725,14 @@ const Composer: FC = ({ } openCalendar(); } else { - handleSend(); + void handleSend(); requestAnimationFrame(() => { resetComposer(); }); } break; case MainButtonState.Record: - startRecordingVoice(); + void startRecordingVoice(); break; case MainButtonState.Edit: handleEditComplete(); @@ -800,7 +838,6 @@ const Composer: FC = ({ )} = ({ loadMore={loadMoreForInlineBot} onClose={closeInlineBotTooltip} /> +
@@ -827,6 +871,19 @@ const Composer: FC = ({ disabled={!allowedAttachmentOptions.canAttachEmbedLinks} />
+ {isChatWithBot && botCommands !== false && !activeVoiceRecording && !editingMessage && ( + + + + )} {IS_SINGLE_COLUMN_LAYOUT ? ( ) : ( @@ -885,7 +942,7 @@ const Composer: FC = ({ )} {botKeyboardMessageId && !activeVoiceRecording && !editingMessage && ( = ({ )} {!activeVoiceRecording && !editingMessage && ( = ({ onClose={closeBotKeyboard} /> )} + {botCommands && ( + + )} ( (global, { chatId, threadId, messageListType }): StateProps => { const chat = selectChat(global, chatId); const chatUser = chat && selectChatUser(global, chat); - const isChatWithBot = chat ? selectIsChatWithBot(global, chat) : undefined; + const chatBot = chatId !== REPLIES_USER_ID ? selectChatBot(global, chatId) : undefined; + const isChatWithBot = Boolean(chatBot); const isChatWithSelf = selectIsChatWithSelf(global, chatId); const messageWithActualBotKeyboard = isChatWithBot && selectNewestMessageWithBotKeyboardButtons(global, chatId); const scheduledIds = selectScheduledIds(global, chatId); @@ -1055,6 +1120,8 @@ export default memo(withGlobal( serverTimeOffset: global.serverTimeOffset, inlineBots: global.inlineBots.byUsername, isInlineBotLoading: global.inlineBots.isLoading, + chatBotCommands: chat && chat.fullInfo && chat.fullInfo.botCommands, + botCommands: chatBot && chatBot.fullInfo ? (chatBot.fullInfo.botCommands || false) : undefined, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ diff --git a/src/components/middle/composer/EmojiTooltip.tsx b/src/components/middle/composer/EmojiTooltip.tsx index 86656eab7..d54f93bda 100644 --- a/src/components/middle/composer/EmojiTooltip.tsx +++ b/src/components/middle/composer/EmojiTooltip.tsx @@ -1,16 +1,15 @@ import React, { - FC, memo, useCallback, useEffect, useRef, useState, + FC, memo, useCallback, useEffect, useRef, } from '../../../lib/teact/teact'; import { IS_TOUCH_ENV } from '../../../util/environment'; import buildClassName from '../../../util/buildClassName'; -import cycleRestrict from '../../../util/cycleRestrict'; -import captureKeyboardListeners from '../../../util/captureKeyboardListeners'; import findInViewport from '../../../util/findInViewport'; import isFullyVisible from '../../../util/isFullyVisible'; import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal'; import useShowTransition from '../../../hooks/useShowTransition'; import usePrevDuringAnimation from '../../../hooks/usePrevDuringAnimation'; +import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'; import Loading from '../../ui/Loading'; import EmojiButton from './EmojiButton'; @@ -20,7 +19,6 @@ import './EmojiTooltip.scss'; const VIEWPORT_MARGIN = 8; const EMOJI_BUTTON_WIDTH = 44; const CLOSE_DURATION = 350; -const NO_EMOJI_SELECTED_INDEX = -1; function setItemVisible(index: number, containerRef: Record) { const container = containerRef.current!; @@ -70,52 +68,27 @@ const EmojiTooltip: FC = ({ const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); const listEmojis: Emoji[] = usePrevDuringAnimation(emojis.length ? emojis : undefined, CLOSE_DURATION) || []; - const [selectedIndex, setSelectedIndex] = useState(NO_EMOJI_SELECTED_INDEX); - - useEffect(() => { - setSelectedIndex(0); - }, [emojis]); - - useEffect(() => { - setItemVisible(selectedIndex, containerRef); - }, [selectedIndex]); - - const getSelectedIndex = useCallback((newIndex: number) => { - if (!emojis.length) { - return NO_EMOJI_SELECTED_INDEX; - } - - const emojisCount = emojis.length; - return cycleRestrict(emojisCount, newIndex); - }, [emojis]); - - const handleArrowKey = useCallback((value: number, e: KeyboardEvent) => { - e.preventDefault(); - setSelectedIndex((index) => (getSelectedIndex(index + value))); - }, [setSelectedIndex, getSelectedIndex]); - - const handleSelectEmoji = useCallback((e: KeyboardEvent) => { - if (emojis.length && selectedIndex > NO_EMOJI_SELECTED_INDEX) { - const emoji = emojis[selectedIndex]; - if (emoji) { - e.preventDefault(); - onEmojiSelect(emoji.native); - addRecentEmoji({ emoji: emoji.id }); - } - } - }, [addRecentEmoji, emojis, onEmojiSelect, selectedIndex]); + const handleSelectEmoji = useCallback((emoji: Emoji) => { + onEmojiSelect(emoji.native); + addRecentEmoji({ emoji: emoji.id }); + }, [addRecentEmoji, onEmojiSelect]); const handleClick = useCallback((native: string, id: string) => { onEmojiSelect(native); addRecentEmoji({ emoji: id }); }, [addRecentEmoji, onEmojiSelect]); - useEffect(() => (isOpen ? captureKeyboardListeners({ - onEsc: onClose, - onLeft: (e: KeyboardEvent) => handleArrowKey(-1, e), - onRight: (e: KeyboardEvent) => handleArrowKey(1, e), - onEnter: handleSelectEmoji, - }) : undefined), [handleArrowKey, handleSelectEmoji, isOpen, onClose]); + const selectedIndex = useKeyboardNavigation({ + isActive: isOpen, + isHorizontal: true, + items: emojis, + onSelect: handleSelectEmoji, + onClose, + }); + + useEffect(() => { + setItemVisible(selectedIndex, containerRef); + }, [selectedIndex]); const handleMouseEnter = () => { document.body.classList.add('no-select'); diff --git a/src/components/middle/composer/InlineBotTooltip.tsx b/src/components/middle/composer/InlineBotTooltip.tsx index 669feab3d..f7b7a985f 100644 --- a/src/components/middle/composer/InlineBotTooltip.tsx +++ b/src/components/middle/composer/InlineBotTooltip.tsx @@ -1,5 +1,5 @@ import React, { - FC, memo, useCallback, useEffect, useRef, useState, + FC, memo, useCallback, useEffect, useRef, } from '../../../lib/teact/teact'; import { withGlobal } from '../../../lib/teact/teactn'; @@ -11,13 +11,12 @@ import { LoadMoreDirection } from '../../../types'; import { IS_TOUCH_ENV } from '../../../util/environment'; import setTooltipItemVisible from '../../../util/setTooltipItemVisible'; import buildClassName from '../../../util/buildClassName'; -import captureKeyboardListeners from '../../../util/captureKeyboardListeners'; -import cycleRestrict from '../../../util/cycleRestrict'; import useShowTransition from '../../../hooks/useShowTransition'; import { throttle } from '../../../util/schedulers'; import { pick } from '../../../util/iteratees'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import usePrevious from '../../../hooks/usePrevious'; +import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'; import MediaResult from './inlineResults/MediaResult'; import ArticleResult from './inlineResults/ArticleResult'; @@ -43,7 +42,7 @@ export type OwnProps = { onClose: NoneToVoidFunction; }; -type DispatchProps = Pick; +type DispatchProps = Pick; const InlineBotTooltip: FC = ({ isOpen, @@ -54,13 +53,12 @@ const InlineBotTooltip: FC = ({ loadMore, onClose, openChat, - sendBotCommand, + startBot, onSelectResult, }) => { // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); - const [selectedIndex, setSelectedIndex] = useState(-1); const { observe: observeIntersection, } = useIntersectionObserver({ @@ -69,58 +67,29 @@ const InlineBotTooltip: FC = ({ isDisabled: !isOpen, }); - useEffect(() => { - setSelectedIndex(isGallery ? -1 : 0); - }, [inlineBotResults, isGallery]); - - useEffect(() => { - setTooltipItemVisible('.chat-item-clickable', selectedIndex, containerRef); - }, [selectedIndex]); - - const getSelectedIndex = useCallback((newIndex: number) => { - if (!inlineBotResults || !inlineBotResults.length) { - return -1; - } - - return cycleRestrict(inlineBotResults.length, newIndex); - }, [inlineBotResults]); - - const handleArrowKey = useCallback((value: number, e: KeyboardEvent) => { - if (isGallery) { - return; - } - - e.preventDefault(); - setSelectedIndex((index) => (getSelectedIndex(index + value))); - }, [isGallery, getSelectedIndex]); - - const handleSelectInlineBotResult = useCallback((e: KeyboardEvent) => { - if (inlineBotResults && inlineBotResults.length && selectedIndex > -1) { - const inlineResult = inlineBotResults[selectedIndex]; - if (inlineResult) { - e.preventDefault(); - onSelectResult(inlineResult); - } - } - }, [inlineBotResults, onSelectResult, selectedIndex]); - const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { if (direction === LoadMoreDirection.Backwards) { runThrottled(loadMore); } }, [loadMore]); - useEffect(() => (isOpen ? captureKeyboardListeners({ - onEsc: onClose, - onUp: (e: KeyboardEvent) => handleArrowKey(-1, e), - onDown: (e: KeyboardEvent) => handleArrowKey(1, e), - onEnter: handleSelectInlineBotResult, - }) : undefined), [handleArrowKey, handleSelectInlineBotResult, isGallery, isOpen, onClose]); + const selectedIndex = useKeyboardNavigation({ + isActive: isOpen, + shouldRemoveSelectionOnReset: isGallery, + noArrowNavigation: isGallery, + items: inlineBotResults, + onSelect: onSelectResult, + onClose, + }); + + useEffect(() => { + setTooltipItemVisible('.chat-item-clickable', selectedIndex, containerRef); + }, [selectedIndex]); const handleSendPm = useCallback(() => { openChat({ id: botId }); - sendBotCommand({ chatId: botId, command: `/start ${switchPm!.startParam}` }); - }, [botId, openChat, sendBotCommand, switchPm]); + startBot({ botId, param: switchPm!.startParam }); + }, [botId, openChat, startBot, switchPm]); const prevInlineBotResults = usePrevious( inlineBotResults && inlineBotResults.length @@ -230,6 +199,6 @@ const InlineBotTooltip: FC = ({ export default memo(withGlobal( undefined, (setGlobal, actions): DispatchProps => pick(actions, [ - 'sendBotCommand', 'openChat', 'sendInlineBotResult', + 'startBot', 'openChat', 'sendInlineBotResult', ]), )(InlineBotTooltip)); diff --git a/src/components/middle/composer/MentionTooltip.tsx b/src/components/middle/composer/MentionTooltip.tsx index ba900cb4a..aecadfced 100644 --- a/src/components/middle/composer/MentionTooltip.tsx +++ b/src/components/middle/composer/MentionTooltip.tsx @@ -1,5 +1,5 @@ import React, { - FC, useCallback, useEffect, useState, useRef, memo, + FC, useCallback, useEffect, useRef, memo, } from '../../../lib/teact/teact'; import usePrevious from '../../../hooks/usePrevious'; @@ -7,9 +7,8 @@ import { ApiUser } from '../../../api/types'; import useShowTransition from '../../../hooks/useShowTransition'; import buildClassName from '../../../util/buildClassName'; -import captureKeyboardListeners from '../../../util/captureKeyboardListeners'; import setTooltipItemVisible from '../../../util/setTooltipItemVisible'; -import cycleRestrict from '../../../util/cycleRestrict'; +import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'; import ListItem from '../../ui/ListItem'; import PrivateChatInfo from '../../common/PrivateChatInfo'; @@ -18,7 +17,6 @@ import './MentionTooltip.scss'; export type OwnProps = { isOpen: boolean; - filter: string; onClose: () => void; onInsertUserName: (user: ApiUser, forceFocus?: boolean) => void; filteredUsers?: ApiUser[]; @@ -27,7 +25,6 @@ export type OwnProps = { const MentionTooltip: FC = ({ isOpen, - filter, onClose, onInsertUserName, usersById, @@ -37,21 +34,6 @@ const MentionTooltip: FC = ({ const containerRef = useRef(null); const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); - const getSelectedIndex = useCallback((newIndex: number) => { - if (!filteredUsers) { - return -1; - } - const membersCount = filteredUsers!.length; - return cycleRestrict(membersCount, newIndex); - }, [filteredUsers]); - - const [selectedMentionIndex, setSelectedMentionIndex] = useState(-1); - - const handleArrowKey = useCallback((value: number, e: KeyboardEvent) => { - e.preventDefault(); - setSelectedMentionIndex((index) => (getSelectedIndex(index + value))); - }, [setSelectedMentionIndex, getSelectedIndex]); - const handleUserSelect = useCallback((userId: number, forceFocus = false) => { const user = usersById && usersById[userId]; if (!user) { @@ -61,23 +43,21 @@ const MentionTooltip: FC = ({ onInsertUserName(user, forceFocus); }, [usersById, onInsertUserName]); - const handleSelectMention = useCallback((e: KeyboardEvent) => { - if (filteredUsers && filteredUsers.length && selectedMentionIndex > -1) { - const member = filteredUsers[selectedMentionIndex]; - if (member) { - e.preventDefault(); - handleUserSelect(member.id, true); - } - } - }, [filteredUsers, selectedMentionIndex, handleUserSelect]); + const handleSelectMention = useCallback((member: ApiUser) => { + handleUserSelect(member.id, true); + }, [handleUserSelect]); - useEffect(() => (isOpen ? captureKeyboardListeners({ - onEsc: onClose, - onUp: (e: KeyboardEvent) => handleArrowKey(-1, e), - onDown: (e: KeyboardEvent) => handleArrowKey(1, e), - onEnter: handleSelectMention, - onTab: handleSelectMention, - }) : undefined), [isOpen, onClose, handleArrowKey, handleSelectMention]); + const selectedMentionIndex = useKeyboardNavigation({ + isActive: isOpen, + items: filteredUsers, + onSelect: handleSelectMention, + shouldSelectOnTab: true, + onClose, + }); + + useEffect(() => { + setTooltipItemVisible('.chat-item-clickable', selectedMentionIndex, containerRef); + }, [selectedMentionIndex]); useEffect(() => { if (filteredUsers && !filteredUsers.length) { @@ -85,14 +65,6 @@ const MentionTooltip: FC = ({ } }, [filteredUsers, onClose]); - useEffect(() => { - setSelectedMentionIndex(0); - }, [filter]); - - useEffect(() => { - setTooltipItemVisible('.chat-item-clickable', selectedMentionIndex, containerRef); - }, [selectedMentionIndex]); - const prevChatMembers = usePrevious( filteredUsers && filteredUsers.length ? filteredUsers diff --git a/src/components/middle/composer/SymbolMenu.scss b/src/components/middle/composer/SymbolMenu.scss index 866e100f7..4ff91541d 100644 --- a/src/components/middle/composer/SymbolMenu.scss +++ b/src/components/middle/composer/SymbolMenu.scss @@ -90,6 +90,13 @@ } } + .Button.bot-commands ~ & { + .is-pointer-env & > .backdrop { + left: 3rem; + width: 3.25rem; + } + } + .bubble { padding: 0; width: var(--symbol-menu-width); diff --git a/src/components/middle/composer/hooks/useBotCommandTooltip.ts b/src/components/middle/composer/hooks/useBotCommandTooltip.ts new file mode 100644 index 000000000..ad1de4842 --- /dev/null +++ b/src/components/middle/composer/hooks/useBotCommandTooltip.ts @@ -0,0 +1,67 @@ +import { + useCallback, useEffect, useState, +} from '../../../../lib/teact/teact'; + +import { ApiBotCommand } from '../../../../api/types'; + +import { throttle } from '../../../../util/schedulers'; +import useFlag from '../../../../hooks/useFlag'; + +const runThrottled = throttle((cb) => cb(), 500, true); +const RE_COMMAND = /[\w\d_-]*/i; + +export default function useBotCommandTooltip( + isAllowed: boolean, + html: string, + botCommands?: ApiBotCommand[] | false, + chatBotCommands?: ApiBotCommand[], +) { + const [isOpen, markIsOpen, unmarkIsOpen] = useFlag(); + const [filteredBotCommands, setFilteredBotCommands] = useState(); + + const getFilteredCommands = useCallback((filter) => { + if (!botCommands && !chatBotCommands) { + setFilteredBotCommands(undefined); + + return; + } + + runThrottled(() => { + const nextFilteredBotCommands = (botCommands || chatBotCommands || []) + .filter(({ command }) => !filter || command.includes(filter)); + setFilteredBotCommands( + nextFilteredBotCommands && nextFilteredBotCommands.length ? nextFilteredBotCommands : undefined, + ); + }); + }, [botCommands, chatBotCommands]); + + useEffect(() => { + if (!isAllowed || !html.length) { + unmarkIsOpen(); + return; + } + + const shouldShowCommands = html.startsWith('/'); + + if (shouldShowCommands) { + const filter = html.substr(1).match(RE_COMMAND); + getFilteredCommands(filter ? filter[0] : ''); + } else { + unmarkIsOpen(); + } + }, [getFilteredCommands, html, isAllowed, unmarkIsOpen]); + + useEffect(() => { + if (filteredBotCommands && filteredBotCommands.length) { + markIsOpen(); + } else { + unmarkIsOpen(); + } + }, [filteredBotCommands, markIsOpen, unmarkIsOpen]); + + return { + isOpen, + close: unmarkIsOpen, + filteredBotCommands, + }; +} diff --git a/src/components/middle/composer/hooks/useKeyboardNavigation.ts b/src/components/middle/composer/hooks/useKeyboardNavigation.ts new file mode 100644 index 000000000..ce1d59c48 --- /dev/null +++ b/src/components/middle/composer/hooks/useKeyboardNavigation.ts @@ -0,0 +1,66 @@ +import { useCallback, useEffect, useState } from '../../../../lib/teact/teact'; +import captureKeyboardListeners from '../../../../util/captureKeyboardListeners'; +import cycleRestrict from '../../../../util/cycleRestrict'; + +export function useKeyboardNavigation({ + isActive, + isHorizontal, + shouldRemoveSelectionOnReset, + noArrowNavigation, + items, + shouldSelectOnTab, + onSelect, + onClose, +}: { + isActive: boolean; + isHorizontal?: boolean; + shouldRemoveSelectionOnReset?: boolean; + noArrowNavigation?: boolean; + items?: any[]; + shouldSelectOnTab?: boolean; + onSelect: AnyToVoidFunction; + onClose: NoneToVoidFunction; +}) { + const [selectedItemIndex, setSelectedItemIndex] = useState(-1); + + const getSelectedIndex = useCallback((newIndex: number) => { + if (!items) { + return -1; + } + + return cycleRestrict(items.length, newIndex); + }, [items]); + + const handleArrowKey = useCallback((value: number, e: KeyboardEvent) => { + e.preventDefault(); + setSelectedItemIndex((index) => (getSelectedIndex(index + value))); + }, [setSelectedItemIndex, getSelectedIndex]); + + const handleItemSelect = useCallback((e: KeyboardEvent) => { + if (items && items.length && selectedItemIndex > -1) { + const item = items[selectedItemIndex]; + if (item) { + e.preventDefault(); + onSelect(item); + } + } + }, [items, onSelect, selectedItemIndex]); + + useEffect(() => { + setSelectedItemIndex(shouldRemoveSelectionOnReset ? -1 : 0); + }, [items, shouldRemoveSelectionOnReset]); + + useEffect(() => (isActive ? captureKeyboardListeners({ + onEsc: onClose, + onUp: noArrowNavigation || isHorizontal ? undefined : (e: KeyboardEvent) => handleArrowKey(-1, e), + onDown: noArrowNavigation || isHorizontal ? undefined : (e: KeyboardEvent) => handleArrowKey(1, e), + onLeft: noArrowNavigation || !isHorizontal ? undefined : (e: KeyboardEvent) => handleArrowKey(-1, e), + onRight: noArrowNavigation || !isHorizontal ? undefined : (e: KeyboardEvent) => handleArrowKey(1, e), + onTab: shouldSelectOnTab ? handleItemSelect : undefined, + onEnter: handleItemSelect, + }) : undefined), [ + noArrowNavigation, handleArrowKey, handleItemSelect, isActive, isHorizontal, onClose, shouldSelectOnTab, + ]); + + return selectedItemIndex; +} diff --git a/src/components/middle/composer/hooks/useMentionTooltip.ts b/src/components/middle/composer/hooks/useMentionTooltip.ts index 5ef53f7c1..31663b1aa 100644 --- a/src/components/middle/composer/hooks/useMentionTooltip.ts +++ b/src/components/middle/composer/hooks/useMentionTooltip.ts @@ -35,7 +35,6 @@ export default function useMentionTooltip( usersById?: Record, ) { const [isOpen, markIsOpen, unmarkIsOpen] = useFlag(); - const [currentFilter, setCurrentFilter] = useState(''); const [usersToMention, setUsersToMention] = useState(); const topInlineBots = useMemo(() => { @@ -77,7 +76,6 @@ export default function useMentionTooltip( if (usernameFilter) { const filter = usernameFilter ? usernameFilter.substr(1) : ''; - setCurrentFilter(filter); getFilteredUsers(filter, canSuggestInlineBots(html)); } else { unmarkIsOpen(); @@ -121,7 +119,6 @@ export default function useMentionTooltip( return { isMentionTooltipOpen: isOpen, - mentionFilter: currentFilter, closeMentionTooltip: unmarkIsOpen, insertMention, mentionFilteredUsers: usersToMention, diff --git a/src/components/payment/PaymentModal.tsx b/src/components/payment/PaymentModal.tsx index 45e0fdda7..d79fef6b5 100644 --- a/src/components/payment/PaymentModal.tsx +++ b/src/components/payment/PaymentModal.tsx @@ -100,7 +100,6 @@ const Invoice: FC = ({ [error.field]: error.message, }, }); - return; } }, [error, paymentDispatch]); diff --git a/src/components/right/management/ManageChatPrivacyType.tsx b/src/components/right/management/ManageChatPrivacyType.tsx index eccfda0ee..097fb63d9 100644 --- a/src/components/right/management/ManageChatPrivacyType.tsx +++ b/src/components/right/management/ManageChatPrivacyType.tsx @@ -62,7 +62,7 @@ const ManageChatPrivacyType: FC = ({ const canUpdate = Boolean( (privacyType === 'public' && username && isUsernameAvailable) - || (privacyType === 'private' && isPublic) + || (privacyType === 'private' && isPublic), ); useHistoryBack(isActive, onClose); diff --git a/src/config.ts b/src/config.ts index 5d5ee7b42..d98348311 100644 --- a/src/config.ts +++ b/src/config.ts @@ -133,6 +133,7 @@ export const RE_TME_INVITE_LINK = /^(?:https?:\/\/)?(?:t\.me\/joinchat\/)([\d\w_ // MTProto constants export const SERVICE_NOTIFICATIONS_USER_ID = 777000; +export const REPLIES_USER_ID = 1271266957; export const ALL_FOLDER_ID = 0; export const ARCHIVED_FOLDER_ID = 1; export const DELETED_COMMENTS_CHANNEL_ID = 777; diff --git a/src/global/types.ts b/src/global/types.ts index e410fe5e3..945c74b13 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -500,7 +500,7 @@ export type ActionTypes = ( 'openStickerSetShortName' | // bots 'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' | - 'resetInlineBot' | 'restartBot' | + 'resetInlineBot' | 'restartBot' | 'startBot' | // misc 'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'closeAudioPlayer' | 'openPollModal' | 'closePollModal' | 'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' | diff --git a/src/hooks/useMouseInside.ts b/src/hooks/useMouseInside.ts index 1d2ee7229..32107f454 100644 --- a/src/hooks/useMouseInside.ts +++ b/src/hooks/useMouseInside.ts @@ -8,7 +8,13 @@ let closeTimeout: number | undefined; export default function useMouseInside( isOpen: boolean, onClose: NoneToVoidFunction, menuCloseTimeout = MENU_CLOSE_TIMEOUT, isDisabled = false, ) { - const isMouseInside = useRef(false); + const isMouseInside = useRef(true); + + useEffect(() => { + if (isOpen) { + isMouseInside.current = true; + } + }, [isOpen]); useEffect(() => { if (closeTimeout) { diff --git a/src/lib/gramjs/client/auth.ts b/src/lib/gramjs/client/auth.ts index 065ac2511..e37a064cb 100644 --- a/src/lib/gramjs/client/auth.ts +++ b/src/lib/gramjs/client/auth.ts @@ -50,7 +50,6 @@ export async function authFlow( client._log.info('Signed in successfully as', utils.getDisplayName(me)); } - export async function checkAuthorization(client: TelegramClient) { try { await client.invoke(new Api.updates.GetState()); diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index 90ec48117..21b3848a0 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1000,6 +1000,7 @@ messages.importChatInvite#6c50051c hash:string = Updates; messages.getStickerSet#2619a90e stickerset:InputStickerSet = messages.StickerSet; messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult; messages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool; +messages.startBot#e6df7378 bot:InputUser peer:InputPeer random_id:long start_param:string = Updates; messages.migrateChat#15a3b8e3 chat_id:int = Updates; messages.searchGlobal#4bc6589a flags:# folder_id:flags.0?int q:string filter:MessagesFilter min_date:int max_date:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document; diff --git a/src/lib/gramjs/tl/static/api.reduced.tl b/src/lib/gramjs/tl/static/api.reduced.tl index d3e109281..7af017bd8 100644 --- a/src/lib/gramjs/tl/static/api.reduced.tl +++ b/src/lib/gramjs/tl/static/api.reduced.tl @@ -1001,6 +1001,7 @@ messages.importChatInvite#6c50051c hash:string = Updates; messages.getStickerSet#2619a90e stickerset:InputStickerSet = messages.StickerSet; messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult; messages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool; +messages.startBot#e6df7378 bot:InputUser peer:InputPeer random_id:long start_param:string = Updates; messages.migrateChat#15a3b8e3 chat_id:int = Updates; messages.searchGlobal#4bc6589a flags:# folder_id:flags.0?int q:string filter:MessagesFilter min_date:int max_date:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document; @@ -1086,4 +1087,4 @@ langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDiffer langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector = Vector; langpack.getLanguages#42c6978f lang_pack:string = Vector; folders.editPeerFolders#6847d0ab folder_peers:Vector = Updates; -// LAYER 128 \ No newline at end of file +// LAYER 128 diff --git a/src/modules/actions/api/bots.ts b/src/modules/actions/api/bots.ts index fdb1ea689..1341603f3 100644 --- a/src/modules/actions/api/bots.ts +++ b/src/modules/actions/api/bots.ts @@ -200,7 +200,7 @@ addReducer('sendInlineBotResult', (global, actions, payload) => { }); }); -addReducer('resetInlineBot', ((global, actions, payload) => { +addReducer('resetInlineBot', (global, actions, payload) => { const { username } = payload; let inlineBotData = global.inlineBots.byUsername[username]; @@ -219,7 +219,23 @@ addReducer('resetInlineBot', ((global, actions, payload) => { }; setGlobal(replaceInlineBotSettings(global, username, inlineBotData)); -})); +}); + +addReducer('startBot', (global, actions, payload) => { + const { botId, param } = payload; + + const bot = selectUser(global, botId); + if (!bot) { + return; + } + + (async () => { + await callApi('startBot', { + bot, + startParam: param, + }); + })(); +}); async function searchInlineBot({ username, diff --git a/src/modules/actions/apiUpdaters/initial.ts b/src/modules/actions/apiUpdaters/initial.ts index f2e53147a..d4f45a8d5 100644 --- a/src/modules/actions/apiUpdaters/initial.ts +++ b/src/modules/actions/apiUpdaters/initial.ts @@ -57,7 +57,7 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { onUpdateCurrentUser(update); break; - case 'error': + case 'error': { if (update.error.message === 'SESSION_REVOKED') { actions.signOut(); } @@ -70,6 +70,7 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { } break; + } } }); diff --git a/src/styles/Telegram T.json b/src/styles/Telegram T.json index f30da8388..cc0bc7ed8 100644 --- a/src/styles/Telegram T.json +++ b/src/styles/Telegram T.json @@ -2,18 +2,34 @@ "metadata": { "name": "Telegram T", "lastOpened": 0, - "created": 1617759787079 + "created": 1628131266337 }, "iconSets": [ { "selection": [ + { + "order": 667, + "id": 34, + "name": "bot-commands-filled", + "prevSize": 32, + "code": 59775, + "tempChar": "" + }, + { + "order": 664, + "id": 33, + "name": "reply-filled", + "prevSize": 32, + "code": 59776, + "tempChar": "" + }, { "order": 656, "id": 32, "name": "bug", "prevSize": 32, "code": 59774, - "tempChar": "" + "tempChar": "" }, { "order": 619, @@ -21,7 +37,7 @@ "name": "data", "prevSize": 32, "code": 59773, - "tempChar": "" + "tempChar": "" }, { "order": 622, @@ -29,7 +45,7 @@ "name": "darkmode", "prevSize": 32, "code": 59769, - "tempChar": "" + "tempChar": "" }, { "order": 623, @@ -37,7 +53,7 @@ "name": "animations", "prevSize": 32, "code": 59770, - "tempChar": "" + "tempChar": "" }, { "order": 626, @@ -45,7 +61,7 @@ "name": "enter", "prevSize": 32, "code": 59771, - "tempChar": "" + "tempChar": "" }, { "order": 627, @@ -53,7 +69,7 @@ "name": "fontsize", "prevSize": 32, "code": 59772, - "tempChar": "" + "tempChar": "" }, { "order": 630, @@ -61,7 +77,7 @@ "name": "permissions", "prevSize": 32, "code": 59766, - "tempChar": "" + "tempChar": "" }, { "order": 631, @@ -69,7 +85,7 @@ "name": "card", "prevSize": 32, "code": 59767, - "tempChar": "" + "tempChar": "" }, { "order": 634, @@ -77,15 +93,15 @@ "name": "truck", "prevSize": 32, "code": 59768, - "tempChar": "" + "tempChar": "" }, { - "order": 635, + "order": 663, "id": 23, "name": "share-filled", "prevSize": 32, "code": 59738, - "tempChar": "" + "tempChar": "" }, { "order": 638, @@ -93,7 +109,7 @@ "name": "bold", "prevSize": 32, "code": 59745, - "tempChar": "" + "tempChar": "" }, { "order": 639, @@ -101,7 +117,7 @@ "name": "bot-command", "prevSize": 32, "code": 59746, - "tempChar": "" + "tempChar": "" }, { "order": 642, @@ -109,7 +125,7 @@ "name": "calendar-filter", "prevSize": 32, "code": 59747, - "tempChar": "" + "tempChar": "" }, { "order": 643, @@ -117,7 +133,7 @@ "name": "comments", "prevSize": 32, "code": 59748, - "tempChar": "" + "tempChar": "" }, { "order": 645, @@ -125,7 +141,7 @@ "name": "comments-sticker", "prevSize": 32, "code": 59749, - "tempChar": "" + "tempChar": "" }, { "order": 646, @@ -133,7 +149,7 @@ "name": "arrow-down", "prevSize": 32, "code": 59750, - "tempChar": "" + "tempChar": "" }, { "order": 647, @@ -141,7 +157,7 @@ "name": "email", "prevSize": 32, "code": 59751, - "tempChar": "" + "tempChar": "" }, { "order": 648, @@ -149,7 +165,7 @@ "name": "italic", "prevSize": 32, "code": 59752, - "tempChar": "" + "tempChar": "" }, { "order": 620, @@ -157,7 +173,7 @@ "name": "link", "prevSize": 32, "code": 59753, - "tempChar": "" + "tempChar": "" }, { "order": 621, @@ -165,7 +181,7 @@ "name": "mention", "prevSize": 32, "code": 59754, - "tempChar": "" + "tempChar": "" }, { "order": 624, @@ -173,7 +189,7 @@ "name": "monospace", "prevSize": 32, "code": 59755, - "tempChar": "" + "tempChar": "" }, { "order": 625, @@ -181,7 +197,7 @@ "name": "next", "prevSize": 32, "code": 59756, - "tempChar": "" + "tempChar": "" }, { "order": 628, @@ -189,7 +205,7 @@ "name": "password-off", "prevSize": 32, "code": 59757, - "tempChar": "" + "tempChar": "" }, { "order": 629, @@ -197,7 +213,7 @@ "name": "pin-list", "prevSize": 32, "code": 59758, - "tempChar": "" + "tempChar": "" }, { "order": 632, @@ -205,7 +221,7 @@ "name": "previous", "prevSize": 32, "code": 59759, - "tempChar": "" + "tempChar": "" }, { "order": 633, @@ -213,7 +229,7 @@ "name": "replace", "prevSize": 32, "code": 59760, - "tempChar": "" + "tempChar": "" }, { "order": 636, @@ -221,7 +237,7 @@ "name": "schedule", "prevSize": 32, "code": 59761, - "tempChar": "" + "tempChar": "" }, { "order": 637, @@ -229,7 +245,7 @@ "name": "strikethrough", "prevSize": 32, "code": 59762, - "tempChar": "" + "tempChar": "" }, { "order": 640, @@ -237,7 +253,7 @@ "name": "underlined", "prevSize": 32, "code": 59763, - "tempChar": "" + "tempChar": "" }, { "order": 641, @@ -245,7 +261,7 @@ "name": "zoom-in", "prevSize": 32, "code": 59764, - "tempChar": "" + "tempChar": "" }, { "order": 649, @@ -253,20 +269,50 @@ "name": "zoom-out", "prevSize": 32, "code": 59765, - "tempChar": "" + "tempChar": "" } ], "id": 2, "metadata": { "name": "Untitled Set", "importSize": { - "width": 768, - "height": 768 + "width": 22, + "height": 22 } }, "height": 1024, "prevSize": 32, "icons": [ + { + "id": 34, + "paths": [ + "M698.182 0c179.944 0 325.818 145.874 325.818 325.818v372.364c0 179.944-145.874 325.818-325.818 325.818h-372.364c-179.944 0-325.818-145.874-325.818-325.818v-372.364c0-179.944 145.874-325.818 325.818-325.818zM751.377 651.636l-483.406 0.313c-19.842 2.689-35.243 22.362-35.243 46.232 0 25.706 17.862 46.545 39.896 46.545l483.406-0.313c19.842-2.689 35.243-22.362 35.243-46.232 0-25.706-17.862-46.545-39.896-46.545zM751.377 465.455l-483.406 0.313c-19.842 2.689-35.243 22.362-35.243 46.232 0 25.706 17.862 46.545 39.896 46.545l483.406-0.313c19.842-2.689 35.243-22.362 35.243-46.232 0-25.706-17.862-46.545-39.896-46.545zM751.377 279.273l-483.406 0.313c-19.842 2.689-35.243 22.362-35.243 46.232 0 25.706 17.862 46.545 39.896 46.545l483.406-0.313c19.842-2.689 35.243-22.362 35.243-46.232 0-25.706-17.862-46.545-39.896-46.545z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "bot-commands-filled" + ] + }, + { + "id": 33, + "paths": [ + "M499.091 204.599c0-25.135-20.376-45.511-45.511-45.511-11.451 0-22.48 4.316-30.889 12.088l-332.624 307.401c-18.459 17.060-19.595 45.853-1.952 64.919l1.952 1.929 332.624 307.401c18.459 17.060 47.253 15.925 64.313-2.535 7.771-8.409 12.088-19.439 12.088-30.889v-140.747c171.605 4.143 288.836 58.904 354.051 163.311l6.231 10.368c8.549 14.833 27.505 19.928 42.34 11.379 9.467-5.457 15.363-15.495 15.517-26.421 4.404-311.701-132.133-477.967-404.411-490.876l-13.727-0.517v-141.297l-0.003-0.003z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 24, + "tags": [ + "reply-filled" + ] + }, { "id": 32, "paths": [ @@ -2289,7 +2335,7 @@ "name": "select", "prevSize": 32, "code": 59744, - "tempChar": "" + "tempChar": "" }, { "order": 480, @@ -2297,7 +2343,7 @@ "name": "folder", "prevSize": 32, "code": 59667, - "tempChar": "" + "tempChar": "" }, { "order": 481, @@ -2305,7 +2351,7 @@ "name": "bots", "prevSize": 32, "code": 59669, - "tempChar": "" + "tempChar": "" }, { "order": 482, @@ -2313,7 +2359,7 @@ "name": "calendar", "prevSize": 32, "code": 59670, - "tempChar": "" + "tempChar": "" }, { "order": 483, @@ -2321,7 +2367,7 @@ "name": "cloud-download", "prevSize": 32, "code": 59671, - "tempChar": "" + "tempChar": "" }, { "order": 484, @@ -2329,7 +2375,7 @@ "name": "colorize", "prevSize": 32, "code": 59672, - "tempChar": "" + "tempChar": "" }, { "order": 651, @@ -2337,7 +2383,7 @@ "name": "forward", "prevSize": 32, "code": 59687, - "tempChar": "" + "tempChar": "" }, { "order": 650, @@ -2345,7 +2391,7 @@ "name": "reply", "prevSize": 32, "code": 59719, - "tempChar": "" + "tempChar": "" }, { "order": 487, @@ -2353,7 +2399,7 @@ "name": "help", "prevSize": 32, "code": 59690, - "tempChar": "" + "tempChar": "" }, { "order": 488, @@ -2361,7 +2407,7 @@ "name": "info", "prevSize": 32, "code": 59691, - "tempChar": "" + "tempChar": "" }, { "order": 489, @@ -2369,7 +2415,7 @@ "name": "info-filled", "prevSize": 32, "code": 59675, - "tempChar": "" + "tempChar": "" }, { "order": 490, @@ -2377,7 +2423,7 @@ "name": "delete-filled", "prevSize": 32, "code": 59676, - "tempChar": "" + "tempChar": "" }, { "order": 491, @@ -2385,7 +2431,7 @@ "name": "delete", "prevSize": 32, "code": 59677, - "tempChar": "" + "tempChar": "" }, { "order": 492, @@ -2393,7 +2439,7 @@ "name": "edit", "prevSize": 32, "code": 59683, - "tempChar": "" + "tempChar": "" }, { "order": 493, @@ -2401,7 +2447,7 @@ "name": "new-chat-filled", "prevSize": 32, "code": 59705, - "tempChar": "" + "tempChar": "" }, { "order": 494, @@ -2409,7 +2455,7 @@ "name": "send", "prevSize": 32, "code": 59722, - "tempChar": "" + "tempChar": "" }, { "order": 495, @@ -2417,7 +2463,7 @@ "name": "send-outline", "prevSize": 32, "code": 59723, - "tempChar": "" + "tempChar": "" }, { "order": 496, @@ -2425,7 +2471,7 @@ "name": "add-user-filled", "prevSize": 32, "code": 59652, - "tempChar": "" + "tempChar": "" }, { "order": 497, @@ -2433,7 +2479,7 @@ "name": "add-user", "prevSize": 32, "code": 59653, - "tempChar": "" + "tempChar": "" }, { "order": 498, @@ -2441,7 +2487,7 @@ "name": "delete-user", "prevSize": 32, "code": 59678, - "tempChar": "" + "tempChar": "" }, { "order": 499, @@ -2449,7 +2495,7 @@ "name": "microphone", "prevSize": 32, "code": 59701, - "tempChar": "" + "tempChar": "" }, { "order": 500, @@ -2457,7 +2503,7 @@ "name": "microphone-alt", "prevSize": 32, "code": 59707, - "tempChar": "" + "tempChar": "" }, { "order": 501, @@ -2465,7 +2511,7 @@ "name": "poll", "prevSize": 32, "code": 59704, - "tempChar": "" + "tempChar": "" }, { "order": 502, @@ -2473,7 +2519,7 @@ "name": "revote", "prevSize": 32, "code": 59706, - "tempChar": "" + "tempChar": "" }, { "order": 503, @@ -2481,7 +2527,7 @@ "name": "photo", "prevSize": 32, "code": 59712, - "tempChar": "" + "tempChar": "" }, { "order": 504, @@ -2489,7 +2535,7 @@ "name": "document", "prevSize": 32, "code": 59679, - "tempChar": "" + "tempChar": "" }, { "order": 505, @@ -2497,7 +2543,7 @@ "name": "camera", "prevSize": 32, "code": 59662, - "tempChar": "" + "tempChar": "" }, { "order": 506, @@ -2505,7 +2551,7 @@ "name": "camera-add", "prevSize": 32, "code": 59663, - "tempChar": "" + "tempChar": "" }, { "order": 507, @@ -2513,7 +2559,7 @@ "name": "logout", "prevSize": 32, "code": 59698, - "tempChar": "" + "tempChar": "" }, { "order": 508, @@ -2521,7 +2567,7 @@ "name": "saved-messages", "prevSize": 32, "code": 59720, - "tempChar": "" + "tempChar": "" }, { "order": 509, @@ -2529,7 +2575,7 @@ "name": "settings", "prevSize": 32, "code": 59726, - "tempChar": "" + "tempChar": "" }, { "order": 652, @@ -2537,7 +2583,7 @@ "name": "phone", "prevSize": 32, "code": 59711, - "tempChar": "" + "tempChar": "" }, { "order": 653, @@ -2545,7 +2591,7 @@ "name": "attach", "prevSize": 32, "code": 59657, - "tempChar": "" + "tempChar": "" }, { "order": 512, @@ -2553,7 +2599,7 @@ "name": "copy", "prevSize": 32, "code": 59674, - "tempChar": "" + "tempChar": "" }, { "order": 513, @@ -2561,7 +2607,7 @@ "name": "channel", "prevSize": 32, "code": 59665, - "tempChar": "" + "tempChar": "" }, { "order": 514, @@ -2569,7 +2615,7 @@ "name": "group", "prevSize": 32, "code": 59689, - "tempChar": "" + "tempChar": "" }, { "order": 515, @@ -2577,7 +2623,7 @@ "name": "user", "prevSize": 32, "code": 59737, - "tempChar": "" + "tempChar": "" }, { "order": 516, @@ -2585,7 +2631,7 @@ "name": "non-contacts", "prevSize": 32, "code": 59688, - "tempChar": "" + "tempChar": "" }, { "order": 517, @@ -2593,7 +2639,7 @@ "name": "active-sessions", "prevSize": 32, "code": 59650, - "tempChar": "" + "tempChar": "" }, { "order": 518, @@ -2601,7 +2647,7 @@ "name": "admin", "prevSize": 32, "code": 59654, - "tempChar": "" + "tempChar": "" }, { "order": 519, @@ -2609,7 +2655,7 @@ "name": "download", "prevSize": 32, "code": 59681, - "tempChar": "" + "tempChar": "" }, { "order": 520, @@ -2617,7 +2663,7 @@ "name": "location", "prevSize": 32, "code": 59696, - "tempChar": "" + "tempChar": "" }, { "order": 521, @@ -2625,7 +2671,7 @@ "name": "stop", "prevSize": 32, "code": 59730, - "tempChar": "" + "tempChar": "" }, { "order": 523, @@ -2633,7 +2679,7 @@ "name": "archive", "prevSize": 32, "code": 59656, - "tempChar": "" + "tempChar": "" }, { "order": 524, @@ -2641,7 +2687,7 @@ "name": "unarchive", "prevSize": 32, "code": 59731, - "tempChar": "" + "tempChar": "" }, { "order": 525, @@ -2649,7 +2695,7 @@ "name": "readchats", "prevSize": 32, "code": 59699, - "tempChar": "" + "tempChar": "" }, { "order": 526, @@ -2657,7 +2703,7 @@ "name": "unread", "prevSize": 32, "code": 59735, - "tempChar": "" + "tempChar": "" }, { "order": 654, @@ -2665,7 +2711,7 @@ "name": "message", "prevSize": 32, "code": 59700, - "tempChar": "" + "tempChar": "" }, { "order": 659, @@ -2673,7 +2719,7 @@ "name": "lock", "prevSize": 32, "code": 59697, - "tempChar": "" + "tempChar": "" }, { "order": 529, @@ -2681,7 +2727,7 @@ "name": "unlock", "prevSize": 32, "code": 59732, - "tempChar": "" + "tempChar": "" }, { "order": 530, @@ -2689,7 +2735,7 @@ "name": "mute", "prevSize": 32, "code": 59703, - "tempChar": "" + "tempChar": "" }, { "order": 531, @@ -2697,7 +2743,7 @@ "name": "unmute", "prevSize": 32, "code": 59733, - "tempChar": "" + "tempChar": "" }, { "order": 532, @@ -2705,7 +2751,7 @@ "name": "pin", "prevSize": 32, "code": 59713, - "tempChar": "" + "tempChar": "" }, { "order": 533, @@ -2713,7 +2759,7 @@ "name": "unpin", "prevSize": 32, "code": 59734, - "tempChar": "" + "tempChar": "" }, { "order": 534, @@ -2721,7 +2767,7 @@ "name": "smallscreen", "prevSize": 32, "code": 59742, - "tempChar": "" + "tempChar": "" }, { "order": 535, @@ -2729,7 +2775,7 @@ "name": "fullscreen", "prevSize": 32, "code": 59743, - "tempChar": "" + "tempChar": "" }, { "order": 536, @@ -2737,7 +2783,7 @@ "name": "large-pause", "prevSize": 32, "code": 59694, - "tempChar": "" + "tempChar": "" }, { "order": 537, @@ -2745,7 +2791,7 @@ "name": "large-play", "prevSize": 32, "code": 59695, - "tempChar": "" + "tempChar": "" }, { "order": 538, @@ -2753,7 +2799,7 @@ "name": "pause", "prevSize": 32, "code": 59709, - "tempChar": "" + "tempChar": "" }, { "order": 539, @@ -2761,7 +2807,7 @@ "name": "play", "prevSize": 32, "code": 59715, - "tempChar": "" + "tempChar": "" }, { "order": 540, @@ -2769,7 +2815,7 @@ "name": "channelviews", "prevSize": 32, "code": 59666, - "tempChar": "" + "tempChar": "" }, { "order": 541, @@ -2777,7 +2823,7 @@ "name": "message-succeeded", "prevSize": 32, "code": 59648, - "tempChar": "" + "tempChar": "" }, { "order": 657, @@ -2785,7 +2831,7 @@ "name": "message-read", "prevSize": 32, "code": 59649, - "tempChar": "" + "tempChar": "" }, { "order": 543, @@ -2793,7 +2839,7 @@ "name": "message-pending", "prevSize": 32, "code": 59724, - "tempChar": "" + "tempChar": "" }, { "order": 544, @@ -2801,7 +2847,7 @@ "name": "message-failed", "prevSize": 32, "code": 59725, - "tempChar": "" + "tempChar": "" }, { "order": 545, @@ -2809,7 +2855,7 @@ "name": "favorite", "prevSize": 32, "code": 59710, - "tempChar": "" + "tempChar": "" }, { "order": 546, @@ -2817,7 +2863,7 @@ "name": "keyboard", "prevSize": 32, "code": 59716, - "tempChar": "" + "tempChar": "" }, { "order": 547, @@ -2825,7 +2871,7 @@ "name": "delete-left", "prevSize": 32, "code": 59717, - "tempChar": "" + "tempChar": "" }, { "order": 548, @@ -2833,7 +2879,7 @@ "name": "recent", "prevSize": 32, "code": 59718, - "tempChar": "" + "tempChar": "" }, { "order": 549, @@ -2841,7 +2887,7 @@ "name": "gifs", "prevSize": 32, "code": 59727, - "tempChar": "" + "tempChar": "" }, { "order": 550, @@ -2849,7 +2895,7 @@ "name": "stickers", "prevSize": 32, "code": 59739, - "tempChar": "" + "tempChar": "" }, { "order": 551, @@ -2857,7 +2903,7 @@ "name": "smile", "prevSize": 32, "code": 59728, - "tempChar": "" + "tempChar": "" }, { "order": 552, @@ -2865,7 +2911,7 @@ "name": "animals", "prevSize": 32, "code": 59655, - "tempChar": "" + "tempChar": "" }, { "order": 553, @@ -2873,7 +2919,7 @@ "name": "eats", "prevSize": 32, "code": 59682, - "tempChar": "" + "tempChar": "" }, { "order": 554, @@ -2881,7 +2927,7 @@ "name": "sport", "prevSize": 32, "code": 59729, - "tempChar": "" + "tempChar": "" }, { "order": 555, @@ -2889,7 +2935,7 @@ "name": "car", "prevSize": 32, "code": 59664, - "tempChar": "" + "tempChar": "" }, { "order": 556, @@ -2897,7 +2943,7 @@ "name": "lamp", "prevSize": 32, "code": 59692, - "tempChar": "" + "tempChar": "" }, { "order": 557, @@ -2905,7 +2951,7 @@ "name": "language", "prevSize": 32, "code": 59693, - "tempChar": "" + "tempChar": "" }, { "order": 558, @@ -2913,7 +2959,7 @@ "name": "flag", "prevSize": 32, "code": 59686, - "tempChar": "" + "tempChar": "" }, { "order": 559, @@ -2921,7 +2967,7 @@ "name": "more", "prevSize": 32, "code": 59702, - "tempChar": "" + "tempChar": "" }, { "order": 560, @@ -2929,7 +2975,7 @@ "name": "search", "prevSize": 32, "code": 59721, - "tempChar": "" + "tempChar": "" }, { "order": 561, @@ -2937,7 +2983,7 @@ "name": "remove", "prevSize": 32, "code": 59740, - "tempChar": "" + "tempChar": "" }, { "order": 562, @@ -2945,7 +2991,7 @@ "name": "add", "prevSize": 32, "code": 59651, - "tempChar": "" + "tempChar": "" }, { "order": 563, @@ -2953,7 +2999,7 @@ "name": "check", "prevSize": 32, "code": 59668, - "tempChar": "" + "tempChar": "" }, { "order": 564, @@ -2961,7 +3007,7 @@ "name": "close", "prevSize": 32, "code": 59673, - "tempChar": "" + "tempChar": "" }, { "order": 610, @@ -2969,7 +3015,7 @@ "name": "arrow-left", "prevSize": 32, "code": 59661, - "tempChar": "" + "tempChar": "" }, { "order": 566, @@ -2977,7 +3023,7 @@ "name": "arrow-right", "prevSize": 32, "code": 59708, - "tempChar": "" + "tempChar": "" }, { "order": 567, @@ -2985,7 +3031,7 @@ "name": "down", "prevSize": 32, "code": 59680, - "tempChar": "" + "tempChar": "" }, { "order": 568, @@ -2993,7 +3039,7 @@ "name": "up", "prevSize": 32, "code": 59736, - "tempChar": "" + "tempChar": "" }, { "order": 569, @@ -3001,7 +3047,7 @@ "name": "eye-closed", "prevSize": 32, "code": 59685, - "tempChar": "" + "tempChar": "" }, { "order": 570, @@ -3009,7 +3055,7 @@ "name": "eye", "prevSize": 32, "code": 59684, - "tempChar": "" + "tempChar": "" }, { "order": 571, @@ -3017,7 +3063,7 @@ "name": "muted-chat", "prevSize": 32, "code": 59741, - "tempChar": "" + "tempChar": "" }, { "order": 572, @@ -3025,7 +3071,7 @@ "name": "avatar-archived-chats", "prevSize": 32, "code": 59658, - "tempChar": "" + "tempChar": "" }, { "order": 573, @@ -3033,7 +3079,7 @@ "name": "avatar-deleted-account", "prevSize": 32, "code": 59659, - "tempChar": "" + "tempChar": "" }, { "order": 574, @@ -3041,7 +3087,7 @@ "name": "avatar-saved-messages", "prevSize": 32, "code": 59660, - "tempChar": "" + "tempChar": "" }, { "order": 575, @@ -3049,7 +3095,7 @@ "name": "pinned-chat", "prevSize": 32, "code": 59714, - "tempChar": "" + "tempChar": "" } ], "prevSize": 32, diff --git a/src/styles/icons.scss b/src/styles/icons.scss index c9dbd538b..8b217394b 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -1,7 +1,7 @@ @font-face { font-family: 'icomoon'; - src: url('../assets/fonts/icomoon.woff2?7tsg0v') format('woff2'), - url('../assets/fonts/icomoon.woff?7tsg0v') format('woff'); + src: url('../assets/fonts/icomoon.woff2?n9djnk') format('woff2'), + url('../assets/fonts/icomoon.woff?n9djnk') format('woff'); font-weight: normal; font-style: normal; font-display: block; @@ -32,6 +32,12 @@ } } +.icon-bot-commands-filled:before { + content: "\e97f"; +} +.icon-reply-filled:before { + content: "\e980"; +} .icon-bug:before { content: "\e97e"; } diff --git a/src/util/getReadableErrorText.ts b/src/util/getReadableErrorText.ts index a430499f5..0f8a06172 100644 --- a/src/util/getReadableErrorText.ts +++ b/src/util/getReadableErrorText.ts @@ -120,5 +120,5 @@ export default function getReadableErrorText(error: ApiError) { } export function getShippingError(error: ApiError): ApiFieldError | undefined { - return SHIPPING_ERRORS[error.message]; + return SHIPPING_ERRORS[error.message]; }