Chat Picker: Support web bot and share deep links, refactoring (#2037)

This commit is contained in:
Alexander Zinchuk 2022-09-24 01:40:29 +02:00
parent d896bb507d
commit 45a42f0d6c
38 changed files with 792 additions and 459 deletions

View File

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

View File

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

View File

@ -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,
}: {

View File

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

View File

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

View File

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

View File

@ -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<HTMLInputElement>;
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<OwnProps> = ({
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<HTMLInputElement>(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<HTMLDivElement>(null);
const handleFilterChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onFilterChange(e.currentTarget.value);
}, [onFilterChange]);
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
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<OwnProps> = ({
<i className="icon-close" />
</Button>
<InputText
ref={filterRef}
value={filter}
onChange={handleFilterChange}
ref={searchRef}
value={search}
onChange={handleSearchChange}
onKeyDown={handleKeyDown}
placeholder={filterPlaceholder}
placeholder={searchPlaceholder}
/>
</div>
);

View File

@ -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<string, ApiChat>;
activeListIds?: string[];
archivedListIds?: string[];
pinnedIds?: string[];
contactIds?: string[];
};
const RecipientPicker: FC<OwnProps & StateProps> = ({
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 (
<ChatOrUserPicker
isOpen={isOpen}
chatOrUserIds={renderingIds}
searchPlaceholder={searchPlaceholder}
search={search}
onSearchChange={setSearch}
loadMore={loadMore}
onSelectChatOrUser={onSelectRecipient}
onClose={onClose}
onCloseAnimationEnd={onCloseAnimationEnd}
/>
);
};
export default memo(withGlobal<OwnProps>(
(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));

View File

@ -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<OwnProps & StateProps> = ({
} = getActions();
const lang = useLang();
const [filter, setFilter] = useState('');
// eslint-disable-next-line no-null/no-null
const filterRef = useRef<HTMLInputElement>(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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
<ChatOrUserPicker
isOpen={isOpen}
chatOrUserIds={filteredContactIds}
filterRef={filterRef}
filterPlaceholder={lang('BlockedUsers.BlockUser')}
filter={filter}
onFilterChange={setFilter}
searchPlaceholder={lang('BlockedUsers.BlockUser')}
search={search}
onSearchChange={setSearch}
onSelectChatOrUser={handleRemoveUser}
onClose={onClose}
/>

View File

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

View File

@ -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<OwnProps> = ({
const AttachBotInstallModal: FC<OwnProps> = ({
bot,
}) => {
const { closeBotAttachRequestModal, confirmBotAttachRequest } = getActions();
const { cancelAttachBotInstall, confirmAttachBotInstall } = getActions();
const lang = useLang();
@ -24,12 +24,12 @@ const BotAttachModal: FC<OwnProps> = ({
return (
<ConfirmDialog
isOpen={Boolean(bot)}
onClose={closeBotAttachRequestModal}
confirmHandler={confirmBotAttachRequest}
onClose={cancelAttachBotInstall}
confirmHandler={confirmAttachBotInstall}
title={name}
textParts={lang('WebApp.AddToAttachmentText', name)}
/>
);
};
export default BotAttachModal;
export default memo(AttachBotInstallModal);

View File

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

View File

@ -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<OwnProps> = ({
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 (
<RecipientPicker
isOpen={isOpen}
searchPlaceholder={lang('Search')}
filter={filter}
onSelectRecipient={handlePeerRecipient}
onClose={cancelAttachBotInChat}
onCloseAnimationEnd={unmarkIsShown}
/>
);
};
export default memo(AttachBotRecipientPicker);

View File

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

View File

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

View File

@ -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<OwnProps> = ({
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 (
<RecipientPicker
isOpen={isOpen}
searchPlaceholder={lang('ForwardTo')}
onSelectRecipient={handleSelectRecipient}
onClose={handleClose}
onCloseAnimationEnd={unmarkIsShown}
/>
);
};
export default memo(DraftRecipientPicker);

View File

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

View File

@ -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<string, ApiChat>;
activeListIds?: string[];
archivedListIds?: string[];
pinnedIds?: string[];
contactIds?: string[];
currentUserId?: string;
switchBotInline?: GlobalState['switchBotInline'];
};
const ForwardPicker: FC<OwnProps & StateProps> = ({
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<HTMLInputElement>(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 (
<ChatOrUserPicker
currentUserId={currentUserId}
isOpen={isOpen}
chatOrUserIds={renderingChatAndContactIds}
filterRef={filterRef}
filterPlaceholder={lang('ForwardTo')}
filter={filter}
onFilterChange={setFilter}
onSelectChatOrUser={handleSelectUser}
onClose={handleClose}
onCloseAnimationEnd={unmarkIsShown}
/>
);
};
export default memo(withGlobal<OwnProps>(
(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));

View File

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

View File

@ -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<OwnProps> = ({
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 (
<RecipientPicker
isOpen={isOpen}
searchPlaceholder={lang('ForwardTo')}
onSelectRecipient={handleSelectRecipient}
onClose={handleClose}
onCloseAnimationEnd={unmarkIsShown}
/>
);
};
export default memo(ForwardRecipientPicker);

View File

@ -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<StateProps> = ({
isRatePhoneCallModalOpen,
botTrustRequest,
botTrustRequestBot,
botAttachRequestBot,
attachBotToInstall,
requestedAttachBotInChat,
requestedDraft,
webApp,
currentUser,
urlAuth,
@ -186,7 +192,7 @@ const Main: FC<StateProps> = ({
closeCustomEmojiSets,
checkVersionNotification,
loadAppConfig,
loadAttachMenuBots,
loadAttachBots,
loadContactList,
loadCustomEmojis,
closePaymentModal,
@ -219,14 +225,14 @@ const Main: FC<StateProps> = ({
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<StateProps> = ({
<MiddleColumn />
<RightColumn />
<MediaViewer isOpen={isMediaViewerOpen} />
<ForwardPicker isOpen={isForwardModalOpen} />
<ForwardRecipientPicker isOpen={isForwardModalOpen} />
<DraftRecipientPicker requestedDraft={requestedDraft} />
<Notifications isOpen={hasNotifications} />
<Dialogs isOpen={hasDialogs} />
{audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />}
@ -454,7 +461,8 @@ const Main: FC<StateProps> = ({
<UnreadCount isForAppBadge />
<RatePhoneCallModal isOpen={isRatePhoneCallModalOpen} />
<BotTrustModal bot={botTrustRequestBot} type={botTrustRequest?.type} />
<BotAttachModal bot={botAttachRequestBot} />
<AttachBotInstallModal bot={attachBotToInstall} />
<AttachBotRecipientPicker requestedAttachBotInChat={requestedAttachBotInChat} />
<MessageListHistoryHandler />
{isPremiumModalOpen && <PremiumMainModal isOpen={isPremiumModalOpen} />}
<PremiumLimitReachedModal limit={limitReached} />
@ -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));

View File

@ -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<OwnProps & StateProps> = ({
webApp,
chat,
bot,
attachMenuBot,
attachBot,
theme,
isPaymentModalOpen,
paymentStatus,
@ -87,7 +87,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
closeWebApp,
sendWebViewData,
prolongWebView,
toggleBotInAttachMenu,
toggleAttachBot,
openTelegramLink,
openChat,
openInvoice,
@ -279,11 +279,11 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
}, [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<OwnProps & StateProps> = ({
<MenuItem icon="bots" onClick={openBotChat}>{lang('BotWebViewOpenBot')}</MenuItem>
)}
<MenuItem icon="reload" onClick={handleRefreshClick}>{lang('WebApp.ReloadPage')}</MenuItem>
{bot?.isAttachMenuBot && (
{bot?.isAttachBot && (
<MenuItem
icon={attachMenuBot ? 'stop' : 'install'}
icon={attachBot ? 'stop' : 'install'}
onClick={handleToggleClick}
destructive={Boolean(attachMenuBot)}
destructive={Boolean(attachBot)}
>
{lang(attachMenuBot ? 'WebApp.RemoveBot' : 'WebApp.AddToAttachmentAdd')}
{lang(attachBot ? 'WebApp.RemoveBot' : 'WebApp.AddToAttachmentAdd')}
</MenuItem>
)}
{attachMenuBot?.hasSettings && (
{attachBot?.hasSettings && (
<MenuItem icon="settings" onClick={handleSettingsButtonClick}>
{lang('Settings')}
</MenuItem>
@ -371,7 +371,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
</div>
);
}, [
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<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(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,

View File

@ -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<OwnProps & StateProps> = ({
canReportMessages,
canDownloadMessages,
hasProtectedMessage,
isForwardModalOpen,
isAnyModalOpen,
selectedMessageIds,
}) => {
const {
@ -70,7 +70,7 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
})
: undefined;
}, [
isActive, isDeleteModalOpen, isReportModalOpen, openDeleteModal, exitMessageSelectMode, isForwardModalOpen,
isActive, isDeleteModalOpen, isReportModalOpen, openDeleteModal, exitMessageSelectMode, isAnyModalOpen,
canDeleteMessages,
]);
@ -183,6 +183,8 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
canDownloadMessages: canDownload,
selectedMessageIds,
hasProtectedMessage,
isForwardModalOpen,
isAnyModalOpen,
};
},
)(MessageSelectToolbar));

View File

@ -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<OwnProps> = ({
const AttachBotIcon: FC<OwnProps> = ({
icon, theme,
}) => {
const mediaData = useMedia(getDocumentMediaHash(icon), false, ApiMediaFormat.Text);
@ -48,4 +48,4 @@ const AttachmentMenuBotIcon: FC<OwnProps> = ({
);
};
export default memo(AttachmentMenuBotIcon);
export default memo(AttachBotIcon);

View File

@ -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<OwnProps> = ({
const AttachBotItem: FC<OwnProps> = ({
bot,
theme,
chatId,
onMenuOpened,
onMenuClosed,
}) => {
const { callAttachMenuBot, toggleBotInAttachMenu } = getActions();
const { callAttachBot, toggleAttachBot } = getActions();
const lang = useLang();
@ -59,19 +59,19 @@ const AttachmentMenuBotItem: FC<OwnProps> = ({
}, []);
const handleRemoveBot = useCallback(() => {
toggleBotInAttachMenu({
toggleAttachBot({
botId: bot.id,
isEnabled: false,
});
}, [bot.id, toggleBotInAttachMenu]);
}, [bot.id, toggleAttachBot]);
return (
<MenuItem
key={bot.id}
customIcon={icon && <AttachmentMenuBotIcon icon={icon} theme={theme} />}
customIcon={icon && <AttachBotIcon icon={icon} theme={theme} />}
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<OwnProps> = ({
);
};
export default memo(AttachmentMenuBotItem);
export default memo(AttachBotItem);

View File

@ -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<OwnProps> = ({
isButtonVisible,
canAttachMedia,
canAttachPolls,
attachMenuBots,
attachBots,
peerType,
isScheduled,
onFileSelect,
@ -85,14 +85,14 @@ const AttachMenu: FC<OwnProps> = ({
}, [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<OwnProps> = ({
)}
{canAttachMedia && !isScheduled && bots.map((bot) => (
<AttachmentMenuBotItem
<AttachBotItem
bot={bot}
chatId={chatId}
theme={theme}

View File

@ -54,7 +54,7 @@ import {
selectTheme,
selectCurrentMessageList,
selectIsCurrentUserPremium,
selectAttachMenuPeerType,
selectChatType,
} from '../../../global/selectors';
import {
getAllowedAttachmentOptions,
@ -169,7 +169,7 @@ type StateProps =
sendAsId?: string;
editingDraft?: ApiFormattedText;
requestedText?: string;
attachMenuBots: GlobalState['attachMenu']['bots'];
attachBots: GlobalState['attachMenu']['bots'];
attachMenuPeerType?: ApiAttachMenuPeerType;
theme: ISettings['theme'];
fileSizeLimit: number;
@ -249,7 +249,7 @@ const Composer: FC<OwnProps & StateProps> = ({
editingDraft,
requestedText,
botMenuButton,
attachMenuBots,
attachBots,
attachMenuPeerType,
theme,
}) => {
@ -268,8 +268,8 @@ const Composer: FC<OwnProps & StateProps> = ({
sendInlineBotResult,
loadSendAs,
loadFullChat,
resetOpenChatWithText,
callAttachMenuBot,
resetOpenChatWithDraft,
callAttachBot,
openLimitReachedModal,
showNotification,
} = getActions();
@ -674,10 +674,10 @@ const Composer: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
onFileSelect={handleFileSelect}
onPollCreate={openPollModal}
isScheduled={shouldSchedule}
attachMenuBots={attachMenuBots}
attachBots={attachBots}
peerType={attachMenuPeerType}
theme={theme}
/>
@ -1377,8 +1377,8 @@ export default memo(withGlobal<OwnProps>(
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'),

View File

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

View File

@ -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<OwnProps & StateProps> = ({
} = getActions();
const lang = useLang();
const [filter, setFilter] = useState('');
// eslint-disable-next-line no-null/no-null
const filterRef = useRef<HTMLInputElement>(null);
const [search, setSearch] = useState('');
const usersId = useMemo(() => {
const availableMemberIds = (chat.fullInfo?.members || [])
@ -49,8 +47,8 @@ const RemoveGroupUserModal: FC<OwnProps & StateProps> = ({
// 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<OwnProps & StateProps> = ({
<ChatOrUserPicker
isOpen={isOpen}
chatOrUserIds={usersId}
filterRef={filterRef}
filterPlaceholder={lang('ChannelBlockUser')}
filter={filter}
onFilterChange={setFilter}
searchPlaceholder={lang('ChannelBlockUser')}
search={search}
onSearchChange={setSearch}
loadMore={loadMoreMembers}
onSelectChatOrUser={handleRemoveUser}
onClose={onClose}

View File

@ -193,7 +193,7 @@ export const CONTENT_NOT_SUPPORTED = 'The message is not supported on this versi
// eslint-disable-next-line max-len
export const RE_LINK_TEMPLATE = '((ftp|https?):\\/\\/)?((www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,63})\\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)';
export const RE_MENTION_TEMPLATE = '(@[\\w\\d_-]+)';
export const RE_TG_LINK = /^tg:(\/\/)?([?=&\d\w_-]+)?/;
export const RE_TG_LINK = /^tg:(\/\/)?/;
export const RE_TME_LINK = /^(https?:\/\/)?([-a-zA-Z0-9@:%_+~#=]{1,32}\.)?t\.me/;
export const RE_TELEGRAM_LINK = /^(https?:\/\/)?telegram\.org\//;
export const TME_LINK_PREFIX = 'https://t.me/';
@ -201,6 +201,8 @@ export const TME_LINK_PREFIX = 'https://t.me/';
// eslint-disable-next-line max-len
export const COUNTRIES_WITH_12H_TIME_FORMAT = new Set(['AU', 'BD', 'CA', 'CO', 'EG', 'HN', 'IE', 'IN', 'JO', 'MX', 'MY', 'NI', 'NZ', 'PH', 'PK', 'SA', 'SV', 'US']);
export const API_CHAT_TYPES = ['bots', 'channels', 'chats', 'users'] as const;
// MTProto constants
export const SERVICE_NOTIFICATIONS_USER_ID = '777000';
export const REPLIES_USER_ID = '1271266957'; // TODO For Test connection ID must be equal to 708513

View File

@ -3,7 +3,7 @@ import {
} from '../../index';
import type {
ApiChat, ApiContact, ApiUrlAuthResult, ApiUser,
ApiChat, ApiChatType, ApiContact, ApiUrlAuthResult, ApiUser,
} from '../../../api/types';
import type { InlineBotSettings } from '../../../types';
@ -287,27 +287,11 @@ addActionHandler('switchBotInline', (global, actions, payload) => {
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,
};
});

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string, ApiAttachMenuBot>;
bots: Record<string, ApiAttachBot>;
};
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;

View File

@ -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<string, string> = {};
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');
}