diff --git a/src/api/gramjs/apiBuilders/bots.ts b/src/api/gramjs/apiBuilders/bots.ts index f7c5b521b..e5019659e 100644 --- a/src/api/gramjs/apiBuilders/bots.ts +++ b/src/api/gramjs/apiBuilders/bots.ts @@ -1,7 +1,7 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { - ApiAttachMenuBot, - ApiAttachMenuBotIcon, + ApiAttachBot, + ApiAttachBotIcon, ApiAttachMenuPeerType, ApiBotCommand, ApiBotInfo, @@ -62,7 +62,7 @@ export function buildBotSwitchPm(switchPm?: GramJs.InlineBotSwitchPM) { return switchPm ? pick(switchPm, ['text', 'startParam']) as ApiBotInlineSwitchPm : undefined; } -export function buildApiAttachMenuBot(bot: GramJs.AttachMenuBot): ApiAttachMenuBot { +export function buildApiAttachBot(bot: GramJs.AttachMenuBot): ApiAttachBot { return { id: bot.botId.toString(), hasSettings: bot.hasSettings, @@ -73,15 +73,15 @@ export function buildApiAttachMenuBot(bot: GramJs.AttachMenuBot): ApiAttachMenuB } function buildApiAttachMenuPeerType(peerType: GramJs.TypeAttachMenuPeerType): ApiAttachMenuPeerType { - if (peerType instanceof GramJs.AttachMenuPeerTypeBotPM) return 'bot'; - if (peerType instanceof GramJs.AttachMenuPeerTypePM) return 'private'; - if (peerType instanceof GramJs.AttachMenuPeerTypeChat) return 'chat'; - if (peerType instanceof GramJs.AttachMenuPeerTypeBroadcast) return 'channel'; + if (peerType instanceof GramJs.AttachMenuPeerTypeBotPM) return 'bots'; + if (peerType instanceof GramJs.AttachMenuPeerTypePM) return 'users'; + if (peerType instanceof GramJs.AttachMenuPeerTypeChat) return 'chats'; + if (peerType instanceof GramJs.AttachMenuPeerTypeBroadcast) return 'channels'; if (peerType instanceof GramJs.AttachMenuPeerTypeSameBotPM) return 'self'; return undefined!; // Never reached } -function buildApiAttachMenuIcon(icon: GramJs.AttachMenuBotIcon): ApiAttachMenuBotIcon | undefined { +function buildApiAttachMenuIcon(icon: GramJs.AttachMenuBotIcon): ApiAttachBotIcon | undefined { if (!(icon.icon instanceof GramJs.Document)) return undefined; const document = buildApiDocument(icon.icon); diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 99ca98192..3aad3e410 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -68,7 +68,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { ...(avatarHash && { avatarHash }), hasVideoAvatar, ...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }), - ...(mtpUser.bot && mtpUser.botAttachMenu && { isAttachMenuBot: mtpUser.botAttachMenu }), + ...(mtpUser.bot && mtpUser.botAttachMenu && { isAttachBot: mtpUser.botAttachMenu }), }; } diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 76e15b258..7aa007fc6 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -10,7 +10,7 @@ import { invokeRequest } from './client'; import { buildInputPeer, buildInputThemeParams, generateRandomBigInt } from '../gramjsBuilders'; import { buildApiUser } from '../apiBuilders/users'; import { - buildApiAttachMenuBot, buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm, + buildApiAttachBot, buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm, } from '../apiBuilders/bots'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers'; @@ -249,7 +249,7 @@ export async function sendWebViewData({ }), true); } -export async function loadAttachMenuBots({ +export async function loadAttachBots({ hash, }: { hash?: string; @@ -262,13 +262,13 @@ export async function loadAttachMenuBots({ addEntitiesWithPhotosToLocalDb(result.users); return { hash: result.hash.toString(), - bots: buildCollectionByKey(result.bots.map(buildApiAttachMenuBot), 'id'), + bots: buildCollectionByKey(result.bots.map(buildApiAttachBot), 'id'), }; } return undefined; } -export function toggleBotInAttachMenu({ +export function toggleAttachBot({ bot, isEnabled, }: { diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index f67c5d040..ba2247a7d 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -68,7 +68,7 @@ export { export { answerCallbackButton, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, sendInlineBotResult, startBot, - requestWebView, requestSimpleWebView, sendWebViewData, prolongWebView, loadAttachMenuBots, toggleBotInAttachMenu, + requestWebView, requestSimpleWebView, sendWebViewData, prolongWebView, loadAttachBots, toggleAttachBot, requestBotUrlAuth, requestLinkUrlAuth, acceptBotUrlAuth, acceptLinkUrlAuth, } from './bots'; diff --git a/src/api/types/users.ts b/src/api/types/users.ts index e50e3e112..2a7061d24 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -1,5 +1,6 @@ import type { ApiDocument, ApiPhoto } from './messages'; import type { ApiBotInfo } from './bots'; +import type { API_CHAT_TYPES } from '../../config'; export interface ApiUser { id: string; @@ -26,7 +27,7 @@ export interface ApiUser { isFullyLoaded: boolean; }; fakeType?: ApiFakeType; - isAttachMenuBot?: boolean; + isAttachBot?: boolean; // Obtained from GetFullUser / UserFullInfo fullInfo?: ApiUserFullInfo; @@ -56,17 +57,18 @@ export interface ApiUserStatus { expires?: number; } -export type ApiAttachMenuPeerType = 'self' | 'bot' | 'private' | 'chat' | 'channel'; +export type ApiChatType = typeof API_CHAT_TYPES[number]; +export type ApiAttachMenuPeerType = 'self' | ApiChatType; -export interface ApiAttachMenuBot { +export interface ApiAttachBot { id: string; hasSettings?: boolean; shortName: string; peerTypes: ApiAttachMenuPeerType[]; - icons: ApiAttachMenuBotIcon[]; + icons: ApiAttachBotIcon[]; } -export interface ApiAttachMenuBotIcon { +export interface ApiAttachBotIcon { name: string; document: ApiDocument; } diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 5ce8d0baf..dd813d8c8 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -1,7 +1,9 @@ // eslint-disable-next-line import/no-cycle export { default as MediaViewer } from '../components/mediaViewer/MediaViewer'; -export { default as ForwardPicker } from '../components/main/ForwardPicker'; +export { default as ForwardRecipientPicker } from '../components/main/ForwardRecipientPicker'; +export { default as DraftRecipientPicker } from '../components/main/DraftRecipientPicker'; +export { default as AttachBotRecipientPicker } from '../components/main/AttachBotRecipientPicker'; export { default as Dialogs } from '../components/main/Dialogs'; export { default as Notifications } from '../components/main/Notifications'; export { default as SafeLinkModal } from '../components/main/SafeLinkModal'; @@ -10,7 +12,7 @@ export { default as HistoryCalendar } from '../components/main/HistoryCalendar'; export { default as NewContactModal } from '../components/main/NewContactModal'; export { default as WebAppModal } from '../components/main/WebAppModal'; export { default as BotTrustModal } from '../components/main/BotTrustModal'; -export { default as BotAttachModal } from '../components/main/BotAttachModal'; +export { default as AttachBotInstallModal } from '../components/main/AttachBotInstallModal'; export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog'; export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal'; export { default as GiftPremiumModal } from '../components/main/premium/GiftPremiumModal'; diff --git a/src/components/common/ChatOrUserPicker.tsx b/src/components/common/ChatOrUserPicker.tsx index 8b0d2d777..ec9cc1aba 100644 --- a/src/components/common/ChatOrUserPicker.tsx +++ b/src/components/common/ChatOrUserPicker.tsx @@ -1,4 +1,3 @@ -import type { RefObject } from 'react'; import type { FC } from '../../lib/teact/teact'; import React, { memo, useRef, useCallback } from '../../lib/teact/teact'; @@ -24,11 +23,10 @@ export type OwnProps = { currentUserId?: string; chatOrUserIds: string[]; isOpen: boolean; - filterRef: RefObject; - filterPlaceholder: string; - filter: string; + searchPlaceholder: string; + search: string; loadMore?: NoneToVoidFunction; - onFilterChange: (filter: string) => void; + onSearchChange: (search: string) => void; onSelectChatOrUser: (chatOrUserId: string) => void; onClose: NoneToVoidFunction; onCloseAnimationEnd?: NoneToVoidFunction; @@ -38,28 +36,29 @@ const ChatOrUserPicker: FC = ({ isOpen, currentUserId, chatOrUserIds, - filterRef, - filter, - filterPlaceholder, + search, + searchPlaceholder, loadMore, - onFilterChange, + onSearchChange, onSelectChatOrUser, onClose, onCloseAnimationEnd, }) => { const lang = useLang(); - const [viewportIds, getMore] = useInfiniteScroll(loadMore, chatOrUserIds, Boolean(filter)); + const [viewportIds, getMore] = useInfiniteScroll(loadMore, chatOrUserIds, Boolean(search)); + // eslint-disable-next-line no-null/no-null + const searchRef = useRef(null); - const resetFilter = useCallback(() => { - onFilterChange(''); - }, [onFilterChange]); - useInputFocusOnOpen(filterRef, isOpen, resetFilter); + const resetSearch = useCallback(() => { + onSearchChange(''); + }, [onSearchChange]); + useInputFocusOnOpen(searchRef, isOpen, resetSearch); // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); - const handleFilterChange = useCallback((e: React.ChangeEvent) => { - onFilterChange(e.currentTarget.value); - }, [onFilterChange]); + const handleSearchChange = useCallback((e: React.ChangeEvent) => { + onSearchChange(e.currentTarget.value); + }, [onSearchChange]); const handleKeyDown = useKeyboardListNavigation(containerRef, isOpen, (index) => { if (viewportIds && viewportIds.length > 0) { onSelectChatOrUser(viewportIds[index === -1 ? 0 : index]); @@ -78,11 +77,11 @@ const ChatOrUserPicker: FC = ({ ); diff --git a/src/components/common/RecipientPicker.tsx b/src/components/common/RecipientPicker.tsx new file mode 100644 index 000000000..f8596c83c --- /dev/null +++ b/src/components/common/RecipientPicker.tsx @@ -0,0 +1,128 @@ +import React, { memo, useMemo, useState } from '../../lib/teact/teact'; +import { getGlobal, withGlobal } from '../../global'; + +import type { FC } from '../../lib/teact/teact'; +import type { ApiChat, ApiChatType } from '../../api/types'; +import { MAIN_THREAD_ID } from '../../api/types'; + +import { API_CHAT_TYPES } from '../../config'; +import { unique } from '../../util/iteratees'; +import { + filterChatsByName, + filterUsersByName, + getCanPostInChat, + isDeletedUser, + sortChatIds, +} from '../../global/helpers'; + +import useLang from '../../hooks/useLang'; + +import ChatOrUserPicker from './ChatOrUserPicker'; +import { filterChatIdsByType } from '../../global/selectors'; +import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; + +export type OwnProps = { + isOpen: boolean; + searchPlaceholder: string; + filter?: ApiChatType[]; + loadMore?: NoneToVoidFunction; + onSelectRecipient: (peerId: string) => void; + onClose: NoneToVoidFunction; + onCloseAnimationEnd?: NoneToVoidFunction; +}; + +type StateProps = { + currentUserId?: string; + chatsById: Record; + activeListIds?: string[]; + archivedListIds?: string[]; + pinnedIds?: string[]; + contactIds?: string[]; +}; + +const RecipientPicker: FC = ({ + isOpen, + currentUserId, + chatsById, + activeListIds, + archivedListIds, + pinnedIds, + contactIds, + filter = API_CHAT_TYPES, + searchPlaceholder, + loadMore, + onSelectRecipient, + onClose, + onCloseAnimationEnd, +}) => { + const lang = useLang(); + const [search, setSearch] = useState(''); + const ids = useMemo(() => { + if (!isOpen) return undefined; + + let priorityIds = pinnedIds || []; + if (currentUserId) { + priorityIds = unique([currentUserId, ...priorityIds]); + } + + // No need for expensive global updates on users, so we avoid them + const global = getGlobal(); + const usersById = global.users.byId; + + const chatIds = [ + ...(activeListIds || []), + ...((search && archivedListIds) || []), + ].filter((id) => { + const chat = chatsById[id]; + const user = usersById[id]; + if (user && isDeletedUser(user)) return false; + + return chat && getCanPostInChat(chat, MAIN_THREAD_ID); + }); + + const sorted = sortChatIds(unique([ + ...filterChatsByName(lang, chatIds, chatsById, search, currentUserId), + ...(contactIds && filter.includes('users') ? filterUsersByName(contactIds, usersById, search) : []), + ]), chatsById, undefined, priorityIds); + + return filterChatIdsByType(global, sorted, filter); + }, [pinnedIds, currentUserId, activeListIds, search, archivedListIds, lang, chatsById, contactIds, filter, isOpen]); + + const renderingIds = useCurrentOrPrev(ids, true)!; + + return ( + + ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const { + chats: { + byId: chatsById, + listIds, + orderedPinnedIds, + }, + currentUserId, + } = global; + + return { + chatsById, + activeListIds: listIds.active, + archivedListIds: listIds.archived, + pinnedIds: orderedPinnedIds.active, + contactIds: global.contactList?.userIds, + currentUserId, + }; + }, +)(RecipientPicker)); diff --git a/src/components/left/settings/BlockUserModal.tsx b/src/components/left/settings/BlockUserModal.tsx index d43c9194e..22ae8968e 100644 --- a/src/components/left/settings/BlockUserModal.tsx +++ b/src/components/left/settings/BlockUserModal.tsx @@ -1,6 +1,6 @@ import type { FC } from '../../../lib/teact/teact'; import React, { - useMemo, useState, memo, useRef, useCallback, useEffect, + useMemo, useState, memo, useCallback, useEffect, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; @@ -40,13 +40,11 @@ const BlockUserModal: FC = ({ } = getActions(); const lang = useLang(); - const [filter, setFilter] = useState(''); - // eslint-disable-next-line no-null/no-null - const filterRef = useRef(null); + const [search, setSearch] = useState(''); useEffect(() => { - setUserSearchQuery({ query: filter }); - }, [filter, setUserSearchQuery]); + setUserSearchQuery({ query: search }); + }, [search, setUserSearchQuery]); const filteredContactIds = useMemo(() => { const availableContactIds = unique([ @@ -56,14 +54,14 @@ const BlockUserModal: FC = ({ return contactId !== currentUserId && !blockedIds.includes(contactId); })); - return filterUsersByName(availableContactIds, usersById, filter) + return filterUsersByName(availableContactIds, usersById, search) .sort((firstId, secondId) => { const firstName = getUserFullName(usersById[firstId]) || ''; const secondName = getUserFullName(usersById[secondId]) || ''; return firstName.localeCompare(secondName); }); - }, [blockedIds, contactIds, currentUserId, filter, localContactIds, usersById]); + }, [blockedIds, contactIds, currentUserId, search, localContactIds, usersById]); const handleRemoveUser = useCallback((userId: string) => { const { id: contactId, accessHash } = usersById[userId] || {}; @@ -78,10 +76,9 @@ const BlockUserModal: FC = ({ diff --git a/src/components/main/AttachBotInstallModal.async.tsx b/src/components/main/AttachBotInstallModal.async.tsx new file mode 100644 index 000000000..3381773b4 --- /dev/null +++ b/src/components/main/AttachBotInstallModal.async.tsx @@ -0,0 +1,17 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { memo } from '../../lib/teact/teact'; +import { Bundles } from '../../util/moduleLoader'; + +import type { OwnProps } from './AttachBotInstallModal'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const AttachBotInstallModalAsync: FC = (props) => { + const { bot } = props; + const AttachBotInstallModal = useModuleLoader(Bundles.Extra, 'AttachBotInstallModal', !bot); + + // eslint-disable-next-line react/jsx-props-no-spreading + return AttachBotInstallModal ? : undefined; +}; + +export default memo(AttachBotInstallModalAsync); diff --git a/src/components/main/BotAttachModal.tsx b/src/components/main/AttachBotInstallModal.tsx similarity index 62% rename from src/components/main/BotAttachModal.tsx rename to src/components/main/AttachBotInstallModal.tsx index e5fbee35b..6fac7bcc6 100644 --- a/src/components/main/BotAttachModal.tsx +++ b/src/components/main/AttachBotInstallModal.tsx @@ -1,7 +1,7 @@ -import type { FC } from '../../lib/teact/teact'; -import React from '../../lib/teact/teact'; +import React, { memo } from '../../lib/teact/teact'; import { getActions } from '../../global'; +import type { FC } from '../../lib/teact/teact'; import type { ApiUser } from '../../api/types'; import useLang from '../../hooks/useLang'; @@ -12,10 +12,10 @@ export type OwnProps = { bot?: ApiUser; }; -const BotAttachModal: FC = ({ +const AttachBotInstallModal: FC = ({ bot, }) => { - const { closeBotAttachRequestModal, confirmBotAttachRequest } = getActions(); + const { cancelAttachBotInstall, confirmAttachBotInstall } = getActions(); const lang = useLang(); @@ -24,12 +24,12 @@ const BotAttachModal: FC = ({ return ( ); }; -export default BotAttachModal; +export default memo(AttachBotInstallModal); diff --git a/src/components/main/AttachBotRecipientPicker.async.tsx b/src/components/main/AttachBotRecipientPicker.async.tsx new file mode 100644 index 000000000..6610e5845 --- /dev/null +++ b/src/components/main/AttachBotRecipientPicker.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { memo } from '../../lib/teact/teact'; +import { Bundles } from '../../util/moduleLoader'; +import type { OwnProps } from './AttachBotRecipientPicker'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const AttachBotRecipientPickerAsync: FC = (props) => { + const { requestedAttachBotInChat } = props; + const AttachBotRecipientPicker = useModuleLoader( + Bundles.Extra, 'AttachBotRecipientPicker', !requestedAttachBotInChat, + ); + + // eslint-disable-next-line react/jsx-props-no-spreading + return AttachBotRecipientPicker ? : undefined; +}; + +export default memo(AttachBotRecipientPickerAsync); diff --git a/src/components/main/AttachBotRecipientPicker.tsx b/src/components/main/AttachBotRecipientPicker.tsx new file mode 100644 index 000000000..5263624a7 --- /dev/null +++ b/src/components/main/AttachBotRecipientPicker.tsx @@ -0,0 +1,53 @@ +import React, { memo, useCallback, useEffect } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import type { FC } from '../../lib/teact/teact'; +import type { GlobalState } from '../../global/types'; + +import useLang from '../../hooks/useLang'; +import useFlag from '../../hooks/useFlag'; + +import RecipientPicker from '../common/RecipientPicker'; + +export type OwnProps = { + requestedAttachBotInChat?: GlobalState['requestedAttachBotInChat']; +}; + +const AttachBotRecipientPicker: FC = ({ + requestedAttachBotInChat, +}) => { + const { cancelAttachBotInChat, callAttachBot } = getActions(); + const lang = useLang(); + + const isOpen = Boolean(requestedAttachBotInChat); + const [isShown, markIsShown, unmarkIsShown] = useFlag(); + useEffect(() => { + if (isOpen) { + markIsShown(); + } + }, [isOpen, markIsShown]); + + const { botId, filter, startParam } = requestedAttachBotInChat || {}; + + const handlePeerRecipient = useCallback((recipientId: string) => { + callAttachBot({ botId: botId!, chatId: recipientId, startParam }); + cancelAttachBotInChat(); + }, [botId, callAttachBot, cancelAttachBotInChat, startParam]); + + if (!isOpen && !isShown) { + return undefined; + } + + return ( + + ); +}; + +export default memo(AttachBotRecipientPicker); diff --git a/src/components/main/BotAttachModal.async.tsx b/src/components/main/BotAttachModal.async.tsx deleted file mode 100644 index 3136b9042..000000000 --- a/src/components/main/BotAttachModal.async.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { FC } from '../../lib/teact/teact'; -import React, { memo } from '../../lib/teact/teact'; -import { Bundles } from '../../util/moduleLoader'; - -import type { OwnProps } from './BotAttachModal'; - -import useModuleLoader from '../../hooks/useModuleLoader'; - -const BotAttachModalAsync: FC = (props) => { - const { bot } = props; - const BotAttachModal = useModuleLoader(Bundles.Extra, 'BotAttachModal', !bot); - - // eslint-disable-next-line react/jsx-props-no-spreading - return BotAttachModal ? : undefined; -}; - -export default memo(BotAttachModalAsync); diff --git a/src/components/main/DraftRecipientPicker.async.tsx b/src/components/main/DraftRecipientPicker.async.tsx new file mode 100644 index 000000000..d47b4d781 --- /dev/null +++ b/src/components/main/DraftRecipientPicker.async.tsx @@ -0,0 +1,16 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { memo } from '../../lib/teact/teact'; +import { Bundles } from '../../util/moduleLoader'; +import type { OwnProps } from './DraftRecipientPicker'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const DraftRecipientPickerAsync: FC = (props) => { + const { requestedDraft } = props; + const DraftRecipientPicker = useModuleLoader(Bundles.Extra, 'DraftRecipientPicker', !requestedDraft); + + // eslint-disable-next-line react/jsx-props-no-spreading + return DraftRecipientPicker ? : undefined; +}; + +export default memo(DraftRecipientPickerAsync); diff --git a/src/components/main/DraftRecipientPicker.tsx b/src/components/main/DraftRecipientPicker.tsx new file mode 100644 index 000000000..8c3515fc3 --- /dev/null +++ b/src/components/main/DraftRecipientPicker.tsx @@ -0,0 +1,59 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { + memo, useCallback, useEffect, +} from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import type { GlobalState } from '../../global/types'; + +import useLang from '../../hooks/useLang'; +import useFlag from '../../hooks/useFlag'; + +import RecipientPicker from '../common/RecipientPicker'; + +export type OwnProps = { + requestedDraft?: GlobalState['requestedDraft']; +}; + +const DraftRecipientPicker: FC = ({ + requestedDraft, +}) => { + const isOpen = Boolean(requestedDraft && !requestedDraft.chatId); + const { + openChatWithDraft, + resetOpenChatWithDraft, + } = getActions(); + + const lang = useLang(); + + const [isShown, markIsShown, unmarkIsShown] = useFlag(); + useEffect(() => { + if (isOpen) { + markIsShown(); + } + }, [isOpen, markIsShown]); + + const handleSelectRecipient = useCallback((recipientId: string) => { + openChatWithDraft({ chatId: recipientId, text: requestedDraft!.text }); + }, [openChatWithDraft, requestedDraft]); + + const handleClose = useCallback(() => { + resetOpenChatWithDraft(); + }, [resetOpenChatWithDraft]); + + if (!isOpen && !isShown) { + return undefined; + } + + return ( + + ); +}; + +export default memo(DraftRecipientPicker); diff --git a/src/components/main/ForwardPicker.async.tsx b/src/components/main/ForwardPicker.async.tsx deleted file mode 100644 index 5a7642851..000000000 --- a/src/components/main/ForwardPicker.async.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { FC } from '../../lib/teact/teact'; -import React, { memo } from '../../lib/teact/teact'; -import { Bundles } from '../../util/moduleLoader'; -import type { OwnProps } from './ForwardPicker'; - -import useModuleLoader from '../../hooks/useModuleLoader'; - -const ForwardPickerAsync: FC = (props) => { - const { isOpen } = props; - const ForwardPicker = useModuleLoader(Bundles.Extra, 'ForwardPicker', !isOpen); - - // eslint-disable-next-line react/jsx-props-no-spreading - return ForwardPicker ? : undefined; -}; - -export default memo(ForwardPickerAsync); diff --git a/src/components/main/ForwardPicker.tsx b/src/components/main/ForwardPicker.tsx deleted file mode 100644 index c71aefe42..000000000 --- a/src/components/main/ForwardPicker.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import type { FC } from '../../lib/teact/teact'; -import React, { - useMemo, useState, memo, useRef, useCallback, useEffect, -} from '../../lib/teact/teact'; -import { getActions, getGlobal, withGlobal } from '../../global'; - -import type { ApiChat } from '../../api/types'; -import { MAIN_THREAD_ID } from '../../api/types'; -import type { GlobalState } from '../../global/types'; - -import { - filterChatsByName, - filterUsersByName, - getCanPostInChat, - sortChatIds, -} from '../../global/helpers'; -import { unique } from '../../util/iteratees'; -import useLang from '../../hooks/useLang'; -import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; -import useFlag from '../../hooks/useFlag'; - -import ChatOrUserPicker from '../common/ChatOrUserPicker'; - -export type OwnProps = { - isOpen: boolean; -}; - -type StateProps = { - chatsById: Record; - activeListIds?: string[]; - archivedListIds?: string[]; - pinnedIds?: string[]; - contactIds?: string[]; - currentUserId?: string; - switchBotInline?: GlobalState['switchBotInline']; -}; - -const ForwardPicker: FC = ({ - chatsById, - activeListIds, - archivedListIds, - pinnedIds, - contactIds, - currentUserId, - isOpen, - switchBotInline, -}) => { - const { - setForwardChatId, - exitForwardMode, - openChatWithText, - resetSwitchBotInline, - } = getActions(); - - const lang = useLang(); - const [filter, setFilter] = useState(''); - // eslint-disable-next-line no-null/no-null - const filterRef = useRef(null); - - const [isShown, markIsShown, unmarkIsShown] = useFlag(); - useEffect(() => { - if (isOpen) { - markIsShown(); - } - }, [isOpen, markIsShown]); - - const chatAndContactIds = useMemo(() => { - if (!isOpen) { - return undefined; - } - - let priorityIds = pinnedIds || []; - if (currentUserId) { - priorityIds = unique([currentUserId, ...priorityIds]); - } - - const chatIds = [ - ...(activeListIds || []), - ...((filter && archivedListIds) || []), - ].filter((id) => { - const chat = chatsById[id]; - - return chat && getCanPostInChat(chat, MAIN_THREAD_ID); - }); - - // No need for expensive global updates on users, so we avoid them - const usersById = getGlobal().users.byId; - - return sortChatIds(unique([ - ...filterChatsByName(lang, chatIds, chatsById, filter, currentUserId), - ...(contactIds ? filterUsersByName(contactIds, usersById, filter) : []), - ]), chatsById, undefined, priorityIds); - }, [activeListIds, archivedListIds, chatsById, contactIds, currentUserId, filter, isOpen, lang, pinnedIds]); - - const handleSelectUser = useCallback((userId: string) => { - if (switchBotInline) { - const text = `@${switchBotInline.botUsername} ${switchBotInline.query}`; - openChatWithText({ chatId: userId, text }); - resetSwitchBotInline(); - } else { - setForwardChatId({ id: userId }); - } - }, [openChatWithText, resetSwitchBotInline, setForwardChatId, switchBotInline]); - - const handleClose = useCallback(() => { - exitForwardMode(); - resetSwitchBotInline(); - }, [exitForwardMode, resetSwitchBotInline]); - - const renderingChatAndContactIds = useCurrentOrPrev(chatAndContactIds, true)!; - - if (!isOpen && !isShown) { - return undefined; - } - - return ( - - ); -}; - -export default memo(withGlobal( - (global): StateProps => { - const { - chats: { - byId: chatsById, - listIds, - orderedPinnedIds, - }, - currentUserId, - switchBotInline, - } = global; - - return { - chatsById, - activeListIds: listIds.active, - archivedListIds: listIds.archived, - pinnedIds: orderedPinnedIds.active, - contactIds: global.contactList?.userIds, - currentUserId, - switchBotInline, - }; - }, -)(ForwardPicker)); diff --git a/src/components/main/ForwardRecipientPicker.async.tsx b/src/components/main/ForwardRecipientPicker.async.tsx new file mode 100644 index 000000000..3e27bae80 --- /dev/null +++ b/src/components/main/ForwardRecipientPicker.async.tsx @@ -0,0 +1,16 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { memo } from '../../lib/teact/teact'; +import { Bundles } from '../../util/moduleLoader'; +import type { OwnProps } from './ForwardRecipientPicker'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const ForwardRecipientPickerAsync: FC = (props) => { + const { isOpen } = props; + const ForwardRecipientPicker = useModuleLoader(Bundles.Extra, 'ForwardRecipientPicker', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return ForwardRecipientPicker ? : undefined; +}; + +export default memo(ForwardRecipientPickerAsync); diff --git a/src/components/main/ForwardRecipientPicker.tsx b/src/components/main/ForwardRecipientPicker.tsx new file mode 100644 index 000000000..28aacb8b1 --- /dev/null +++ b/src/components/main/ForwardRecipientPicker.tsx @@ -0,0 +1,56 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { + memo, useCallback, useEffect, +} from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import useLang from '../../hooks/useLang'; +import useFlag from '../../hooks/useFlag'; + +import RecipientPicker from '../common/RecipientPicker'; + +export type OwnProps = { + isOpen: boolean; +}; + +const ForwardRecipientPicker: FC = ({ + isOpen, +}) => { + const { + setForwardChatId, + exitForwardMode, + } = getActions(); + + const lang = useLang(); + + const [isShown, markIsShown, unmarkIsShown] = useFlag(); + useEffect(() => { + if (isOpen) { + markIsShown(); + } + }, [isOpen, markIsShown]); + + const handleSelectRecipient = useCallback((recipientId: string) => { + setForwardChatId({ id: recipientId }); + }, [setForwardChatId]); + + const handleClose = useCallback(() => { + exitForwardMode(); + }, [exitForwardMode]); + + if (!isOpen && !isShown) { + return undefined; + } + + return ( + + ); +}; + +export default memo(ForwardRecipientPicker); diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 16e1d02c0..f7d1078f6 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -52,7 +52,7 @@ import DownloadManager from './DownloadManager'; import GameModal from './GameModal'; import Notifications from './Notifications.async'; import Dialogs from './Dialogs.async'; -import ForwardPicker from './ForwardPicker.async'; +import ForwardRecipientPicker from './ForwardRecipientPicker.async'; import SafeLinkModal from './SafeLinkModal.async'; import HistoryCalendar from './HistoryCalendar.async'; import GroupCall from '../calls/group/GroupCall.async'; @@ -63,7 +63,7 @@ import NewContactModal from './NewContactModal.async'; import RatePhoneCallModal from '../calls/phone/RatePhoneCallModal.async'; import WebAppModal from './WebAppModal.async'; import BotTrustModal from './BotTrustModal.async'; -import BotAttachModal from './BotAttachModal.async'; +import AttachBotInstallModal from './AttachBotInstallModal.async'; import ConfettiContainer from './ConfettiContainer'; import UrlAuthModal from './UrlAuthModal.async'; import PremiumMainModal from './premium/PremiumMainModal.async'; @@ -72,6 +72,8 @@ import ReceiptModal from '../payment/ReceiptModal.async'; import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async'; import DeleteFolderDialog from './DeleteFolderDialog.async'; import CustomEmojiSetsModal from '../common/CustomEmojiSetsModal.async'; +import DraftRecipientPicker from './DraftRecipientPicker.async'; +import AttachBotRecipientPicker from './AttachBotRecipientPicker.async'; import './Main.scss'; @@ -109,7 +111,9 @@ type StateProps = { isPremiumModalOpen?: boolean; botTrustRequest?: GlobalState['botTrustRequest']; botTrustRequestBot?: ApiUser; - botAttachRequestBot?: ApiUser; + attachBotToInstall?: ApiUser; + requestedAttachBotInChat?: GlobalState['requestedAttachBotInChat']; + requestedDraft?: GlobalState['requestedDraft']; currentUser?: ApiUser; urlAuth?: GlobalState['urlAuth']; limitReached?: ApiLimitTypeWithModal; @@ -158,7 +162,9 @@ const Main: FC = ({ isRatePhoneCallModalOpen, botTrustRequest, botTrustRequestBot, - botAttachRequestBot, + attachBotToInstall, + requestedAttachBotInChat, + requestedDraft, webApp, currentUser, urlAuth, @@ -186,7 +192,7 @@ const Main: FC = ({ closeCustomEmojiSets, checkVersionNotification, loadAppConfig, - loadAttachMenuBots, + loadAttachBots, loadContactList, loadCustomEmojis, closePaymentModal, @@ -219,14 +225,14 @@ const Main: FC = ({ loadNotificationExceptions(); loadTopInlineBots(); loadEmojiKeywords({ language: BASE_EMOJI_KEYWORD_LANG }); - loadAttachMenuBots(); + loadAttachBots(); loadContactList(); loadPremiumGifts(); checkAppVersion(); } }, [ lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings, - loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachMenuBots, loadContactList, + loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachBots, loadContactList, loadPremiumGifts, checkAppVersion, ]); @@ -423,7 +429,8 @@ const Main: FC = ({ - + + {audioMessage && } @@ -454,7 +461,8 @@ const Main: FC = ({ - + + {isPremiumModalOpen && } @@ -494,8 +502,19 @@ export default memo(withGlobal( animationLevel, language, wasTimeFormatSetManually, }, }, + connectionState, botTrustRequest, - botAttachRequest, + requestedAttachBotInstall, + requestedAttachBotInChat, + requestedDraft, + urlAuth, + webApp, + safeLinkModalUrl, + authState, + lastSyncTime, + openedStickerSetShortName, + openedCustomEmojiSetIds, + shouldSkipHistoryAnimations, } = global; const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer; const audioMessage = audioChatId && audioMessageId @@ -507,9 +526,9 @@ export default memo(withGlobal( const currentUser = global.currentUserId ? selectUser(global, global.currentUserId) : undefined; return { - connectionState: global.connectionState, - authState: global.authState, - lastSyncTime: global.lastSyncTime, + connectionState, + authState, + lastSyncTime, isLeftColumnOpen: global.isLeftColumnShown, isRightColumnOpen: selectIsRightColumnShown(global), isMediaViewerOpen: selectIsMediaViewerOpen(global), @@ -517,11 +536,11 @@ export default memo(withGlobal( hasNotifications: Boolean(global.notifications.length), hasDialogs: Boolean(global.dialogs.length), audioMessage, - safeLinkModalUrl: global.safeLinkModalUrl, + safeLinkModalUrl, isHistoryCalendarOpen: Boolean(global.historyCalendarSelectedAt), - shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations, - openedStickerSetShortName: global.openedStickerSetShortName, - openedCustomEmojiSetIds: global.openedCustomEmojiSetIds, + shouldSkipHistoryAnimations, + openedStickerSetShortName, + openedCustomEmojiSetIds, isServiceChatReady: selectIsServiceChatReady(global), activeGroupCallId: global.groupCalls.activeGroupCallId, animationLevel, @@ -537,15 +556,17 @@ export default memo(withGlobal( isRatePhoneCallModalOpen: Boolean(global.ratingPhoneCall), botTrustRequest, botTrustRequestBot: botTrustRequest && selectUser(global, botTrustRequest.botId), - botAttachRequestBot: botAttachRequest && selectUser(global, botAttachRequest.botId), - webApp: global.webApp, + attachBotToInstall: requestedAttachBotInstall && selectUser(global, requestedAttachBotInstall.botId), + requestedAttachBotInChat, + webApp, currentUser, - urlAuth: global.urlAuth, + urlAuth, isPremiumModalOpen: global.premiumModal?.isOpen, limitReached: global.limitReachedModal?.limit, isPaymentModalOpen: global.payment.isPaymentModalOpen, isReceiptModalOpen: Boolean(global.payment.receipt), deleteFolderDialogId: global.deleteFolderDialogModal, + requestedDraft, }; }, )(Main)); diff --git a/src/components/main/WebAppModal.tsx b/src/components/main/WebAppModal.tsx index fefb512fd..61f742ef4 100644 --- a/src/components/main/WebAppModal.tsx +++ b/src/components/main/WebAppModal.tsx @@ -4,7 +4,7 @@ import React, { import { getActions, withGlobal } from '../../global'; import type { FC } from '../../lib/teact/teact'; -import type { ApiAttachMenuBot, ApiChat, ApiUser } from '../../api/types'; +import type { ApiAttachBot, ApiChat, ApiUser } from '../../api/types'; import type { GlobalState } from '../../global/types'; import type { ThemeKey } from '../../types'; import type { PopupOptions, WebAppInboundEvent } from './hooks/useWebAppFrame'; @@ -48,7 +48,7 @@ export type OwnProps = { type StateProps = { chat?: ApiChat; bot?: ApiUser; - attachMenuBot?: ApiAttachMenuBot; + attachBot?: ApiAttachBot; theme?: ThemeKey; isPaymentModalOpen?: boolean; paymentStatus?: GlobalState['payment']['status']; @@ -78,7 +78,7 @@ const WebAppModal: FC = ({ webApp, chat, bot, - attachMenuBot, + attachBot, theme, isPaymentModalOpen, paymentStatus, @@ -87,7 +87,7 @@ const WebAppModal: FC = ({ closeWebApp, sendWebViewData, prolongWebView, - toggleBotInAttachMenu, + toggleAttachBot, openTelegramLink, openChat, openInvoice, @@ -279,11 +279,11 @@ const WebAppModal: FC = ({ }, [isPaymentModalOpen, paymentStatus, sendEvent, setWebAppPaymentSlug, webApp] as const); const handleToggleClick = useCallback(() => { - toggleBotInAttachMenu({ + toggleAttachBot({ botId: bot!.id, - isEnabled: !attachMenuBot, + isEnabled: !attachBot, }); - }, [bot, attachMenuBot, toggleBotInAttachMenu]); + }, [bot, attachBot, toggleAttachBot]); const handleBackClick = useCallback(() => { if (isBackButtonVisible) { @@ -353,16 +353,16 @@ const WebAppModal: FC = ({ {lang('BotWebViewOpenBot')} )} {lang('WebApp.ReloadPage')} - {bot?.isAttachMenuBot && ( + {bot?.isAttachBot && ( - {lang(attachMenuBot ? 'WebApp.RemoveBot' : 'WebApp.AddToAttachmentAdd')} + {lang(attachBot ? 'WebApp.RemoveBot' : 'WebApp.AddToAttachmentAdd')} )} - {attachMenuBot?.hasSettings && ( + {attachBot?.hasSettings && ( {lang('Settings')} @@ -371,7 +371,7 @@ const WebAppModal: FC = ({ ); }, [ - lang, handleBackClick, bot, MoreMenuButton, chat, openBotChat, handleRefreshClick, attachMenuBot, + lang, handleBackClick, bot, MoreMenuButton, chat, openBotChat, handleRefreshClick, attachBot, handleToggleClick, handleSettingsButtonClick, isBackButtonVisible, headerColor, backButtonClassName, ]); @@ -494,14 +494,14 @@ const WebAppModal: FC = ({ export default memo(withGlobal( (global, { webApp }): StateProps => { const { botId } = webApp || {}; - const attachMenuBot = botId ? global.attachMenu.bots[botId] : undefined; + const attachBot = botId ? global.attachMenu.bots[botId] : undefined; const bot = botId ? selectUser(global, botId) : undefined; const chat = selectCurrentChat(global); const theme = selectTheme(global); const { isPaymentModalOpen, status } = global.payment; return { - attachMenuBot, + attachBot, bot, chat, theme, diff --git a/src/components/middle/MessageSelectToolbar.tsx b/src/components/middle/MessageSelectToolbar.tsx index 8d3040abc..0e17906e2 100644 --- a/src/components/middle/MessageSelectToolbar.tsx +++ b/src/components/middle/MessageSelectToolbar.tsx @@ -39,7 +39,7 @@ type StateProps = { canReportMessages?: boolean; canDownloadMessages?: boolean; hasProtectedMessage?: boolean; - isForwardModalOpen?: boolean; + isAnyModalOpen?: boolean; selectedMessageIds?: number[]; }; @@ -53,7 +53,7 @@ const MessageSelectToolbar: FC = ({ canReportMessages, canDownloadMessages, hasProtectedMessage, - isForwardModalOpen, + isAnyModalOpen, selectedMessageIds, }) => { const { @@ -70,7 +70,7 @@ const MessageSelectToolbar: FC = ({ useCopySelectedMessages(Boolean(isActive), copySelectedMessages); useEffect(() => { - return isActive && !isDeleteModalOpen && !isReportModalOpen && !isForwardModalOpen + return isActive && !isDeleteModalOpen && !isReportModalOpen && !isAnyModalOpen ? captureKeyboardListeners({ onBackspace: canDeleteMessages ? openDeleteModal : undefined, onDelete: canDeleteMessages ? openDeleteModal : undefined, @@ -78,7 +78,7 @@ const MessageSelectToolbar: FC = ({ }) : undefined; }, [ - isActive, isDeleteModalOpen, isReportModalOpen, openDeleteModal, exitMessageSelectMode, isForwardModalOpen, + isActive, isDeleteModalOpen, isReportModalOpen, openDeleteModal, exitMessageSelectMode, isAnyModalOpen, canDeleteMessages, ]); @@ -183,6 +183,8 @@ export default memo(withGlobal( const { messageIds: selectedMessageIds } = global.selectedMessages || {}; const hasProtectedMessage = chatId ? selectHasProtectedMessage(global, chatId, selectedMessageIds) : false; const isForwardModalOpen = global.forwardMessages.isModalShown; + const isAnyModalOpen = Boolean(isForwardModalOpen || global.requestedDraft + || global.requestedAttachBotInChat || global.requestedAttachBotInstall); return { isSchedule, @@ -192,7 +194,7 @@ export default memo(withGlobal( canDownloadMessages: canDownload, selectedMessageIds, hasProtectedMessage, - isForwardModalOpen, + isAnyModalOpen, }; }, )(MessageSelectToolbar)); diff --git a/src/components/middle/composer/AttachmentMenuBotIcon.module.scss b/src/components/middle/composer/AttachBotIcon.module.scss similarity index 100% rename from src/components/middle/composer/AttachmentMenuBotIcon.module.scss rename to src/components/middle/composer/AttachBotIcon.module.scss diff --git a/src/components/middle/composer/AttachmentMenuBotIcon.tsx b/src/components/middle/composer/AttachBotIcon.tsx similarity index 91% rename from src/components/middle/composer/AttachmentMenuBotIcon.tsx rename to src/components/middle/composer/AttachBotIcon.tsx index f224452ad..71140a48e 100644 --- a/src/components/middle/composer/AttachmentMenuBotIcon.tsx +++ b/src/components/middle/composer/AttachBotIcon.tsx @@ -10,7 +10,7 @@ import useMedia from '../../../hooks/useMedia'; import { getDocumentMediaHash } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; -import styles from './AttachmentMenuBotIcon.module.scss'; +import styles from './AttachBotIcon.module.scss'; type OwnProps = { icon: ApiDocument; @@ -22,7 +22,7 @@ const DARK_THEME_COLOR = 'rgb(170, 170, 170)'; const LIGHT_THEME_COLOR = 'rgb(112, 117, 121)'; const COLOR_REPLACE_PATTERN = /#fff/gi; -const AttachmentMenuBotIcon: FC = ({ +const AttachBotIcon: FC = ({ icon, theme, }) => { const mediaData = useMedia(getDocumentMediaHash(icon), false, ApiMediaFormat.Text); @@ -48,4 +48,4 @@ const AttachmentMenuBotIcon: FC = ({ ); }; -export default memo(AttachmentMenuBotIcon); +export default memo(AttachBotIcon); diff --git a/src/components/middle/composer/AttachmentMenuBotItem.tsx b/src/components/middle/composer/AttachBotItem.tsx similarity index 82% rename from src/components/middle/composer/AttachmentMenuBotItem.tsx rename to src/components/middle/composer/AttachBotItem.tsx index 3d8e44b5b..44b19f676 100644 --- a/src/components/middle/composer/AttachmentMenuBotItem.tsx +++ b/src/components/middle/composer/AttachBotItem.tsx @@ -5,7 +5,7 @@ import React, { import { getActions } from '../../../global'; import type { IAnchorPosition, ISettings } from '../../../types'; -import type { ApiAttachMenuBot } from '../../../api/types'; +import type { ApiAttachBot } from '../../../api/types'; import useFlag from '../../../hooks/useFlag'; import useLang from '../../../hooks/useLang'; @@ -13,24 +13,24 @@ import useLang from '../../../hooks/useLang'; import Portal from '../../ui/Portal'; import Menu from '../../ui/Menu'; import MenuItem from '../../ui/MenuItem'; -import AttachmentMenuBotIcon from './AttachmentMenuBotIcon'; +import AttachBotIcon from './AttachBotIcon'; type OwnProps = { - bot: ApiAttachMenuBot; + bot: ApiAttachBot; theme: ISettings['theme']; chatId: string; onMenuOpened: VoidFunction; onMenuClosed: VoidFunction; }; -const AttachmentMenuBotItem: FC = ({ +const AttachBotItem: FC = ({ bot, theme, chatId, onMenuOpened, onMenuClosed, }) => { - const { callAttachMenuBot, toggleBotInAttachMenu } = getActions(); + const { callAttachBot, toggleAttachBot } = getActions(); const lang = useLang(); @@ -59,19 +59,19 @@ const AttachmentMenuBotItem: FC = ({ }, []); const handleRemoveBot = useCallback(() => { - toggleBotInAttachMenu({ + toggleAttachBot({ botId: bot.id, isEnabled: false, }); - }, [bot.id, toggleBotInAttachMenu]); + }, [bot.id, toggleAttachBot]); return ( } + customIcon={icon && } icon={!icon ? 'bots' : undefined} // eslint-disable-next-line react/jsx-no-bind - onClick={() => callAttachMenuBot({ + onClick={() => callAttachBot({ botId: bot.id, chatId, })} @@ -98,4 +98,4 @@ const AttachmentMenuBotItem: FC = ({ ); }; -export default memo(AttachmentMenuBotItem); +export default memo(AttachBotItem); diff --git a/src/components/middle/composer/AttachMenu.tsx b/src/components/middle/composer/AttachMenu.tsx index e4e4c5513..df0f78662 100644 --- a/src/components/middle/composer/AttachMenu.tsx +++ b/src/components/middle/composer/AttachMenu.tsx @@ -18,7 +18,7 @@ import useFlag from '../../../hooks/useFlag'; import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton'; import Menu from '../../ui/Menu'; import MenuItem from '../../ui/MenuItem'; -import AttachmentMenuBotItem from './AttachmentMenuBotItem'; +import AttachBotItem from './AttachBotItem'; import './AttachMenu.scss'; @@ -28,7 +28,7 @@ export type OwnProps = { canAttachMedia: boolean; canAttachPolls: boolean; isScheduled?: boolean; - attachMenuBots: GlobalState['attachMenu']['bots']; + attachBots: GlobalState['attachMenu']['bots']; peerType?: ApiAttachMenuPeerType; onFileSelect: (files: File[], isQuick: boolean) => void; onPollCreate: () => void; @@ -40,7 +40,7 @@ const AttachMenu: FC = ({ isButtonVisible, canAttachMedia, canAttachPolls, - attachMenuBots, + attachBots, peerType, isScheduled, onFileSelect, @@ -85,14 +85,14 @@ const AttachMenu: FC = ({ }, [handleFileSelect]); const bots = useMemo(() => { - return Object.values(attachMenuBots).filter((bot) => { + return Object.values(attachBots).filter((bot) => { if (!peerType) return false; - if (peerType === 'bot' && bot.id === chatId && bot.peerTypes.includes('self')) { + if (peerType === 'bots' && bot.id === chatId && bot.peerTypes.includes('self')) { return true; } return bot.peerTypes.includes(peerType); }); - }, [attachMenuBots, chatId, peerType]); + }, [attachBots, chatId, peerType]); const lang = useLang(); @@ -146,7 +146,7 @@ const AttachMenu: FC = ({ )} {canAttachMedia && !isScheduled && bots.map((bot) => ( - = ({ editingDraft, requestedText, botMenuButton, - attachMenuBots, + attachBots, attachMenuPeerType, theme, }) => { @@ -268,8 +268,8 @@ const Composer: FC = ({ sendInlineBotResult, loadSendAs, loadFullChat, - resetOpenChatWithText, - callAttachMenuBot, + resetOpenChatWithDraft, + callAttachBot, openLimitReachedModal, showNotification, } = getActions(); @@ -674,10 +674,10 @@ const Composer: FC = ({ const handleClickBotMenu = useCallback(() => { if (botMenuButton?.type !== 'webApp') return; - callAttachMenuBot({ + callAttachBot({ botId: chatId, chatId, isFromBotMenu: true, url: botMenuButton.url, }); - }, [botMenuButton, callAttachMenuBot, chatId]); + }, [botMenuButton, callAttachBot, chatId]); const handleActivateBotCommandMenu = useCallback(() => { closeSymbolMenu(); @@ -727,13 +727,13 @@ const Composer: FC = ({ useEffect(() => { if (requestedText) { setHtml(requestedText); - resetOpenChatWithText(); + resetOpenChatWithDraft(); requestAnimationFrame(() => { const messageInput = document.getElementById(EDITABLE_INPUT_ID)!; focusEditableElement(messageInput, true); }); } - }, [requestedText, resetOpenChatWithText]); + }, [requestedText, resetOpenChatWithDraft]); const handleStickerSelect = useCallback(( sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean, shouldPreserveInput = false, @@ -1211,7 +1211,7 @@ const Composer: FC = ({ onFileSelect={handleFileSelect} onPollCreate={openPollModal} isScheduled={shouldSchedule} - attachMenuBots={attachMenuBots} + attachBots={attachBots} peerType={attachMenuPeerType} theme={theme} /> @@ -1377,8 +1377,8 @@ export default memo(withGlobal( sendAsId, editingDraft, requestedText, - attachMenuBots: global.attachMenu.bots, - attachMenuPeerType: selectAttachMenuPeerType(global, chatId), + attachBots: global.attachMenu.bots, + attachMenuPeerType: selectChatType(global, chatId), theme: selectTheme(global), fileSizeLimit: selectCurrentLimit(global, 'uploadMaxFileparts') * MAX_UPLOAD_FILEPART_SIZE, captionLimit: selectCurrentLimit(global, 'captionLength'), diff --git a/src/components/middle/composer/hooks/useInlineBotTooltip.ts b/src/components/middle/composer/hooks/useInlineBotTooltip.ts index 3088aee9e..58e495d0d 100644 --- a/src/components/middle/composer/hooks/useInlineBotTooltip.ts +++ b/src/components/middle/composer/hooks/useInlineBotTooltip.ts @@ -54,10 +54,12 @@ export default function useInlineBotTooltip( }, [query, isAllowed, queryInlineBot, chatId, usernameLowered]); const loadMore = useCallback(() => { - queryInlineBot({ - chatId, username: usernameLowered, query, offset, - }); - }, [offset, chatId, query, queryInlineBot, usernameLowered]); + if (isAllowed && usernameLowered && chatId) { + queryInlineBot({ + chatId, username: usernameLowered, query, offset, + }); + } + }, [isAllowed, usernameLowered, chatId, queryInlineBot, query, offset]); useEffect(() => { if (isAllowed && botId && (switchPm || (results?.length))) { diff --git a/src/components/right/management/RemoveGroupUserModal.tsx b/src/components/right/management/RemoveGroupUserModal.tsx index 6001a980e..0a243fdf9 100644 --- a/src/components/right/management/RemoveGroupUserModal.tsx +++ b/src/components/right/management/RemoveGroupUserModal.tsx @@ -1,6 +1,6 @@ import type { FC } from '../../../lib/teact/teact'; import React, { - useMemo, useState, memo, useRef, useCallback, + useMemo, useState, memo, useCallback, } from '../../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../../global'; @@ -33,9 +33,7 @@ const RemoveGroupUserModal: FC = ({ } = getActions(); const lang = useLang(); - const [filter, setFilter] = useState(''); - // eslint-disable-next-line no-null/no-null - const filterRef = useRef(null); + const [search, setSearch] = useState(''); const usersId = useMemo(() => { const availableMemberIds = (chat.fullInfo?.members || []) @@ -49,8 +47,8 @@ const RemoveGroupUserModal: FC = ({ // No need for expensive global updates on users, so we avoid them const usersById = getGlobal().users.byId; - return filterUsersByName(availableMemberIds, usersById, filter); - }, [chat.fullInfo?.members, currentUserId, filter]); + return filterUsersByName(availableMemberIds, usersById, search); + }, [chat.fullInfo?.members, currentUserId, search]); const handleRemoveUser = useCallback((userId: string) => { deleteChatMember({ chatId: chat.id, userId }); @@ -61,10 +59,9 @@ const RemoveGroupUserModal: FC = ({ { return undefined; } - const text = `@${botSender.username} ${query}`; - - if (isSamePeer) { - actions.openChatWithText({ chatId: chat.id, text }); - return undefined; - } - - return { - ...global, - switchBotInline: { - query, - botUsername: botSender.username, - }, - }; -}); - -addActionHandler('resetSwitchBotInline', (global) => { - return { - ...global, - switchBotInline: undefined, - }; + actions.openChatWithDraft({ + text: `@${botSender.username} ${query}`, + chatId: isSamePeer ? chat.id : undefined, + }); + return undefined; }); addActionHandler('sendInlineBotResult', (global, actions, payload) => { @@ -557,28 +541,28 @@ addActionHandler('markBotTrusted', (global, actions, payload) => { } }); -addActionHandler('loadAttachMenuBots', async (global, actions, payload) => { +addActionHandler('loadAttachBots', async (global, actions, payload) => { const { hash } = payload || {}; - await loadAttachMenuBots(hash); + await loadAttachBots(hash); }); -addActionHandler('toggleBotInAttachMenu', async (global, actions, payload) => { +addActionHandler('toggleAttachBot', async (global, actions, payload) => { const { botId, isEnabled } = payload; const bot = selectUser(global, botId); if (!bot) return; - await toggleBotInAttachMenu(bot, isEnabled); + await toggleAttachBot(bot, isEnabled); }); -async function toggleBotInAttachMenu(bot: ApiUser, isEnabled: boolean) { - await callApi('toggleBotInAttachMenu', { bot, isEnabled }); - await loadAttachMenuBots(); +async function toggleAttachBot(bot: ApiUser, isEnabled: boolean) { + await callApi('toggleAttachBot', { bot, isEnabled }); + await loadAttachBots(); } -async function loadAttachMenuBots(hash?: string) { - const result = await callApi('loadAttachMenuBots', { hash }); +async function loadAttachBots(hash?: string) { + const result = await callApi('loadAttachBots', { hash }); if (!result) { return; } @@ -593,7 +577,7 @@ async function loadAttachMenuBots(hash?: string) { }); } -addActionHandler('callAttachMenuBot', (global, actions, payload) => { +addActionHandler('callAttachBot', (global, actions, payload) => { const { chatId, botId, isFromBotMenu, url, startParam, } = payload; @@ -601,14 +585,17 @@ addActionHandler('callAttachMenuBot', (global, actions, payload) => { if (!isFromBotMenu && !bots[botId]) { return { ...global, - botAttachRequest: { + requestedAttachBotInstall: { botId, - chatId, - startParam, + onConfirm: { + action: 'callAttachBot', + payload: { chatId, botId, startParam }, + }, }, }; } const theme = extractCurrentThemeParams(); + actions.openChat({ id: chatId }); actions.requestWebView({ url, peerId: chatId, @@ -622,29 +609,67 @@ addActionHandler('callAttachMenuBot', (global, actions, payload) => { return undefined; }); -addActionHandler('confirmBotAttachRequest', async (global, actions) => { - const { botAttachRequest } = global; - if (!botAttachRequest) return; +addActionHandler('confirmAttachBotInstall', async (global) => { + const { requestedAttachBotInstall } = global; - const { botId, chatId, startParam } = botAttachRequest; + const { botId, onConfirm } = requestedAttachBotInstall!; setGlobal({ ...global, - botAttachRequest: undefined, + requestedAttachBotInstall: undefined, }); const bot = selectUser(global, botId); if (!bot) return; - await toggleBotInAttachMenu(bot, true); - - actions.callAttachMenuBot({ chatId, botId, startParam }); + await toggleAttachBot(bot, true); + if (onConfirm) { + const { action, payload } = onConfirm; + getActions()[action](payload); + } }); -addActionHandler('closeBotAttachRequestModal', (global) => { +addActionHandler('cancelAttachBotInstall', (global) => { return { ...global, - botAttachRequest: undefined, + requestedAttachBotInstall: undefined, + }; +}); + +addActionHandler('requestAttachBotInChat', (global, actions, payload) => { + const { botId, filter, startParam } = payload; + const currentChatId = selectCurrentMessageList(global)?.chatId; + + const { attachMenu: { bots } } = global; + const bot = bots[botId]; + if (!bot) return; + const supportedFilters = bot.peerTypes.filter((type): type is ApiChatType => ( + type !== 'self' && filter.includes(type) + )); + + if (!supportedFilters.length) { + actions.callAttachBot({ + chatId: currentChatId || botId, + botId, + startParam, + }); + return; + } + + setGlobal({ + ...global, + requestedAttachBotInChat: { + botId, + filter: supportedFilters, + startParam, + }, + }); +}); + +addActionHandler('cancelAttachBotInChat', (global) => { + return { + ...global, + requestedAttachBotInChat: undefined, }; }); diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index ee7880e41..aa800c3ad 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -7,7 +7,7 @@ import type { } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types'; import { NewChatMembersProgress, ChatCreationProgress, ManagementProgress } from '../../../types'; -import type { GlobalActions } from '../../types'; +import type { GlobalActions, GlobalState } from '../../types'; import { ARCHIVED_FOLDER_ID, @@ -35,9 +35,14 @@ import { import { buildCollectionByKey, omit } from '../../../util/iteratees'; import { debounce, pause, throttle } from '../../../util/schedulers'; import { - isChatSummaryOnly, isChatArchived, isChatBasicGroup, isUserBot, isChatChannel, isChatSuperGroup, + isChatSummaryOnly, + isChatArchived, + isChatBasicGroup, + isChatChannel, + isChatSuperGroup, + isUserBot, } from '../../helpers'; -import { processDeepLink } from '../../../util/deeplink'; +import { formatShareText, parseChooseParameter, processDeepLink } from '../../../util/deeplink'; import { updateGroupCall } from '../../reducers/calls'; import { selectGroupCall } from '../../selectors/calls'; import { getOrderedIds } from '../../../util/folderManager'; @@ -586,10 +591,21 @@ addActionHandler('openChatByPhoneNumber', async (global, actions, payload) => { addActionHandler('openTelegramLink', (global, actions, payload) => { const { url } = payload!; + const { + openChatByPhoneNumber, + openChatByInvite, + openStickerSet, + openChatWithDraft, + joinVoiceChatByLink, + showNotification, + focusMessage, + openInvoice, + processAttachBotParameters, + openChatByUsername: openChatByUsernameAction, + } = actions; - const tgLinkMatch = url.match(RE_TG_LINK); - if (tgLinkMatch) { - processDeepLink(tgLinkMatch[0]); + if (url.match(RE_TG_LINK)) { + processDeepLink(url); return; } @@ -611,9 +627,10 @@ addActionHandler('openTelegramLink', (global, actions, payload) => { } const startAttach = params.hasOwnProperty('startattach') && !params.startattach ? true : params.startattach; + const choose = parseChooseParameter(params.choose); if (part1.match(/^\+([0-9]+)(\?|$)/)) { - actions.openChatByPhoneNumber({ + openChatByPhoneNumber({ phoneNumber: part1.substr(1, part1.length - 1), startAttach, attach: params.attach, @@ -626,12 +643,12 @@ addActionHandler('openTelegramLink', (global, actions, payload) => { } if (hash) { - actions.openChatByInvite({ hash }); + openChatByInvite({ hash }); return; } if (part1 === 'addstickers' || part1 === 'addemoji') { - actions.openStickerSet({ + openStickerSet({ stickerSetInfo: { shortName: part2, }, @@ -643,8 +660,11 @@ addActionHandler('openTelegramLink', (global, actions, payload) => { const messageId = part3 ? Number(part3) : undefined; const commentId = params.comment ? Number(params.comment) : undefined; - if (params.hasOwnProperty('voicechat') || params.hasOwnProperty('livestream')) { - actions.joinVoiceChatByLink({ + if (part1 === 'share') { + const text = formatShareText(params.url, params.text); + openChatWithDraft({ text }); + } else if (params.hasOwnProperty('voicechat') || params.hasOwnProperty('livestream')) { + joinVoiceChatByLink({ username: part1, inviteHash: params.voicechat || params.livestream, }); @@ -652,24 +672,30 @@ addActionHandler('openTelegramLink', (global, actions, payload) => { const chatId = `-${chatOrChannelPostId}`; const chat = selectChat(global, chatId); if (!chat) { - actions.showNotification({ message: 'Chat does not exist' }); + showNotification({ message: 'Chat does not exist' }); return; } - actions.focusMessage({ + focusMessage({ chatId, messageId, }); } else if (part1.startsWith('$')) { - actions.openInvoice({ + openInvoice({ slug: part1.substring(1), }); } else if (part1 === 'invoice') { - actions.openInvoice({ + openInvoice({ slug: part2, }); + } else if (startAttach && choose) { + processAttachBotParameters({ + username: part1, + filter: choose, + ...(typeof startAttach === 'string' && { startParam: startAttach }), + }); } else { - actions.openChatByUsername({ + openChatByUsernameAction({ username: part1, messageId: messageId || Number(chatOrChannelPostId), commentId, @@ -1002,10 +1028,10 @@ addActionHandler('setActiveChatFolder', (global, actions, payload) => { }; }); -addActionHandler('resetOpenChatWithText', (global) => { +addActionHandler('resetOpenChatWithDraft', (global) => { return { ...global, - openChatWithText: undefined, + requestedDraft: undefined, }; }); @@ -1115,6 +1141,38 @@ addActionHandler('toggleJoinRequest', async (global, actions, payload) => { await callApi('toggleJoinRequest', chat, isEnabled); }); +addActionHandler('processAttachBotParameters', async (global, actions, payload) => { + const { username, filter, startParam } = payload; + const bot = await getAttachBotOrNotify(global, username); + if (!bot) return; + + global = getGlobal(); + const { attachMenu: { bots } } = global; + if (!bots[bot.id]) { + setGlobal({ + ...global, + requestedAttachBotInstall: { + botId: bot.id, + onConfirm: { + action: 'requestAttachBotInChat', + payload: { + botId: bot.id, + filter, + startParam, + }, + }, + }, + }); + return; + } + + getActions().requestAttachBotInChat({ + botId: bot.id, + filter, + startParam, + }); +}); + async function loadChats( listType: 'active' | 'archived', offsetId?: string, offsetDate?: number, shouldReplace = false, ) { @@ -1504,6 +1562,22 @@ export async function fetchChatByPhoneNumber(phoneNumber: string) { return chat; } +async function getAttachBotOrNotify(global: GlobalState, username: string) { + const chat = await fetchChatByUsername(username); + if (!chat) return undefined; + + const user = selectUser(global, chat.id); + if (!user) return undefined; + + const isBot = isUserBot(user); + if (!isBot || !user.isAttachBot) { + getActions().showNotification({ message: langProvider.getTranslation('WebApp.AddToAttachmentUnavailableError') }); + + return undefined; + } + return user; +} + async function openChatByUsername( actions: GlobalActions, username: string, @@ -1512,29 +1586,16 @@ async function openChatByUsername( startAttach?: string | boolean, attach?: string, ) { - let global = getGlobal(); + const global = getGlobal(); const currentChat = selectCurrentChat(global); // Attach in the current chat if (startAttach && !attach) { - const chat = await fetchChatByUsername(username); - if (!chat) return; + const user = await getAttachBotOrNotify(global, username); - global = getGlobal(); + if (!currentChat || !user) return; - const user = selectUser(global, chat.id); - if (!user) return; - - const isBot = isUserBot(user); - if (!isBot || !user.isAttachMenuBot) { - actions.showNotification({ message: langProvider.getTranslation('WebApp.AddToAttachmentUnavailableError') }); - - return; - } - - if (!currentChat) return; - - actions.callAttachMenuBot({ + actions.callAttachBot({ botId: user.id, chatId: currentChat.id, ...(typeof startAttach === 'string' && { startParam: startAttach }), @@ -1582,12 +1643,12 @@ async function openAttachMenuFromLink( const botChat = await fetchChatByUsername(attach); if (!botChat) return; const botUser = selectUser(getGlobal(), botChat.id); - if (!botUser || !botUser.isAttachMenuBot) { + if (!botUser || !botUser.isAttachBot) { actions.showNotification({ message: langProvider.getTranslation('WebApp.AddToAttachmentUnavailableError') }); return; } - actions.callAttachMenuBot({ + actions.callAttachBot({ botId: botUser.id, chatId, ...(typeof startAttach === 'string' && { startParam: startAttach }), diff --git a/src/global/actions/ui/chats.ts b/src/global/actions/ui/chats.ts index a29f21275..88e9e4f65 100644 --- a/src/global/actions/ui/chats.ts +++ b/src/global/actions/ui/chats.ts @@ -70,14 +70,16 @@ addActionHandler('openChatWithInfo', (global, actions, payload) => { actions.openChat(payload); }); -addActionHandler('openChatWithText', (global, actions, payload) => { +addActionHandler('openChatWithDraft', (global, actions, payload) => { const { chatId, text } = payload; - actions.openChat({ id: chatId }); + if (chatId) { + actions.openChat({ id: chatId }); + } return { ...global, - openChatWithText: { + requestedDraft: { chatId, text, }, diff --git a/src/global/selectors/chats.ts b/src/global/selectors/chats.ts index c97a3d122..9d16e1e59 100644 --- a/src/global/selectors/chats.ts +++ b/src/global/selectors/chats.ts @@ -1,4 +1,4 @@ -import type { ApiAttachMenuPeerType, ApiChat } from '../../api/types'; +import type { ApiChatType, ApiChat } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types'; import type { GlobalState } from '../types'; @@ -74,25 +74,25 @@ export function selectIsTrustedBot(global: GlobalState, botId: string) { return bot && (bot.isVerified || global.trustedBotIds.includes(botId)); } -export function selectAttachMenuPeerType(global: GlobalState, chatId: string) : ApiAttachMenuPeerType | undefined { +export function selectChatType(global: GlobalState, chatId: string) : ApiChatType | undefined { const chat = selectChat(global, chatId); if (!chat) return undefined; const bot = selectBot(global, chatId); if (bot) { - return 'bot'; + return 'bots'; } const user = selectChatUser(global, chat); if (user) { - return 'private'; + return 'users'; } if (isChatChannel(chat)) { - return 'channel'; + return 'channels'; } - return 'chat'; + return 'chats'; } export function selectIsChatBotNotStarted(global: GlobalState, chatId: string) { @@ -194,8 +194,18 @@ export function selectSendAs(global: GlobalState, chatId: string) { } export function selectRequestedText(global: GlobalState, chatId: string) { - if (global.openChatWithText?.chatId === chatId) { - return global.openChatWithText.text; + if (global.requestedDraft?.chatId === chatId) { + return global.requestedDraft.text; } return undefined; } + +export function filterChatIdsByType(global: GlobalState, chatIds: string[], filter: readonly ApiChatType[]) { + return chatIds.filter((id) => { + const type = selectChatType(global, id); + if (!type) { + return false; + } + return filter.includes(type); + }); +} diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 675dc1eed..a2d5ca95d 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -673,8 +673,8 @@ export function selectIsPollResultsOpen(global: GlobalState) { } export function selectIsForwardModalOpen(global: GlobalState) { - const { forwardMessages, switchBotInline } = global; - return Boolean(switchBotInline || forwardMessages.isModalShown); + const { forwardMessages } = global; + return Boolean(forwardMessages.isModalShown); } export function selectCommonBoxChatId(global: GlobalState, messageId: number) { diff --git a/src/global/types.ts b/src/global/types.ts index 10f396653..e19896b63 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -35,7 +35,7 @@ import type { ApiPhoto, ApiKeyboardButton, ApiThemeParameters, - ApiAttachMenuBot, + ApiAttachBot, ApiPhoneCall, ApiWebSession, ApiPremiumPromo, @@ -43,6 +43,7 @@ import type { ApiInputInvoice, ApiInvoice, ApiStickerSetInfo, + ApiChatType, } from '../api/types'; import type { FocusDirection, @@ -585,13 +586,8 @@ export type GlobalState = { messageId: number; }; - switchBotInline?: { - query: string; - botUsername: string; - }; - - openChatWithText?: { - chatId: string; + requestedDraft?: { + chatId?: string; text: string; }; @@ -614,18 +610,25 @@ export type GlobalState = { type: 'game' | 'webApp'; onConfirm?: { action: keyof GlobalActions; - payload: any; // TODO add TS support + payload: any; // TODO Add TS support }; }; - botAttachRequest?: { + requestedAttachBotInstall?: { botId: string; - chatId: string; + onConfirm?: { + action: keyof GlobalActions; + payload: any; // TODO Add TS support + }; + }; + requestedAttachBotInChat?: { + botId: string; + filter: ApiChatType[]; startParam?: string; }; attachMenu: { hash?: string; - bots: Record; + bots: Record; }; confetti?: { @@ -722,11 +725,11 @@ export interface ActionPayloads { shouldReplaceHistory?: boolean; }; - openChatWithText: { - chatId: string; + openChatWithDraft: { + chatId?: string; text: string; }; - resetOpenChatWithText: never; + resetOpenChatWithDraft: never; toggleJoinToSend: { chatId: string; @@ -956,8 +959,6 @@ export interface ActionPayloads { isSamePeer?: boolean; }; - resetSwitchBotInline: never; - openGame: { url: string; chatId: string; @@ -998,8 +999,20 @@ export interface ActionPayloads { botId: string; }; - closeBotAttachRequestModal: never; - confirmBotAttachRequest: never; + cancelAttachBotInstall: never; + confirmAttachBotInstall: never; + + processAttachBotParameters: { + username: string; + filter: ApiChatType[]; + startParam?: string; + }; + requestAttachBotInChat: { + botId: string; + filter: ApiChatType[]; + startParam?: string; + }; + cancelAttachBotInChat: never; sendWebViewData: { bot: ApiUser; @@ -1007,16 +1020,16 @@ export interface ActionPayloads { buttonText: string; }; - loadAttachMenuBots: { + loadAttachBots: { hash?: string; }; - toggleBotInAttachMenu: { + toggleAttachBot: { botId: string; isEnabled: boolean; }; - callAttachMenuBot: { + callAttachBot: { chatId: string; botId: string; isFromBotMenu?: boolean; diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts index 64da612d5..9f22a4271 100644 --- a/src/util/deeplink.ts +++ b/src/util/deeplink.ts @@ -1,4 +1,8 @@ import { getActions } from '../global'; + +import type { ApiChatType } from '../api/types'; + +import { API_CHAT_TYPES } from '../config'; import { IS_SAFARI } from './environment'; type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' | @@ -20,14 +24,13 @@ export const processDeepLink = (url: string) => { focusMessage, joinVoiceChatByLink, openInvoice, + processAttachBotParameters, + openChatWithDraft, } = getActions(); // Safari thinks the path in tg://path links is hostname for some reason const method = (IS_SAFARI ? hostname : pathname).replace(/^\/\//, '') as DeepLinkMethod; - const params: Record = {}; - searchParams.forEach((value, key) => { - params[key] = value; - }); + const params = Object.fromEntries(searchParams); switch (method) { case 'resolve': { @@ -36,9 +39,16 @@ export const processDeepLink = (url: string) => { } = params; const startAttach = params.hasOwnProperty('startattach') && !startattach ? true : startattach; + const choose = parseChooseParameter(params.choose); if (domain !== 'telegrampassport') { - if (params.hasOwnProperty('voicechat') || params.hasOwnProperty('livestream')) { + if (startAttach && choose) { + processAttachBotParameters({ + username: domain, + filter: choose, + ...(typeof startAttach === 'string' && { startParam: startAttach }), + }); + } else if (params.hasOwnProperty('voicechat') || params.hasOwnProperty('livestream')) { joinVoiceChatByLink({ username: domain, inviteHash: voicechat || livestream, @@ -93,8 +103,10 @@ export const processDeepLink = (url: string) => { break; } case 'share': - case 'msg': { - // const { url, text } = params; + case 'msg': + case 'msg_url': { + const { url: urlParam, text } = params; + openChatWithDraft({ text: formatShareText(urlParam, text) }); break; } case 'login': { @@ -113,3 +125,13 @@ export const processDeepLink = (url: string) => { break; } }; + +export function parseChooseParameter(choose?: string) { + if (!choose) return undefined; + const types = choose.toLowerCase().split(' '); + return types.filter((type): type is ApiChatType => API_CHAT_TYPES.includes(type as ApiChatType)); +} + +export function formatShareText(url?: string, text?: string) { + return [url, text].filter(Boolean).join('\n'); +}