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

View File

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

View File

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

View File

@ -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<ApiUser>(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[]) {

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

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 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';

View File

@ -546,10 +546,9 @@ export default memo(withGlobal<OwnProps>(
&& !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<OwnProps>(
isGroupChat: isChatGroup(chat),
isCreator: chat.isCreator,
isChatWithSelf: selectIsChatWithSelf(global, chatId),
isBot: Boolean(bot),
isBot: Boolean(chatBot),
messageIds,
messagesById,
firstUnreadId: selectFirstUnreadId(global, chatId, threadId),

View File

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

View File

@ -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<string, false | InlineBotSettings>;
botCommands?: ApiBotCommand[] | false;
chatBotCommands?: ApiBotCommand[];
} & Pick<GlobalState, 'connectionState'>;
type DispatchProps = Pick<GlobalActions, (
@ -197,6 +205,8 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
recentEmojis,
inlineBots,
isInlineBotLoading,
botCommands,
chatBotCommands,
sendMessage,
editMessage,
saveDraft,
@ -259,6 +269,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
const [attachments, setAttachments] = useState<ApiAttachment[]>([]);
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<OwnProps & StateProps & DispatchProps> = ({
const canShowCustomSendMenu = !shouldSchedule;
const {
isMentionTooltipOpen, mentionFilter,
closeMentionTooltip, insertMention,
mentionFilteredUsers,
isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers,
} = useMentionTooltip(
!attachments.length,
html,
@ -313,6 +322,17 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
});
}, [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<OwnProps & StateProps & DispatchProps> = ({
setScheduledMessageArgs({ isSilent: true });
openCalendar();
} else {
handleSend(true);
void handleSend(true);
}
}, [handleSend, openCalendar, shouldSchedule]);
@ -610,7 +647,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
+ (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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
}
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<OwnProps & StateProps & DispatchProps> = ({
)}
<MentionTooltip
isOpen={isMentionTooltipOpen}
filter={mentionFilter}
onClose={closeMentionTooltip}
onInsertUserName={insertMention}
filteredUsers={mentionFilteredUsers}
@ -817,6 +854,13 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
loadMore={loadMoreForInlineBot}
onClose={closeInlineBotTooltip}
/>
<BotCommandTooltip
isOpen={isBotCommandTooltipOpen}
withUsername={Boolean(chatBotCommands)}
botCommands={botTooltipCommands}
onClick={handleBotCommandSelect}
onClose={closeBotCommandTooltip}
/>
<div id="message-compose">
<div className="svg-appendix" ref={appendixRef} />
<ComposerEmbeddedMessage />
@ -827,6 +871,19 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
disabled={!allowedAttachmentOptions.canAttachEmbedLinks}
/>
<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 ? (
<Button
className={symbolMenuButtonClassName}
@ -842,11 +899,11 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
</Button>
) : (
<ResponsiveHoverButton
className={`${isSymbolMenuOpen ? 'activated' : ''}`}
className={isSymbolMenuOpen ? 'activated' : ''}
round
faded
color="translucent"
onActivate={openSymbolMenu}
onActivate={handleActivateSymbolMenu}
ariaLabel="Choose emoji, sticker or GIF"
>
<i className="icon-smile" />
@ -885,7 +942,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
)}
{botKeyboardMessageId && !activeVoiceRecording && !editingMessage && (
<ResponsiveHoverButton
className={`${isBotKeyboardOpen ? 'activated' : ''}`}
className={isBotKeyboardOpen ? 'activated' : ''}
round
faded
color="translucent"
@ -897,7 +954,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
)}
{!activeVoiceRecording && !editingMessage && (
<ResponsiveHoverButton
className={`${isAttachMenuOpen ? 'activated' : ''}`}
className={isAttachMenuOpen ? 'activated' : ''}
round
faded
color="translucent"
@ -937,6 +994,13 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
onClose={closeBotKeyboard}
/>
)}
{botCommands && (
<BotCommandMenu
isOpen={isBotCommandMenuOpen}
botCommands={botCommands}
onClose={closeBotCommandMenu}
/>
)}
<SymbolMenu
isOpen={isSymbolMenuOpen}
allowedAttachmentOptions={allowedAttachmentOptions}
@ -1006,7 +1070,8 @@ export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
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, [

View File

@ -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<string, any>) {
const container = containerRef.current!;
@ -70,52 +68,27 @@ const EmojiTooltip: FC<OwnProps> = ({
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');

View File

@ -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<GlobalActions, ('sendBotCommand' | 'openChat' | 'sendInlineBotResult')>;
type DispatchProps = Pick<GlobalActions, ('startBot' | 'openChat' | 'sendInlineBotResult')>;
const InlineBotTooltip: FC<OwnProps & DispatchProps> = ({
isOpen,
@ -54,13 +53,12 @@ const InlineBotTooltip: FC<OwnProps & DispatchProps> = ({
loadMore,
onClose,
openChat,
sendBotCommand,
startBot,
onSelectResult,
}) => {
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(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<OwnProps & DispatchProps> = ({
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<OwnProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
undefined,
(setGlobal, actions): DispatchProps => pick(actions, [
'sendBotCommand', 'openChat', 'sendInlineBotResult',
'startBot', 'openChat', 'sendInlineBotResult',
]),
)(InlineBotTooltip));

View File

@ -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<OwnProps> = ({
isOpen,
filter,
onClose,
onInsertUserName,
usersById,
@ -37,21 +34,6 @@ const MentionTooltip: FC<OwnProps> = ({
const containerRef = useRef<HTMLDivElement>(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<OwnProps> = ({
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<OwnProps> = ({
}
}, [filteredUsers, onClose]);
useEffect(() => {
setSelectedMentionIndex(0);
}, [filter]);
useEffect(() => {
setTooltipItemVisible('.chat-item-clickable', selectedMentionIndex, containerRef);
}, [selectedMentionIndex]);
const prevChatMembers = usePrevious(
filteredUsers && filteredUsers.length
? filteredUsers

View File

@ -90,6 +90,13 @@
}
}
.Button.bot-commands ~ & {
.is-pointer-env & > .backdrop {
left: 3rem;
width: 3.25rem;
}
}
.bubble {
padding: 0;
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>,
) {
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
const [currentFilter, setCurrentFilter] = useState('');
const [usersToMention, setUsersToMention] = useState<ApiUser[] | undefined>();
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,

View File

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

View File

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

View File

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

View File

@ -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' |

View File

@ -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) {

View File

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

View File

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

View File

@ -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<string> = Vector<LangPackString>;
langpack.getLanguages#42c6978f lang_pack:string = Vector<LangPackLanguage>;
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;
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,

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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