Support chat links (#4469)

This commit is contained in:
Alexander Zinchuk 2024-04-19 13:38:21 +04:00
parent 3c4196679a
commit b9ed982170
14 changed files with 183 additions and 43 deletions

View File

@ -2,6 +2,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
import type { ApiPrivacyKey } from '../../../types';
import type {
ApiChatLink,
ApiConfig, ApiCountry, ApiLangString,
ApiPeerColors,
ApiSession, ApiTimezone, ApiUrlAuthResult, ApiWallpaper, ApiWebSession,
@ -11,7 +12,7 @@ import { buildCollectionByCallback, omit, pick } from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime';
import { addUserToLocalDb } from '../helpers';
import { omitVirtualClassFields } from './helpers';
import { buildApiDocument } from './messageContent';
import { buildApiDocument, buildMessageTextContent } from './messageContent';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
import { buildApiReaction } from './reactions';
import { buildApiUser } from './users';
@ -261,3 +262,11 @@ export function buildApiTimezone(timezone: GramJs.TypeTimezone): ApiTimezone {
utcOffset,
};
}
export function buildApiChatLink(data: GramJs.account.ResolvedBusinessChatLinks): ApiChatLink {
const chatId = getApiChatIdFromMtpPeer(data.peer);
return {
chatId,
text: buildMessageTextContent(data.message, data.entities),
};
}

View File

@ -5,6 +5,9 @@ import type {
ApiPeer, ApiPhoto, ApiReportReason,
} from '../../types';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildApiChatLink } from '../apiBuilders/misc';
import { buildApiUser } from '../apiBuilders/users';
import { buildInputPeer, buildInputPhoto, buildInputReportReason } from '../gramjsBuilders';
import { invokeRequest } from './client';
@ -71,3 +74,23 @@ export async function changeSessionTtl({
return result;
}
export async function resolveBusinessChatLink({ slug } : { slug: string }) {
const result = await invokeRequest(new GramJs.account.ResolveBusinessChatLink({
slug,
}), {
shouldIgnoreErrors: true,
});
if (!result) return undefined;
const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const chatLink = buildApiChatLink(result);
return {
users,
chats,
chatLink,
};
}

View File

@ -3,9 +3,7 @@ export {
setForceHttpTransport, setShouldDebugExportedSenders, setAllowHttpTransport, requestChannelDifference,
} from './client';
export {
reportPeer, reportProfilePhoto, changeSessionSettings, changeSessionTtl,
} from './account';
export * from './account';
export {
provideAuthPhoneNumber, provideAuthCode, provideAuthPassword, provideAuthRegistration, restartAuth, restartAuthWithQr,

View File

@ -1,7 +1,7 @@
import type { ThreadId } from '../../types';
import type { ApiBotCommand } from './bots';
import type {
ApiChatReactions, ApiPhoto, ApiStickerSet,
ApiChatReactions, ApiFormattedText, ApiPhoto, ApiStickerSet,
} from './messages';
import type { ApiChatInviteImporter } from './misc';
import type {
@ -284,3 +284,8 @@ export interface ApiMissingInvitedUser {
isRequiringPremiumToInvite?: boolean;
isRequiringPremiumToMessage?: boolean;
}
export interface ApiChatLink {
chatId: string;
text: ApiFormattedText;
}

View File

@ -76,8 +76,8 @@ import {
selectIsRightColumnShown,
selectNewestMessageWithBotKeyboardButtons,
selectPeerStory,
selectRequestedDraft,
selectRequestedDraftFiles,
selectRequestedDraftText,
selectScheduledIds,
selectTabState,
selectTheme,
@ -99,7 +99,6 @@ import { IS_IOS, IS_VOICE_RECORDING_SUPPORTED } from '../../util/windowEnvironme
import windowSize from '../../util/windowSize';
import applyIosAutoCapitalizationFix from '../middle/composer/helpers/applyIosAutoCapitalizationFix';
import buildAttachment, { prepareAttachmentsToSend } from '../middle/composer/helpers/buildAttachment';
import { escapeHtml } from '../middle/composer/helpers/cleanHtml';
import { buildCustomEmojiHtml } from '../middle/composer/helpers/customEmoji';
import { isSelectionInsideInput } from '../middle/composer/helpers/selection';
import { getPeerColorClass } from './helpers/peerColor';
@ -228,7 +227,7 @@ type StateProps =
sendAsChat?: ApiChat;
sendAsId?: string;
editingDraft?: ApiFormattedText;
requestedDraftText?: string;
requestedDraft?: ApiFormattedText;
requestedDraftFiles?: File[];
attachBots: GlobalState['attachMenu']['bots'];
attachMenuPeerType?: ApiAttachMenuPeerType;
@ -332,7 +331,7 @@ const Composer: FC<OwnProps & StateProps> = ({
sendAsChat,
sendAsId,
editingDraft,
requestedDraftText,
requestedDraft,
requestedDraftFiles,
botMenuButton,
attachBots,
@ -691,7 +690,7 @@ const Composer: FC<OwnProps & StateProps> = ({
getHtml,
setHtml,
editedMessage: editingMessage,
isDisabled: isInStoryViewer,
isDisabled: isInStoryViewer || Boolean(requestedDraft),
});
const resetComposer = useLastCallback((shouldPreserveInput = false) => {
@ -1080,8 +1079,8 @@ const Composer: FC<OwnProps & StateProps> = ({
}, [contentToBeScheduled, currentMessageList, handleMessageSchedule, requestCalendar]);
useEffect(() => {
if (requestedDraftText) {
setHtml(escapeHtml(requestedDraftText));
if (requestedDraft) {
insertFormattedTextAndUpdateCursor(requestedDraft);
resetOpenChatWithDraft();
requestNextMutation(() => {
@ -1089,7 +1088,7 @@ const Composer: FC<OwnProps & StateProps> = ({
focusEditableElement(messageInput, true);
});
}
}, [editableInputId, requestedDraftText, resetOpenChatWithDraft, setHtml]);
}, [editableInputId, requestedDraft, resetOpenChatWithDraft, setHtml]);
useEffect(() => {
if (requestedDraftFiles?.length) {
@ -1987,7 +1986,7 @@ export default memo(withGlobal<OwnProps>(
);
const sendAsUser = sendAsId ? selectUser(global, sendAsId) : undefined;
const sendAsChat = !sendAsUser && sendAsId ? selectChat(global, sendAsId) : undefined;
const requestedDraftText = selectRequestedDraftText(global, chatId);
const requestedDraft = selectRequestedDraft(global, chatId);
const requestedDraftFiles = selectRequestedDraftFiles(global, chatId);
const tabState = selectTabState(global);
@ -2064,7 +2063,7 @@ export default memo(withGlobal<OwnProps>(
sendAsChat,
sendAsId,
editingDraft,
requestedDraftText,
requestedDraft,
requestedDraftFiles,
attachBots: global.attachMenu.bots,
attachMenuPeerType: selectChatType(global, chatId),

View File

@ -53,7 +53,7 @@ const InviteLink: FC<OwnProps> = ({
});
const handleShare = useLastCallback(() => {
openChatWithDraft({ text: link });
openChatWithDraft({ text: { text: link } });
});
const PrimaryLinkMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {

View File

@ -341,7 +341,9 @@ addActionHandler('switchBotInline', (global, actions, payload): ActionReturnType
}
actions.openChatWithDraft({
text: `@${botSender.usernames![0].username} ${query}`,
text: {
text: `@${botSender.usernames![0].username} ${query}`,
},
chatId: isSamePeer ? chat.id : undefined,
filter,
tabId,

View File

@ -1235,7 +1235,7 @@ addActionHandler('openChatByInvite', async (global, actions, payload): Promise<v
addActionHandler('openChatByPhoneNumber', async (global, actions, payload): Promise<void> => {
const {
phoneNumber, startAttach, attach, tabId = getCurrentTabId(),
phoneNumber, startAttach, attach, text, tabId = getCurrentTabId(),
} = payload!;
// Open temporary empty chat to make the click response feel faster
@ -1251,7 +1251,11 @@ addActionHandler('openChatByPhoneNumber', async (global, actions, payload): Prom
return;
}
actions.openChat({ id: chat.id, tabId });
if (text) {
actions.openChatWithDraft({ chatId: chat.id, text: { text }, tabId });
} else {
actions.openChat({ id: chat.id, tabId });
}
if (attach) {
global = getGlobal();
@ -1317,6 +1321,7 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
phoneNumber: part1.substr(1, part1.length - 1),
startAttach: params.startattach,
attach: params.attach,
text: params.text,
tabId,
});
return;
@ -1481,7 +1486,7 @@ addActionHandler('acceptInviteConfirmation', async (global, actions, payload): P
addActionHandler('openChatByUsername', async (global, actions, payload): Promise<void> => {
const {
username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, startApp,
username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, startApp, text,
tabId = getCurrentTabId(),
} = payload!;
@ -1499,7 +1504,15 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
}
if (!isWebApp) {
await openChatByUsername(
global, actions, username, threadId, messageId, startParam, startAttach, attach, tabId,
global, actions, {
username,
threadId,
channelPostId: messageId,
startParam,
startAttach,
attach,
text,
}, tabId,
);
return;
}
@ -2643,6 +2656,31 @@ addActionHandler('toggleChannelRecommendations', (global, actions, payload): Act
setGlobal(global);
});
addActionHandler('resolveBusinessChatLink', async (global, actions, payload): Promise<void> => {
const { slug, tabId = getCurrentTabId() } = payload;
const result = await callApi('resolveBusinessChatLink', { slug });
if (!result) {
actions.showNotification({
message: langProvider.translate('BusinessLink.ErrorExpired'),
tabId,
});
return;
}
const { users, chats, chatLink } = result;
global = getGlobal();
global = addUsers(global, buildCollectionByKey(users, 'id'));
global = addChats(global, buildCollectionByKey(chats, 'id'));
setGlobal(global);
actions.openChatWithDraft({
chatId: chatLink.chatId,
text: chatLink.text,
tabId,
});
});
async function loadChats(
listType: ChatListType,
offsetId?: string,
@ -2944,14 +2982,20 @@ async function getAttachBotOrNotify<T extends GlobalState>(
async function openChatByUsername<T extends GlobalState>(
global: T,
actions: RequiredGlobalActions,
username: string,
threadId?: ThreadId,
channelPostId?: number,
startParam?: string,
startAttach?: string,
attach?: string,
params: {
username: string;
threadId?: ThreadId;
channelPostId?: number;
startParam?: string;
startAttach?: string;
attach?: string;
text?: string;
},
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const {
username, threadId, channelPostId, startParam, startAttach, attach, text,
} = params;
global = getGlobal();
const currentChat = selectCurrentChat(global, tabId);
@ -3004,6 +3048,10 @@ async function openChatByUsername<T extends GlobalState>(
global = getGlobal();
openAttachMenuFromLink(global, actions, chat.id, attach, startAttach, tabId);
}
if (text) {
actions.openChatWithDraft({ chatId: chat.id, text: { text }, tabId });
}
}
async function openAttachMenuFromLink<T extends GlobalState>(

View File

@ -221,7 +221,7 @@ export function selectSendAs<T extends GlobalState>(global: T, chatId: string) {
return selectUser(global, id) || selectChat(global, id);
}
export function selectRequestedDraftText<T extends GlobalState>(
export function selectRequestedDraft<T extends GlobalState>(
global: T, chatId: string,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {

View File

@ -559,7 +559,7 @@ export type TabState = {
requestedDraft?: {
chatId?: string;
text: string;
text: ApiFormattedText;
files?: File[];
filter?: ApiChatType[];
};
@ -1323,6 +1323,7 @@ export interface ActionPayloads {
phoneNumber: string;
startAttach?: string | boolean;
attach?: string;
text?: string;
} & WithTabId;
openChatByInvite: {
hash: string;
@ -1500,6 +1501,9 @@ export interface ActionPayloads {
openTelegramLink: {
url: string;
} & WithTabId;
resolveBusinessChatLink: {
slug: string;
} & WithTabId;
openChatByUsername: {
username: string;
threadId?: ThreadId;
@ -1509,6 +1513,7 @@ export interface ActionPayloads {
startAttach?: string;
attach?: string;
startApp?: string;
text?: string;
originalParts?: string[];
} & WithTabId;
processBoostParameters: {
@ -1967,7 +1972,7 @@ export interface ActionPayloads {
openChatWithDraft: {
chatId?: string;
threadId?: ThreadId;
text: string;
text: ApiFormattedText;
files?: File[];
filter?: ApiChatType[];
} & WithTabId;

View File

@ -1344,6 +1344,7 @@ account.updateEmojiStatus#fbd3de6b emoji_status:EmojiStatus = Bool;
account.getRecentEmojiStatuses#f578105 hash:long = account.EmojiStatuses;
account.reorderUsernames#ef500eab order:Vector<string> = Bool;
account.toggleUsername#58d6b376 username:string active:Bool = Bool;
account.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessChatLinks;
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#b60f5918 id:InputUser = users.UserFull;
contacts.getContacts#5dd69e12 hash:long = contacts.Contacts;

View File

@ -58,6 +58,7 @@
"account.getRecentEmojiStatuses",
"account.reorderUsernames",
"account.toggleUsername",
"account.resolveBusinessChatLink",
"users.getUsers",
"users.getFullUser",
"contacts.getContacts",

View File

@ -6,7 +6,7 @@ import { isUsernameValid } from './username';
export type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' |
'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' |
'invoice' | 'addlist' | 'boost' | 'giftcode';
'invoice' | 'addlist' | 'boost' | 'giftcode' | 'message';
interface PublicMessageLink {
type: 'publicMessageLink';
@ -59,7 +59,13 @@ interface TelegramPassportLink {
interface PublicUsernameOrBotLink {
type: 'publicUsernameOrBotLink';
username: string;
parameter?: string;
start?: string;
text?: string;
}
interface BusinessChatLink {
type: 'businessChatLink';
slug: string;
}
type DeepLink =
@ -69,7 +75,8 @@ type DeepLink =
PrivateMessageLink |
ShareLink |
ChatFolderLink |
PublicUsernameOrBotLink;
PublicUsernameOrBotLink |
BusinessChatLink;
type BuilderParams<T extends DeepLink> = Record<keyof Omit<T, 'type'>, string | undefined>;
type BuilderReturnType<T extends DeepLink> = T | undefined;
@ -173,8 +180,11 @@ function parseTgLink(url: URL) {
case 'publicUsernameOrBotLink':
return buildPublicUsernameOrBotLink({
username: queryParams.domain,
parameter: queryParams.start,
start: queryParams.start,
text: queryParams.text,
});
case 'businessChatLink':
return buildBusinessChatLink({ slug: queryParams.slug });
default:
break;
}
@ -254,8 +264,11 @@ function parseHttpLink(url: URL) {
case 'publicUsernameOrBotLink':
return buildPublicUsernameOrBotLink({
username: pathParams[0],
parameter: queryParams.start,
start: queryParams.start,
text: queryParams.text,
});
case 'businessChatLink':
return buildBusinessChatLink({ slug: pathParams[1] });
default:
break;
}
@ -285,6 +298,9 @@ function getHttpDeepLinkType(
if (isUsernameValid(pathParams[0]) && isNumber(pathParams[1])) {
return 'publicMessageLink';
}
if (method === 'm') {
return 'businessChatLink';
}
} else if (len === 3) {
if (method === 'c' && pathParams.slice(1).every(isNumber)) {
return 'privateMessageLink';
@ -337,6 +353,8 @@ function getTgDeepLinkType(
return 'loginCodeLink';
case 'passport':
return 'telegramPassportLink';
case 'message':
return 'businessChatLink';
default:
break;
}
@ -467,7 +485,8 @@ function buildPublicUsernameOrBotLink(
): BuilderReturnType<PublicUsernameOrBotLink> {
const {
username,
parameter,
start,
text,
} = params;
if (!username) {
return undefined;
@ -478,7 +497,23 @@ function buildPublicUsernameOrBotLink(
return {
type: 'publicUsernameOrBotLink',
username,
parameter,
start,
text,
};
}
function buildBusinessChatLink(params: BuilderParams<BusinessChatLink>): BuilderReturnType<BusinessChatLink> {
const {
slug,
} = params;
if (!slug) {
return undefined;
}
return {
type: 'businessChatLink',
slug,
};
}

View File

@ -1,6 +1,6 @@
import { getActions } from '../global';
import type { ApiChatType } from '../api/types';
import type { ApiChatType, ApiFormattedText } from '../api/types';
import type { DeepLinkMethod, PrivateMessageLink } from './deepLinkParser';
import { API_CHAT_TYPES, RE_TG_LINK } from '../config';
@ -20,7 +20,13 @@ export const processDeepLink = (url: string): boolean => {
case 'publicUsernameOrBotLink':
actions.openChatByUsername({
username: parsedLink.username,
startParam: parsedLink.parameter,
startParam: parsedLink.start,
text: parsedLink.text,
});
return true;
case 'businessChatLink':
actions.resolveBusinessChatLink({
slug: parsedLink.slug,
});
return true;
default:
@ -61,7 +67,7 @@ export const processDeepLink = (url: string): boolean => {
case 'resolve': {
const {
domain, phone, post, comment, voicechat, livestream, start, startattach, attach, thread, topic,
appname, startapp, story,
appname, startapp, story, text,
} = params;
const hasStartAttach = params.hasOwnProperty('startattach');
@ -76,6 +82,7 @@ export const processDeepLink = (url: string): boolean => {
username: domain,
startApp: startapp,
originalParts: [domain, appname],
text,
});
} else if ((hasStartAttach && choose) || (!appname && hasStartApp)) {
processAttachBotParameters({
@ -91,7 +98,12 @@ export const processDeepLink = (url: string): boolean => {
} else if (hasBoost) {
processBoostParameters({ usernameOrId: domain });
} else if (phone) {
openChatByPhoneNumber({ phoneNumber: phone, startAttach: startattach, attach });
openChatByPhoneNumber({
phoneNumber: phone,
startAttach: startattach,
attach,
text,
});
} else if (story) {
openStoryViewerByUsername({ username: domain, storyId: Number(story) });
} else {
@ -180,8 +192,10 @@ export function parseChooseParameter(choose?: string) {
return types.filter((type): type is ApiChatType => API_CHAT_TYPES.includes(type as ApiChatType));
}
export function formatShareText(url?: string, text?: string, title?: string): string {
return [url, title, text].filter(Boolean).join('\n');
export function formatShareText(url?: string, text?: string, title?: string): ApiFormattedText {
return {
text: [url, title, text].filter(Boolean).join('\n'),
};
}
function handlePrivateMessageLink(link: PrivateMessageLink, actions: ReturnType<typeof getActions>) {