Composer: Add Bot Command Menu and Bot Command Tooltip (#1364)

This commit is contained in:
Alexander Zinchuk 2021-08-20 23:47:01 +03:00
parent ad36326c83
commit a185fd9407
44 changed files with 887 additions and 321 deletions

View File

@ -1,6 +1,6 @@
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import { import {
ApiBotInlineMediaResult, ApiBotInlineResult, ApiInlineResultType, ApiWebDocument, ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm, ApiInlineResultType, ApiWebDocument,
} from '../../types'; } from '../../types';
import { pick } from '../../../util/iteratees'; 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 { function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDocument | undefined {
return document ? pick(document, ['url', 'mimeType']) : undefined; return document ? pick(document, ['url', 'mimeType']) : undefined;
} }

View File

@ -3,6 +3,7 @@ import {
ApiChat, ApiChat,
ApiChatAdminRights, ApiChatAdminRights,
ApiChatBannedRights, ApiChatBannedRights,
ApiBotCommand,
ApiChatFolder, ApiChatFolder,
ApiChatMember, ApiChatMember,
ApiRestrictionReason, ApiRestrictionReason,
@ -376,3 +377,14 @@ export function buildApiChatFolderFromSuggested({
description, 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[]);
}

View File

@ -1,5 +1,7 @@
import { Api as GramJs } from '../../../lib/gramjs'; 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 { export function buildApiUserFromFull(mtpUserFull: GramJs.UserFull): ApiUser {
const { const {
@ -14,6 +16,7 @@ export function buildApiUserFromFull(mtpUserFull: GramJs.UserFull): ApiUser {
pinnedMessageId: pinnedMsgId, pinnedMessageId: pinnedMsgId,
isBlocked: Boolean(blocked), isBlocked: Boolean(blocked),
...(botInfo && { botDescription: botInfo.description }), ...(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' }; return { type: 'userStatusLastMonth' };
} }
} }
function buildApiBotCommands(botId: number, botInfo: GramJs.BotInfo) {
return botInfo.commands.map(({ command, description }) => ({
botId,
command,
description,
})) as ApiBotCommand[];
}

View File

@ -1,15 +1,14 @@
import BigInt from 'big-integer'; import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import { ApiBotInlineSwitchPm, ApiChat, ApiUser } from '../../types'; import { ApiChat, ApiUser } from '../../types';
import localDb from '../localDb'; import localDb from '../localDb';
import { invokeRequest } from './client'; import { invokeRequest } from './client';
import { buildInputPeer, calculateResultHash, generateRandomBigInt } from '../gramjsBuilders'; import { buildInputPeer, calculateResultHash, generateRandomBigInt } from '../gramjsBuilders';
import { buildApiUser } from '../apiBuilders/users'; import { buildApiUser } from '../apiBuilders/users';
import { buildApiBotInlineMediaResult, buildApiBotInlineResult } from '../apiBuilders/bots'; import { buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm } from '../apiBuilders/bots';
import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { pick } from '../../../util/iteratees';
export function init() { export function init() {
} }
@ -92,7 +91,7 @@ export async function fetchInlineBotResults({
isGallery: Boolean(result.gallery), isGallery: Boolean(result.gallery),
help: bot.botPlaceholder, help: bot.botPlaceholder,
nextOffset: getInlineBotResultsNextOffset(bot.username, result.nextOffset), nextOffset: getInlineBotResultsNextOffset(bot.username, result.nextOffset),
switchPm: buildSwitchPm(result.switchPm), switchPm: buildBotSwitchPm(result.switchPm),
users: result.users.map(buildApiUser).filter<ApiUser>(Boolean as any), users: result.users.map(buildApiUser).filter<ApiUser>(Boolean as any),
results: processInlineBotResult(String(result.queryId), result.results), results: processInlineBotResult(String(result.queryId), result.results),
}; };
@ -118,8 +117,20 @@ export async function sendInlineBotResult({
}), true); }), true);
} }
function buildSwitchPm(switchPm?: GramJs.InlineBotSwitchPM) { export async function startBot({
return switchPm ? pick(switchPm, ['text', 'startParam']) as ApiBotInlineSwitchPm : undefined; 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[]) { function processInlineBotResult(queryId: string, results: GramJs.TypeBotInlineResult[]) {

View File

@ -22,6 +22,7 @@ import {
getApiChatIdFromMtpPeer, getApiChatIdFromMtpPeer,
buildApiChatFolder, buildApiChatFolder,
buildApiChatFolderFromSuggested, buildApiChatFolderFromSuggested,
buildApiChatBotCommands,
} from '../apiBuilders/chats'; } from '../apiBuilders/chats';
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages'; import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
import { buildApiUser } from '../apiBuilders/users'; import { buildApiUser } from '../apiBuilders/users';
@ -317,10 +318,12 @@ async function getFullChatInfo(chatId: number): Promise<{
about, about,
participants, participants,
exportedInvite, exportedInvite,
botInfo,
} = result.fullChat; } = result.fullChat;
const members = buildChatMembers(participants); const members = buildChatMembers(participants);
const adminMembers = members ? members.filter(({ isAdmin, isOwner }) => isAdmin || isOwner) : undefined; const adminMembers = members ? members.filter(({ isAdmin, isOwner }) => isAdmin || isOwner) : undefined;
const botCommands = botInfo ? buildApiChatBotCommands(botInfo) : undefined;
return { return {
fullInfo: { fullInfo: {
@ -328,6 +331,7 @@ async function getFullChatInfo(chatId: number): Promise<{
members, members,
adminMembers, adminMembers,
canViewMembers: true, canViewMembers: true,
botCommands,
...(exportedInvite && { ...(exportedInvite && {
inviteLink: exportedInvite.link, inviteLink: exportedInvite.link,
}), }),
@ -361,6 +365,7 @@ async function getFullChannelInfo(
linkedChatId, linkedChatId,
hiddenPrehistory, hiddenPrehistory,
call, call,
botInfo,
} = result.fullChat; } = result.fullChat;
const inviteLink = exportedInvite instanceof GramJs.ChatInviteExported const inviteLink = exportedInvite instanceof GramJs.ChatInviteExported
@ -374,6 +379,7 @@ async function getFullChannelInfo(
const { members: adminMembers, users: adminUsers } = ( const { members: adminMembers, users: adminUsers } = (
canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'admin') canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'admin')
) || {}; ) || {};
const botCommands = botInfo ? buildApiChatBotCommands(botInfo) : undefined;
return { return {
fullInfo: { fullInfo: {
@ -395,6 +401,7 @@ async function getFullChannelInfo(
adminMembers, adminMembers,
groupCallId: call ? call.id.toString() : undefined, groupCallId: call ? call.id.toString() : undefined,
linkedChatId: linkedChatId ? getApiChatIdFromMtpPeer({ chatId: linkedChatId } as GramJs.TypePeer) : undefined, linkedChatId: linkedChatId ? getApiChatIdFromMtpPeer({ chatId: linkedChatId } as GramJs.TypePeer) : undefined,
botCommands,
}, },
users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])], users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])],
}; };

View File

@ -55,7 +55,7 @@ export {
} from './twoFaSettings'; } from './twoFaSettings';
export { export {
answerCallbackButton, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, sendInlineBotResult, answerCallbackButton, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, sendInlineBotResult, startBot,
} from './bots'; } from './bots';
export { export {

View File

@ -38,3 +38,9 @@ export interface ApiBotInlineSwitchPm {
text: string; text: string;
startParam: string; startParam: string;
} }
export interface ApiBotCommand {
botId: number;
command: string;
description: string;
}

View File

@ -1,4 +1,5 @@
import { ApiMessage, ApiPhoto } from './messages'; import { ApiMessage, ApiPhoto } from './messages';
import { ApiBotCommand } from './bots';
type ApiChatType = ( type ApiChatType = (
'chatTypePrivate' | 'chatTypeSecret' | 'chatTypePrivate' | 'chatTypeSecret' |
@ -96,6 +97,7 @@ export interface ApiChatFullInfo {
maxMessageId?: number; maxMessageId?: number;
}; };
linkedChatId?: number; linkedChatId?: number;
botCommands?: ApiBotCommand[];
} }
export interface ApiChatMember { export interface ApiChatMember {

View File

@ -1,4 +1,5 @@
import { ApiPhoto } from './messages'; import { ApiPhoto } from './messages';
import { ApiBotCommand } from './bots';
export interface ApiUser { export interface ApiUser {
id: number; id: number;
@ -28,6 +29,7 @@ export interface ApiUserFullInfo {
commonChatsCount?: number; commonChatsCount?: number;
botDescription?: string; botDescription?: string;
pinnedMessageId?: number; pinnedMessageId?: number;
botCommands?: ApiBotCommand[];
} }
export type ApiUserType = 'userTypeBot' | 'userTypeRegular' | 'userTypeDeleted' | 'userTypeUnknown'; export type ApiUserType = 'userTypeBot' | 'userTypeRegular' | 'userTypeDeleted' | 'userTypeUnknown';

Binary file not shown.

Binary file not shown.

View File

@ -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 PollModal } from '../components/middle/composer/PollModal';
export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu'; export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu';
export { default as AttachMenu } from '../components/middle/composer/AttachMenu'; 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 MentionTooltip } from '../components/middle/composer/MentionTooltip';
export { default as StickerTooltip } from '../components/middle/composer/StickerTooltip'; export { default as StickerTooltip } from '../components/middle/composer/StickerTooltip';
export { default as CustomSendMenu } from '../components/middle/composer/CustomSendMenu'; export { default as CustomSendMenu } from '../components/middle/composer/CustomSendMenu';

View File

@ -546,10 +546,9 @@ export default memo(withGlobal<OwnProps>(
&& !messageIds && !chat.unreadCount && !focusingId && lastMessage && !lastMessage.groupedId && !messageIds && !chat.unreadCount && !focusingId && lastMessage && !lastMessage.groupedId
); );
const bot = selectChatBot(global, chatId); const chatBot = selectChatBot(global, chatId)!;
let botDescription: string | undefined; let botDescription: string | undefined;
if (selectIsChatBotNotStarted(global, chatId)) { if (selectIsChatBotNotStarted(global, chatId)) {
const chatBot = selectChatBot(global, chatId)!;
if (chatBot.fullInfo) { if (chatBot.fullInfo) {
botDescription = chatBot.fullInfo.botDescription || 'NoMessages'; botDescription = chatBot.fullInfo.botDescription || 'NoMessages';
} else { } else {
@ -565,7 +564,7 @@ export default memo(withGlobal<OwnProps>(
isGroupChat: isChatGroup(chat), isGroupChat: isChatGroup(chat),
isCreator: chat.isCreator, isCreator: chat.isCreator,
isChatWithSelf: selectIsChatWithSelf(global, chatId), isChatWithSelf: selectIsChatWithSelf(global, chatId),
isBot: Boolean(bot), isBot: Boolean(chatBot),
messageIds, messageIds,
messagesById, messagesById,
firstUnreadId: selectFirstUnreadId(global, chatId, threadId), firstUnreadId: selectFirstUnreadId(global, chatId, threadId),

View File

@ -67,9 +67,7 @@ const AttachmentModal: FC<OwnProps> = ({
const lang = useLang(); const lang = useLang();
const { const {
isMentionTooltipOpen, mentionFilter, isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers,
closeMentionTooltip, insertMention,
mentionFilteredUsers,
} = useMentionTooltip( } = useMentionTooltip(
isOpen, isOpen,
caption, caption,
@ -227,7 +225,6 @@ const AttachmentModal: FC<OwnProps> = ({
<MentionTooltip <MentionTooltip
isOpen={isMentionTooltipOpen} isOpen={isMentionTooltipOpen}
onClose={closeMentionTooltip} onClose={closeMentionTooltip}
filter={mentionFilter}
onInsertUserName={insertMention} onInsertUserName={insertMention}
filteredUsers={mentionFilteredUsers} filteredUsers={mentionFilteredUsers}
usersById={usersById} usersById={usersById}

View File

@ -0,0 +1,27 @@
.BotCommand {
margin: 0 !important;
.ListItem-button {
border-radius: 0;
}
.multiline-item {
padding: 0 1rem;
.subtitle {
padding-top: .25rem;
line-height: 1.3125;
}
}
&.with-avatar {
.multiline-item {
padding: 0;
display: flex;
}
.content-inner {
flex: 1;
}
}
}

View File

@ -0,0 +1,47 @@
import React, { FC, memo } from '../../../lib/teact/teact';
import { ApiBotCommand, ApiUser } from '../../../api/types';
import renderText from '../../common/helpers/renderText';
import buildClassName from '../../../util/buildClassName';
import ListItem from '../../ui/ListItem';
import Avatar from '../../common/Avatar';
import './BotCommand.scss';
type OwnProps = {
botCommand: ApiBotCommand;
bot?: ApiUser;
withAvatar?: boolean;
focus?: boolean;
onClick: (botCommand: ApiBotCommand) => void;
};
const BotCommand: FC<OwnProps> = ({
withAvatar,
focus,
botCommand,
bot,
onClick,
}) => {
return (
<ListItem
key={botCommand.command}
className={buildClassName('BotCommand chat-item-clickable scroll-item', withAvatar && 'with-avatar')}
multiline
onClick={() => onClick(botCommand)}
focus={focus}
>
{withAvatar && (
<Avatar size="small" user={bot} />
)}
<div className="content-inner">
<span className="title">/{botCommand.command}</span>
<span className="subtitle">{renderText(botCommand.description)}</span>
</div>
</ListItem>
);
};
export default memo(BotCommand);

View File

@ -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<OwnProps> = (props) => {
const { isOpen } = props;
const BotCommandMenu = useModuleLoader(Bundles.Extra, 'BotCommandMenu', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return BotCommandMenu ? <BotCommandMenu {...props} /> : undefined;
};
export default memo(BotCommandMenuAsync);

View File

@ -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;
}
}
}

View File

@ -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<GlobalActions, 'sendBotCommand'>;
const BotCommandMenu: FC<OwnProps & DispatchProps> = ({
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 (
<Menu
isOpen={isOpen}
positionX="left"
positionY="bottom"
onClose={onClose}
className="BotCommandMenu"
onCloseAnimationEnd={onClose}
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
noCloseOnBackdrop={!IS_TOUCH_ENV}
>
{botCommands.map((botCommand) => (
<BotCommand
key={botCommand.command}
botCommand={botCommand}
onClick={handleClick}
/>
))}
</Menu>
);
};
export default memo(withGlobal<OwnProps>(
undefined,
(setGlobal, actions): DispatchProps => pick(actions, ['sendBotCommand']),
)(BotCommandMenu));

View File

@ -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<OwnProps> = (props) => {
const { isOpen } = props;
const BotCommandTooltip = useModuleLoader(Bundles.Extra, 'BotCommandTooltip', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return BotCommandTooltip ? <BotCommandTooltip {...props} /> : undefined;
};
export default memo(BotCommandTooltipAsync);

View File

@ -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);
}
}

View File

@ -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<number, ApiUser>;
};
type DispatchProps = Pick<GlobalActions, 'sendBotCommand'>;
const BotCommandTooltip: FC<OwnProps & StateProps & DispatchProps> = ({
usersById,
isOpen,
withUsername,
botCommands,
onClick,
onClose,
sendBotCommand,
}) => {
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(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 (
<div className={className} ref={containerRef}>
{renderedCommands && renderedCommands.map((chatBotCommand, index) => (
<BotCommand
key={`${chatBotCommand.botId}_${chatBotCommand.command}`}
botCommand={chatBotCommand}
bot={usersById[chatBotCommand.botId]}
withAvatar
onClick={handleSendCommand}
focus={selectedCommandIndex === index}
/>
))}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => ({
usersById: global.users.byId,
}),
(setGlobal, actions): DispatchProps => pick(actions, ['sendBotCommand']),
)(BotCommandTooltip));

View File

@ -192,13 +192,14 @@
> .Spinner { > .Spinner {
align-self: center; align-self: center;
--spinner-size: 1.5rem; --spinner-size: 1.5rem;
margin-right: -.5rem;
} }
> .Button { > .Button {
flex-shrink: 0; flex-shrink: 0;
background: none !important; background: none !important;
width: 3.375rem; width: 3.5rem;
height: 3.375rem; height: 3.5rem;
margin: 0; margin: 0;
padding: 0.625rem; padding: 0.625rem;
align-self: flex-end; align-self: flex-end;
@ -210,7 +211,17 @@
} }
+ .Button { + .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 { &.scheduled-button i::after {

View File

@ -17,14 +17,16 @@ import {
ApiChatMember, ApiChatMember,
ApiUser, ApiUser,
MAIN_THREAD_ID, MAIN_THREAD_ID,
ApiBotCommand,
} from '../../../api/types'; } from '../../../api/types';
import { InlineBotSettings } from '../../../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 { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment';
import { import {
selectChat, selectChat,
selectIsChatWithBot,
selectIsRightColumnShown, selectIsRightColumnShown,
selectIsInSelectMode, selectIsInSelectMode,
selectNewestMessageWithBotKeyboardButtons, selectNewestMessageWithBotKeyboardButtons,
@ -32,6 +34,7 @@ import {
selectScheduledIds, selectScheduledIds,
selectEditingMessage, selectEditingMessage,
selectIsChatWithSelf, selectIsChatWithSelf,
selectChatBot,
selectChatUser, selectChatUser,
selectChatMessage, selectChatMessage,
} from '../../../modules/selectors'; } from '../../../modules/selectors';
@ -50,8 +53,10 @@ import insertHtmlInSelection from '../../../util/insertHtmlInSelection';
import deleteLastCharacterOutsideSelection from '../../../util/deleteLastCharacterOutsideSelection'; import deleteLastCharacterOutsideSelection from '../../../util/deleteLastCharacterOutsideSelection';
import { pick } from '../../../util/iteratees'; import { pick } from '../../../util/iteratees';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import windowSize from '../../../util/windowSize';
import { isSelectionInsideInput } from './helpers/selection'; import { isSelectionInsideInput } from './helpers/selection';
import applyIosAutoCapitalizationFix from './helpers/applyIosAutoCapitalizationFix'; import applyIosAutoCapitalizationFix from './helpers/applyIosAutoCapitalizationFix';
import { getServerTime } from '../../../util/serverTime';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useVoiceRecording from './hooks/useVoiceRecording'; import useVoiceRecording from './hooks/useVoiceRecording';
@ -65,8 +70,7 @@ import useMentionTooltip from './hooks/useMentionTooltip';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useInlineBotTooltip from './hooks/useInlineBotTooltip'; import useInlineBotTooltip from './hooks/useInlineBotTooltip';
import windowSize from '../../../util/windowSize'; import useBotCommandTooltip from './hooks/useBotCommandTooltip';
import { getServerTime } from '../../../util/serverTime';
import DeleteMessageModal from '../../common/DeleteMessageModal.async'; import DeleteMessageModal from '../../common/DeleteMessageModal.async';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
@ -79,10 +83,12 @@ import MentionTooltip from './MentionTooltip.async';
import CustomSendMenu from './CustomSendMenu.async'; import CustomSendMenu from './CustomSendMenu.async';
import StickerTooltip from './StickerTooltip.async'; import StickerTooltip from './StickerTooltip.async';
import EmojiTooltip from './EmojiTooltip.async'; import EmojiTooltip from './EmojiTooltip.async';
import BotCommandTooltip from './BotCommandTooltip.async';
import BotKeyboardMenu from './BotKeyboardMenu'; import BotKeyboardMenu from './BotKeyboardMenu';
import MessageInput from './MessageInput'; import MessageInput from './MessageInput';
import ComposerEmbeddedMessage from './ComposerEmbeddedMessage'; import ComposerEmbeddedMessage from './ComposerEmbeddedMessage';
import AttachmentModal from './AttachmentModal.async'; import AttachmentModal from './AttachmentModal.async';
import BotCommandMenu from './BotCommandMenu.async';
import PollModal from './PollModal.async'; import PollModal from './PollModal.async';
import DropArea, { DropAreaState } from './DropArea.async'; import DropArea, { DropAreaState } from './DropArea.async';
import WebPagePreview from './WebPagePreview'; import WebPagePreview from './WebPagePreview';
@ -133,6 +139,8 @@ type StateProps = {
topInlineBotIds?: number[]; topInlineBotIds?: number[];
isInlineBotLoading: boolean; isInlineBotLoading: boolean;
inlineBots?: Record<string, false | InlineBotSettings>; inlineBots?: Record<string, false | InlineBotSettings>;
botCommands?: ApiBotCommand[] | false;
chatBotCommands?: ApiBotCommand[];
} & Pick<GlobalState, 'connectionState'>; } & Pick<GlobalState, 'connectionState'>;
type DispatchProps = Pick<GlobalActions, ( type DispatchProps = Pick<GlobalActions, (
@ -197,6 +205,8 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
recentEmojis, recentEmojis,
inlineBots, inlineBots,
isInlineBotLoading, isInlineBotLoading,
botCommands,
chatBotCommands,
sendMessage, sendMessage,
editMessage, editMessage,
saveDraft, saveDraft,
@ -259,6 +269,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
const [attachments, setAttachments] = useState<ApiAttachment[]>([]); const [attachments, setAttachments] = useState<ApiAttachment[]>([]);
const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag(); const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag();
const [isBotCommandMenuOpen, openBotCommandMenu, closeBotCommandMenu] = useFlag();
const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag(); const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag();
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag(); const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
@ -283,9 +294,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
const canShowCustomSendMenu = !shouldSchedule; const canShowCustomSendMenu = !shouldSchedule;
const { const {
isMentionTooltipOpen, mentionFilter, isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers,
closeMentionTooltip, insertMention,
mentionFilteredUsers,
} = useMentionTooltip( } = useMentionTooltip(
!attachments.length, !attachments.length,
html, html,
@ -313,6 +322,17 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
inlineBots, inlineBots,
); );
const {
isOpen: isBotCommandTooltipOpen,
close: closeBotCommandTooltip,
filteredBotCommands: botTooltipCommands,
} = useBotCommandTooltip(
Boolean((botCommands && botCommands.length) || (chatBotCommands && chatBotCommands.length)),
html,
botCommands,
chatBotCommands,
);
const { const {
isContextMenuOpen: isCustomSendMenuOpen, isContextMenuOpen: isCustomSendMenuOpen,
handleContextMenu, handleContextMenu,
@ -532,6 +552,16 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
resetComposer, stopRecordingVoice, showDialog, slowMode, isAdmin, sendMessage, forwardMessages, lang, 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) => { const handleStickerSelect = useCallback((sticker: ApiSticker, shouldPreserveInput = false) => {
sticker = { sticker = {
...sticker, ...sticker,
@ -582,6 +612,13 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
}); });
}, [chatId, clearDraft, connectionState, resetComposer, sendInlineBotResult]); }, [chatId, clearDraft, connectionState, resetComposer, sendInlineBotResult]);
const handleBotCommandSelect = useCallback(() => {
clearDraft({ chatId, localOnly: true });
requestAnimationFrame(() => {
resetComposer();
});
}, [chatId, clearDraft, resetComposer]);
const handlePollSend = useCallback((poll: ApiNewPoll) => { const handlePollSend = useCallback((poll: ApiNewPoll) => {
if (shouldSchedule) { if (shouldSchedule) {
setScheduledMessageArgs({ poll }); setScheduledMessageArgs({ poll });
@ -598,7 +635,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
setScheduledMessageArgs({ isSilent: true }); setScheduledMessageArgs({ isSilent: true });
openCalendar(); openCalendar();
} else { } else {
handleSend(true); void handleSend(true);
} }
}, [handleSend, openCalendar, shouldSchedule]); }, [handleSend, openCalendar, shouldSchedule]);
@ -610,7 +647,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
+ (isWhenOnline ? 0 : serverTimeOffset); + (isWhenOnline ? 0 : serverTimeOffset);
if (!scheduledMessageArgs || Object.keys(restArgs).length === 0) { if (!scheduledMessageArgs || Object.keys(restArgs).length === 0) {
handleSend(!!isSilent, scheduledAt); void handleSend(!!isSilent, scheduledAt);
} else { } else {
sendMessage({ sendMessage({
...scheduledMessageArgs, ...scheduledMessageArgs,
@ -652,9 +689,10 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
messageInput.blur(); messageInput.blur();
setTimeout(() => { setTimeout(() => {
closeBotCommandMenu();
openSymbolMenu(); openSymbolMenu();
}, MOBILE_KEYBOARD_HIDE_DELAY_MS); }, MOBILE_KEYBOARD_HIDE_DELAY_MS);
}, [openSymbolMenu]); }, [openSymbolMenu, closeBotCommandMenu]);
const handleAllScheduledClick = useCallback(() => { const handleAllScheduledClick = useCallback(() => {
openChat({ id: chatId, threadId, type: 'scheduled' }); openChat({ id: chatId, threadId, type: 'scheduled' });
@ -687,14 +725,14 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
} }
openCalendar(); openCalendar();
} else { } else {
handleSend(); void handleSend();
requestAnimationFrame(() => { requestAnimationFrame(() => {
resetComposer(); resetComposer();
}); });
} }
break; break;
case MainButtonState.Record: case MainButtonState.Record:
startRecordingVoice(); void startRecordingVoice();
break; break;
case MainButtonState.Edit: case MainButtonState.Edit:
handleEditComplete(); handleEditComplete();
@ -800,7 +838,6 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
)} )}
<MentionTooltip <MentionTooltip
isOpen={isMentionTooltipOpen} isOpen={isMentionTooltipOpen}
filter={mentionFilter}
onClose={closeMentionTooltip} onClose={closeMentionTooltip}
onInsertUserName={insertMention} onInsertUserName={insertMention}
filteredUsers={mentionFilteredUsers} filteredUsers={mentionFilteredUsers}
@ -817,6 +854,13 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
loadMore={loadMoreForInlineBot} loadMore={loadMoreForInlineBot}
onClose={closeInlineBotTooltip} onClose={closeInlineBotTooltip}
/> />
<BotCommandTooltip
isOpen={isBotCommandTooltipOpen}
withUsername={Boolean(chatBotCommands)}
botCommands={botTooltipCommands}
onClick={handleBotCommandSelect}
onClose={closeBotCommandTooltip}
/>
<div id="message-compose"> <div id="message-compose">
<div className="svg-appendix" ref={appendixRef} /> <div className="svg-appendix" ref={appendixRef} />
<ComposerEmbeddedMessage /> <ComposerEmbeddedMessage />
@ -827,6 +871,19 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
disabled={!allowedAttachmentOptions.canAttachEmbedLinks} disabled={!allowedAttachmentOptions.canAttachEmbedLinks}
/> />
<div className="message-input-wrapper"> <div className="message-input-wrapper">
{isChatWithBot && botCommands !== false && !activeVoiceRecording && !editingMessage && (
<ResponsiveHoverButton
className={buildClassName('bot-commands', isBotCommandMenuOpen && 'activated')}
round
faded
disabled={botCommands === undefined}
color="translucent"
onActivate={handleActivateBotCommandMenu}
ariaLabel="Open bot command keyboard"
>
<i className="icon-bot-commands-filled" />
</ResponsiveHoverButton>
)}
{IS_SINGLE_COLUMN_LAYOUT ? ( {IS_SINGLE_COLUMN_LAYOUT ? (
<Button <Button
className={symbolMenuButtonClassName} className={symbolMenuButtonClassName}
@ -842,11 +899,11 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
</Button> </Button>
) : ( ) : (
<ResponsiveHoverButton <ResponsiveHoverButton
className={`${isSymbolMenuOpen ? 'activated' : ''}`} className={isSymbolMenuOpen ? 'activated' : ''}
round round
faded faded
color="translucent" color="translucent"
onActivate={openSymbolMenu} onActivate={handleActivateSymbolMenu}
ariaLabel="Choose emoji, sticker or GIF" ariaLabel="Choose emoji, sticker or GIF"
> >
<i className="icon-smile" /> <i className="icon-smile" />
@ -885,7 +942,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
)} )}
{botKeyboardMessageId && !activeVoiceRecording && !editingMessage && ( {botKeyboardMessageId && !activeVoiceRecording && !editingMessage && (
<ResponsiveHoverButton <ResponsiveHoverButton
className={`${isBotKeyboardOpen ? 'activated' : ''}`} className={isBotKeyboardOpen ? 'activated' : ''}
round round
faded faded
color="translucent" color="translucent"
@ -897,7 +954,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
)} )}
{!activeVoiceRecording && !editingMessage && ( {!activeVoiceRecording && !editingMessage && (
<ResponsiveHoverButton <ResponsiveHoverButton
className={`${isAttachMenuOpen ? 'activated' : ''}`} className={isAttachMenuOpen ? 'activated' : ''}
round round
faded faded
color="translucent" color="translucent"
@ -937,6 +994,13 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
onClose={closeBotKeyboard} onClose={closeBotKeyboard}
/> />
)} )}
{botCommands && (
<BotCommandMenu
isOpen={isBotCommandMenuOpen}
botCommands={botCommands}
onClose={closeBotCommandMenu}
/>
)}
<SymbolMenu <SymbolMenu
isOpen={isSymbolMenuOpen} isOpen={isSymbolMenuOpen}
allowedAttachmentOptions={allowedAttachmentOptions} allowedAttachmentOptions={allowedAttachmentOptions}
@ -1006,7 +1070,8 @@ export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, messageListType }): StateProps => { (global, { chatId, threadId, messageListType }): StateProps => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const chatUser = chat && selectChatUser(global, chat); 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 isChatWithSelf = selectIsChatWithSelf(global, chatId);
const messageWithActualBotKeyboard = isChatWithBot && selectNewestMessageWithBotKeyboardButtons(global, chatId); const messageWithActualBotKeyboard = isChatWithBot && selectNewestMessageWithBotKeyboardButtons(global, chatId);
const scheduledIds = selectScheduledIds(global, chatId); const scheduledIds = selectScheduledIds(global, chatId);
@ -1055,6 +1120,8 @@ export default memo(withGlobal<OwnProps>(
serverTimeOffset: global.serverTimeOffset, serverTimeOffset: global.serverTimeOffset,
inlineBots: global.inlineBots.byUsername, inlineBots: global.inlineBots.byUsername,
isInlineBotLoading: global.inlineBots.isLoading, 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, [ (setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -1,16 +1,15 @@
import React, { import React, {
FC, memo, useCallback, useEffect, useRef, useState, FC, memo, useCallback, useEffect, useRef,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { IS_TOUCH_ENV } from '../../../util/environment'; import { IS_TOUCH_ENV } from '../../../util/environment';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import cycleRestrict from '../../../util/cycleRestrict';
import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
import findInViewport from '../../../util/findInViewport'; import findInViewport from '../../../util/findInViewport';
import isFullyVisible from '../../../util/isFullyVisible'; import isFullyVisible from '../../../util/isFullyVisible';
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal'; import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import usePrevDuringAnimation from '../../../hooks/usePrevDuringAnimation'; import usePrevDuringAnimation from '../../../hooks/usePrevDuringAnimation';
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import EmojiButton from './EmojiButton'; import EmojiButton from './EmojiButton';
@ -20,7 +19,6 @@ import './EmojiTooltip.scss';
const VIEWPORT_MARGIN = 8; const VIEWPORT_MARGIN = 8;
const EMOJI_BUTTON_WIDTH = 44; const EMOJI_BUTTON_WIDTH = 44;
const CLOSE_DURATION = 350; const CLOSE_DURATION = 350;
const NO_EMOJI_SELECTED_INDEX = -1;
function setItemVisible(index: number, containerRef: Record<string, any>) { function setItemVisible(index: number, containerRef: Record<string, any>) {
const container = containerRef.current!; const container = containerRef.current!;
@ -70,52 +68,27 @@ const EmojiTooltip: FC<OwnProps> = ({
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
const listEmojis: Emoji[] = usePrevDuringAnimation(emojis.length ? emojis : undefined, CLOSE_DURATION) || []; const listEmojis: Emoji[] = usePrevDuringAnimation(emojis.length ? emojis : undefined, CLOSE_DURATION) || [];
const [selectedIndex, setSelectedIndex] = useState(NO_EMOJI_SELECTED_INDEX); const handleSelectEmoji = useCallback((emoji: Emoji) => {
onEmojiSelect(emoji.native);
useEffect(() => { addRecentEmoji({ emoji: emoji.id });
setSelectedIndex(0); }, [addRecentEmoji, onEmojiSelect]);
}, [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 handleClick = useCallback((native: string, id: string) => { const handleClick = useCallback((native: string, id: string) => {
onEmojiSelect(native); onEmojiSelect(native);
addRecentEmoji({ emoji: id }); addRecentEmoji({ emoji: id });
}, [addRecentEmoji, onEmojiSelect]); }, [addRecentEmoji, onEmojiSelect]);
useEffect(() => (isOpen ? captureKeyboardListeners({ const selectedIndex = useKeyboardNavigation({
onEsc: onClose, isActive: isOpen,
onLeft: (e: KeyboardEvent) => handleArrowKey(-1, e), isHorizontal: true,
onRight: (e: KeyboardEvent) => handleArrowKey(1, e), items: emojis,
onEnter: handleSelectEmoji, onSelect: handleSelectEmoji,
}) : undefined), [handleArrowKey, handleSelectEmoji, isOpen, onClose]); onClose,
});
useEffect(() => {
setItemVisible(selectedIndex, containerRef);
}, [selectedIndex]);
const handleMouseEnter = () => { const handleMouseEnter = () => {
document.body.classList.add('no-select'); document.body.classList.add('no-select');

View File

@ -1,5 +1,5 @@
import React, { import React, {
FC, memo, useCallback, useEffect, useRef, useState, FC, memo, useCallback, useEffect, useRef,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { withGlobal } from '../../../lib/teact/teactn'; import { withGlobal } from '../../../lib/teact/teactn';
@ -11,13 +11,12 @@ import { LoadMoreDirection } from '../../../types';
import { IS_TOUCH_ENV } from '../../../util/environment'; import { IS_TOUCH_ENV } from '../../../util/environment';
import setTooltipItemVisible from '../../../util/setTooltipItemVisible'; import setTooltipItemVisible from '../../../util/setTooltipItemVisible';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
import cycleRestrict from '../../../util/cycleRestrict';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
import { pick } from '../../../util/iteratees'; import { pick } from '../../../util/iteratees';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation';
import MediaResult from './inlineResults/MediaResult'; import MediaResult from './inlineResults/MediaResult';
import ArticleResult from './inlineResults/ArticleResult'; import ArticleResult from './inlineResults/ArticleResult';
@ -43,7 +42,7 @@ export type OwnProps = {
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
}; };
type DispatchProps = Pick<GlobalActions, ('sendBotCommand' | 'openChat' | 'sendInlineBotResult')>; type DispatchProps = Pick<GlobalActions, ('startBot' | 'openChat' | 'sendInlineBotResult')>;
const InlineBotTooltip: FC<OwnProps & DispatchProps> = ({ const InlineBotTooltip: FC<OwnProps & DispatchProps> = ({
isOpen, isOpen,
@ -54,13 +53,12 @@ const InlineBotTooltip: FC<OwnProps & DispatchProps> = ({
loadMore, loadMore,
onClose, onClose,
openChat, openChat,
sendBotCommand, startBot,
onSelectResult, onSelectResult,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
const [selectedIndex, setSelectedIndex] = useState(-1);
const { const {
observe: observeIntersection, observe: observeIntersection,
} = useIntersectionObserver({ } = useIntersectionObserver({
@ -69,58 +67,29 @@ const InlineBotTooltip: FC<OwnProps & DispatchProps> = ({
isDisabled: !isOpen, 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 }) => { const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
if (direction === LoadMoreDirection.Backwards) { if (direction === LoadMoreDirection.Backwards) {
runThrottled(loadMore); runThrottled(loadMore);
} }
}, [loadMore]); }, [loadMore]);
useEffect(() => (isOpen ? captureKeyboardListeners({ const selectedIndex = useKeyboardNavigation({
onEsc: onClose, isActive: isOpen,
onUp: (e: KeyboardEvent) => handleArrowKey(-1, e), shouldRemoveSelectionOnReset: isGallery,
onDown: (e: KeyboardEvent) => handleArrowKey(1, e), noArrowNavigation: isGallery,
onEnter: handleSelectInlineBotResult, items: inlineBotResults,
}) : undefined), [handleArrowKey, handleSelectInlineBotResult, isGallery, isOpen, onClose]); onSelect: onSelectResult,
onClose,
});
useEffect(() => {
setTooltipItemVisible('.chat-item-clickable', selectedIndex, containerRef);
}, [selectedIndex]);
const handleSendPm = useCallback(() => { const handleSendPm = useCallback(() => {
openChat({ id: botId }); openChat({ id: botId });
sendBotCommand({ chatId: botId, command: `/start ${switchPm!.startParam}` }); startBot({ botId, param: switchPm!.startParam });
}, [botId, openChat, sendBotCommand, switchPm]); }, [botId, openChat, startBot, switchPm]);
const prevInlineBotResults = usePrevious( const prevInlineBotResults = usePrevious(
inlineBotResults && inlineBotResults.length inlineBotResults && inlineBotResults.length
@ -230,6 +199,6 @@ const InlineBotTooltip: FC<OwnProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
undefined, undefined,
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [
'sendBotCommand', 'openChat', 'sendInlineBotResult', 'startBot', 'openChat', 'sendInlineBotResult',
]), ]),
)(InlineBotTooltip)); )(InlineBotTooltip));

View File

@ -1,5 +1,5 @@
import React, { import React, {
FC, useCallback, useEffect, useState, useRef, memo, FC, useCallback, useEffect, useRef, memo,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
@ -7,9 +7,8 @@ import { ApiUser } from '../../../api/types';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
import setTooltipItemVisible from '../../../util/setTooltipItemVisible'; import setTooltipItemVisible from '../../../util/setTooltipItemVisible';
import cycleRestrict from '../../../util/cycleRestrict'; import { useKeyboardNavigation } from './hooks/useKeyboardNavigation';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import PrivateChatInfo from '../../common/PrivateChatInfo'; import PrivateChatInfo from '../../common/PrivateChatInfo';
@ -18,7 +17,6 @@ import './MentionTooltip.scss';
export type OwnProps = { export type OwnProps = {
isOpen: boolean; isOpen: boolean;
filter: string;
onClose: () => void; onClose: () => void;
onInsertUserName: (user: ApiUser, forceFocus?: boolean) => void; onInsertUserName: (user: ApiUser, forceFocus?: boolean) => void;
filteredUsers?: ApiUser[]; filteredUsers?: ApiUser[];
@ -27,7 +25,6 @@ export type OwnProps = {
const MentionTooltip: FC<OwnProps> = ({ const MentionTooltip: FC<OwnProps> = ({
isOpen, isOpen,
filter,
onClose, onClose,
onInsertUserName, onInsertUserName,
usersById, usersById,
@ -37,21 +34,6 @@ const MentionTooltip: FC<OwnProps> = ({
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); 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 handleUserSelect = useCallback((userId: number, forceFocus = false) => {
const user = usersById && usersById[userId]; const user = usersById && usersById[userId];
if (!user) { if (!user) {
@ -61,23 +43,21 @@ const MentionTooltip: FC<OwnProps> = ({
onInsertUserName(user, forceFocus); onInsertUserName(user, forceFocus);
}, [usersById, onInsertUserName]); }, [usersById, onInsertUserName]);
const handleSelectMention = useCallback((e: KeyboardEvent) => { const handleSelectMention = useCallback((member: ApiUser) => {
if (filteredUsers && filteredUsers.length && selectedMentionIndex > -1) { handleUserSelect(member.id, true);
const member = filteredUsers[selectedMentionIndex]; }, [handleUserSelect]);
if (member) {
e.preventDefault();
handleUserSelect(member.id, true);
}
}
}, [filteredUsers, selectedMentionIndex, handleUserSelect]);
useEffect(() => (isOpen ? captureKeyboardListeners({ const selectedMentionIndex = useKeyboardNavigation({
onEsc: onClose, isActive: isOpen,
onUp: (e: KeyboardEvent) => handleArrowKey(-1, e), items: filteredUsers,
onDown: (e: KeyboardEvent) => handleArrowKey(1, e), onSelect: handleSelectMention,
onEnter: handleSelectMention, shouldSelectOnTab: true,
onTab: handleSelectMention, onClose,
}) : undefined), [isOpen, onClose, handleArrowKey, handleSelectMention]); });
useEffect(() => {
setTooltipItemVisible('.chat-item-clickable', selectedMentionIndex, containerRef);
}, [selectedMentionIndex]);
useEffect(() => { useEffect(() => {
if (filteredUsers && !filteredUsers.length) { if (filteredUsers && !filteredUsers.length) {
@ -85,14 +65,6 @@ const MentionTooltip: FC<OwnProps> = ({
} }
}, [filteredUsers, onClose]); }, [filteredUsers, onClose]);
useEffect(() => {
setSelectedMentionIndex(0);
}, [filter]);
useEffect(() => {
setTooltipItemVisible('.chat-item-clickable', selectedMentionIndex, containerRef);
}, [selectedMentionIndex]);
const prevChatMembers = usePrevious( const prevChatMembers = usePrevious(
filteredUsers && filteredUsers.length filteredUsers && filteredUsers.length
? filteredUsers ? filteredUsers

View File

@ -90,6 +90,13 @@
} }
} }
.Button.bot-commands ~ & {
.is-pointer-env & > .backdrop {
left: 3rem;
width: 3.25rem;
}
}
.bubble { .bubble {
padding: 0; padding: 0;
width: var(--symbol-menu-width); width: var(--symbol-menu-width);

View File

@ -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<ApiBotCommand[] | undefined>();
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,
};
}

View File

@ -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;
}

View File

@ -35,7 +35,6 @@ export default function useMentionTooltip(
usersById?: Record<number, ApiUser>, usersById?: Record<number, ApiUser>,
) { ) {
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag(); const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
const [currentFilter, setCurrentFilter] = useState('');
const [usersToMention, setUsersToMention] = useState<ApiUser[] | undefined>(); const [usersToMention, setUsersToMention] = useState<ApiUser[] | undefined>();
const topInlineBots = useMemo(() => { const topInlineBots = useMemo(() => {
@ -77,7 +76,6 @@ export default function useMentionTooltip(
if (usernameFilter) { if (usernameFilter) {
const filter = usernameFilter ? usernameFilter.substr(1) : ''; const filter = usernameFilter ? usernameFilter.substr(1) : '';
setCurrentFilter(filter);
getFilteredUsers(filter, canSuggestInlineBots(html)); getFilteredUsers(filter, canSuggestInlineBots(html));
} else { } else {
unmarkIsOpen(); unmarkIsOpen();
@ -121,7 +119,6 @@ export default function useMentionTooltip(
return { return {
isMentionTooltipOpen: isOpen, isMentionTooltipOpen: isOpen,
mentionFilter: currentFilter,
closeMentionTooltip: unmarkIsOpen, closeMentionTooltip: unmarkIsOpen,
insertMention, insertMention,
mentionFilteredUsers: usersToMention, mentionFilteredUsers: usersToMention,

View File

@ -100,7 +100,6 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
[error.field]: error.message, [error.field]: error.message,
}, },
}); });
return;
} }
}, [error, paymentDispatch]); }, [error, paymentDispatch]);

View File

@ -62,7 +62,7 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps & DispatchProps> = ({
const canUpdate = Boolean( const canUpdate = Boolean(
(privacyType === 'public' && username && isUsernameAvailable) (privacyType === 'public' && username && isUsernameAvailable)
|| (privacyType === 'private' && isPublic) || (privacyType === 'private' && isPublic),
); );
useHistoryBack(isActive, onClose); useHistoryBack(isActive, onClose);

View File

@ -133,6 +133,7 @@ export const RE_TME_INVITE_LINK = /^(?:https?:\/\/)?(?:t\.me\/joinchat\/)([\d\w_
// MTProto constants // MTProto constants
export const SERVICE_NOTIFICATIONS_USER_ID = 777000; export const SERVICE_NOTIFICATIONS_USER_ID = 777000;
export const REPLIES_USER_ID = 1271266957;
export const ALL_FOLDER_ID = 0; export const ALL_FOLDER_ID = 0;
export const ARCHIVED_FOLDER_ID = 1; export const ARCHIVED_FOLDER_ID = 1;
export const DELETED_COMMENTS_CHANNEL_ID = 777; export const DELETED_COMMENTS_CHANNEL_ID = 777;

View File

@ -500,7 +500,7 @@ export type ActionTypes = (
'openStickerSetShortName' | 'openStickerSetShortName' |
// bots // bots
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' | 'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
'resetInlineBot' | 'restartBot' | 'resetInlineBot' | 'restartBot' | 'startBot' |
// misc // misc
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'closeAudioPlayer' | 'openPollModal' | 'closePollModal' | 'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'closeAudioPlayer' | 'openPollModal' | 'closePollModal' |
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' | 'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' |

View File

@ -8,7 +8,13 @@ let closeTimeout: number | undefined;
export default function useMouseInside( export default function useMouseInside(
isOpen: boolean, onClose: NoneToVoidFunction, menuCloseTimeout = MENU_CLOSE_TIMEOUT, isDisabled = false, 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(() => { useEffect(() => {
if (closeTimeout) { if (closeTimeout) {

View File

@ -50,7 +50,6 @@ export async function authFlow(
client._log.info('Signed in successfully as', utils.getDisplayName(me)); client._log.info('Signed in successfully as', utils.getDisplayName(me));
} }
export async function checkAuthorization(client: TelegramClient) { export async function checkAuthorization(client: TelegramClient) {
try { try {
await client.invoke(new Api.updates.GetState()); await client.invoke(new Api.updates.GetState());

View File

@ -1000,6 +1000,7 @@ messages.importChatInvite#6c50051c hash:string = Updates;
messages.getStickerSet#2619a90e stickerset:InputStickerSet = messages.StickerSet; messages.getStickerSet#2619a90e stickerset:InputStickerSet = messages.StickerSet;
messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult; messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult;
messages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool; 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.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.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; messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document;

View File

@ -1001,6 +1001,7 @@ messages.importChatInvite#6c50051c hash:string = Updates;
messages.getStickerSet#2619a90e stickerset:InputStickerSet = messages.StickerSet; messages.getStickerSet#2619a90e stickerset:InputStickerSet = messages.StickerSet;
messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult; messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult;
messages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool; 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.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.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; 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<string> = Vector<LangPackString>; langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector<string> = Vector<LangPackString>;
langpack.getLanguages#42c6978f lang_pack:string = Vector<LangPackLanguage>; langpack.getLanguages#42c6978f lang_pack:string = Vector<LangPackLanguage>;
folders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates; folders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;
// LAYER 128 // LAYER 128

View File

@ -200,7 +200,7 @@ addReducer('sendInlineBotResult', (global, actions, payload) => {
}); });
}); });
addReducer('resetInlineBot', ((global, actions, payload) => { addReducer('resetInlineBot', (global, actions, payload) => {
const { username } = payload; const { username } = payload;
let inlineBotData = global.inlineBots.byUsername[username]; let inlineBotData = global.inlineBots.byUsername[username];
@ -219,7 +219,23 @@ addReducer('resetInlineBot', ((global, actions, payload) => {
}; };
setGlobal(replaceInlineBotSettings(global, username, inlineBotData)); 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({ async function searchInlineBot({
username, username,

View File

@ -57,7 +57,7 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
onUpdateCurrentUser(update); onUpdateCurrentUser(update);
break; break;
case 'error': case 'error': {
if (update.error.message === 'SESSION_REVOKED') { if (update.error.message === 'SESSION_REVOKED') {
actions.signOut(); actions.signOut();
} }
@ -70,6 +70,7 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
} }
break; break;
}
} }
}); });

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
@font-face { @font-face {
font-family: 'icomoon'; font-family: 'icomoon';
src: url('../assets/fonts/icomoon.woff2?7tsg0v') format('woff2'), src: url('../assets/fonts/icomoon.woff2?n9djnk') format('woff2'),
url('../assets/fonts/icomoon.woff?7tsg0v') format('woff'); url('../assets/fonts/icomoon.woff?n9djnk') format('woff');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block; font-display: block;
@ -32,6 +32,12 @@
} }
} }
.icon-bot-commands-filled:before {
content: "\e97f";
}
.icon-reply-filled:before {
content: "\e980";
}
.icon-bug:before { .icon-bug:before {
content: "\e97e"; content: "\e97e";
} }

View File

@ -120,5 +120,5 @@ export default function getReadableErrorText(error: ApiError) {
} }
export function getShippingError(error: ApiError): ApiFieldError | undefined { export function getShippingError(error: ApiError): ApiFieldError | undefined {
return SHIPPING_ERRORS[error.message]; return SHIPPING_ERRORS[error.message];
} }