From b39ae6f0a13174ed59f450ea1a95d49a3d36d1b5 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 6 Jun 2022 01:44:25 +0400 Subject: [PATCH] Implement Seamless Login (#1865) --- src/api/gramjs/apiBuilders/appConfig.ts | 8 +- src/api/gramjs/apiBuilders/messages.ts | 9 + src/api/gramjs/apiBuilders/misc.ts | 45 ++- src/api/gramjs/methods/bots.ts | 104 +++++- src/api/gramjs/methods/index.ts | 4 +- src/api/gramjs/methods/settings.ts | 28 +- src/api/gramjs/provider.ts | 2 + src/api/types/messages.ts | 8 + src/api/types/misc.ts | 34 ++ src/api/types/statistics.ts | 2 +- src/assets/fonts/icomoon.woff | Bin 44092 -> 44924 bytes src/assets/fonts/icomoon.woff2 | Bin 20660 -> 21236 bytes src/bundles/extra.ts | 1 + .../calls/phone/RatePhoneCallModal.tsx | 13 +- src/components/common/SafeLink.tsx | 25 +- src/components/left/LeftColumn.tsx | 1 + src/components/left/main/LeftMainHeader.tsx | 2 +- src/components/left/settings/Settings.scss | 1 + src/components/left/settings/Settings.tsx | 10 +- .../left/settings/SettingsActiveSessions.scss | 2 +- .../left/settings/SettingsActiveSessions.tsx | 1 + .../SettingsActiveWebsite.module.scss | 48 +++ .../left/settings/SettingsActiveWebsite.tsx | 103 ++++++ .../SettingsActiveWebsites.module.scss | 21 ++ .../left/settings/SettingsActiveWebsites.tsx | 166 +++++++++ .../left/settings/SettingsHeader.tsx | 2 + src/components/left/settings/SettingsMain.tsx | 5 +- .../left/settings/SettingsPrivacy.tsx | 18 +- src/components/main/Main.tsx | 12 +- src/components/main/SafeLinkModal.tsx | 2 +- src/components/main/UrlAuthModal.async.tsx | 17 + src/components/main/UrlAuthModal.module.scss | 3 + src/components/main/UrlAuthModal.tsx | 114 ++++++ src/components/main/WebAppModal.tsx | 10 +- .../middle/message/InlineButtons.scss | 5 +- .../middle/message/InlineButtons.tsx | 31 +- src/components/middle/message/Location.tsx | 2 +- src/components/ui/Checkbox.tsx | 9 +- src/global/actions/api/accounts.ts | 53 +++ src/global/actions/api/bots.ts | 142 +++++++- src/global/actions/api/messages.ts | 37 ++ src/global/cache.ts | 7 + src/global/initialState.ts | 5 + src/global/types.ts | 98 ++++- src/lib/gramjs/tl/apiTl.js | 5 + src/lib/gramjs/tl/static/api.json | 5 + src/styles/Telegram T.json | 335 ++++++++++-------- src/styles/icons.scss | 3 + src/types/index.ts | 1 + 49 files changed, 1317 insertions(+), 242 deletions(-) create mode 100644 src/components/left/settings/SettingsActiveWebsite.module.scss create mode 100644 src/components/left/settings/SettingsActiveWebsite.tsx create mode 100644 src/components/left/settings/SettingsActiveWebsites.module.scss create mode 100644 src/components/left/settings/SettingsActiveWebsites.tsx create mode 100644 src/components/main/UrlAuthModal.async.tsx create mode 100644 src/components/main/UrlAuthModal.module.scss create mode 100644 src/components/main/UrlAuthModal.tsx diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts index 514875293..8f9c21c0e 100644 --- a/src/api/gramjs/apiBuilders/appConfig.ts +++ b/src/api/gramjs/apiBuilders/appConfig.ts @@ -17,6 +17,9 @@ type GramJsAppConfig = { reactions_uniq_max: number; chat_read_mark_size_threshold: number; chat_read_mark_expire_period: number; + autologin_domains: string[]; + autologin_token: string; + url_auth_domains: string[]; }; function buildEmojiSounds(appConfig: GramJsAppConfig) { @@ -38,7 +41,7 @@ function buildEmojiSounds(appConfig: GramJsAppConfig) { }, {}) : {}; } -export function buildApiConfig(json: GramJs.TypeJSONValue): ApiAppConfig { +export function buildAppConfig(json: GramJs.TypeJSONValue): ApiAppConfig { const appConfig = buildJson(json) as GramJsAppConfig; return { @@ -46,5 +49,8 @@ export function buildApiConfig(json: GramJs.TypeJSONValue): ApiAppConfig { defaultReaction: appConfig.reactions_default, seenByMaxChatMembers: appConfig.chat_read_mark_size_threshold, seenByExpiresAt: appConfig.chat_read_mark_expire_period, + autologinDomains: appConfig.autologin_domains || [], + autologinToken: appConfig.autologin_token || '', + urlAuthDomains: appConfig.url_auth_domains || [], }; } diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 15eff055b..0037f710e 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -1104,6 +1104,15 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi }; } + if (button instanceof GramJs.KeyboardButtonUrlAuth) { + return { + type: 'urlAuth', + text, + url: button.url, + buttonId: button.buttonId, + }; + } + return { type: 'unsupported', text, diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index 35c48e6a6..2f7eac6d6 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -1,7 +1,7 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { - ApiCountry, ApiSession, ApiWallpaper, + ApiCountry, ApiSession, ApiUrlAuthResult, ApiWallpaper, ApiWebSession, } from '../../types'; import type { ApiPrivacySettings, ApiPrivacyKey, PrivacyVisibility } from '../../../types'; @@ -9,6 +9,8 @@ import { buildApiDocument } from './messages'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { pick } from '../../../util/iteratees'; import { getServerTime } from '../../../util/serverTime'; +import { buildApiUser } from './users'; +import { addUserToLocalDb } from '../helpers'; export function buildApiWallpaper(wallpaper: GramJs.TypeWallPaper): ApiWallpaper | undefined { if (wallpaper instanceof GramJs.WallPaperNoFile) { @@ -45,6 +47,16 @@ export function buildApiSession(session: GramJs.Authorization): ApiSession { }; } +export function buildApiWebSession(session: GramJs.WebAuthorization): ApiWebSession { + return { + hash: String(session.hash), + botId: buildApiPeerId(session.botId, 'user'), + ...pick(session, [ + 'platform', 'browser', 'dateCreated', 'dateActive', 'ip', 'region', 'domain', + ]), + }; +} + export function buildPrivacyKey(key: GramJs.TypePrivacyKey): ApiPrivacyKey | undefined { switch (key.className) { case 'PrivacyKeyPhoneNumber': @@ -176,3 +188,34 @@ export function buildJson(json: GramJs.TypeJSONValue): any { return acc; }, {}); } + +export function buildApiUrlAuthResult(result: GramJs.TypeUrlAuthResult): ApiUrlAuthResult | undefined { + if (result instanceof GramJs.UrlAuthResultRequest) { + const { bot, domain, requestWriteAccess } = result; + const user = buildApiUser(bot); + if (!user) return undefined; + + addUserToLocalDb(bot); + + return { + type: 'request', + domain, + shouldRequestWriteAccess: requestWriteAccess, + bot: user, + }; + } + + if (result instanceof GramJs.UrlAuthResultAccepted) { + return { + type: 'accepted', + url: result.url, + }; + } + + if (result instanceof GramJs.UrlAuthResultDefault) { + return { + type: 'default', + }; + } + return undefined; +} diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index f7b7f836f..11adfcbd6 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -1,7 +1,9 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; -import type { ApiChat, ApiThemeParameters, ApiUser } from '../../types'; +import type { + ApiChat, ApiThemeParameters, ApiUser, OnApiUpdate, +} from '../../types'; import localDb from '../localDb'; import { invokeRequest } from './client'; @@ -14,8 +16,12 @@ import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers'; import { omitVirtualClassFields } from '../apiBuilders/helpers'; import { buildCollectionByKey } from '../../../util/iteratees'; +import { buildApiUrlAuthResult } from '../apiBuilders/misc'; -export function init() { +let onUpdate: OnApiUpdate; + +export function init(_onUpdate: OnApiUpdate) { + onUpdate = _onUpdate; } export async function answerCallbackButton({ @@ -269,6 +275,100 @@ export function toggleBotInAttachMenu({ })); } +export async function requestBotUrlAuth({ + chat, buttonId, messageId, +}: { + chat: ApiChat; + buttonId: number; + messageId: number; +}) { + const result = await invokeRequest(new GramJs.messages.RequestUrlAuth({ + peer: buildInputPeer(chat.id, chat.accessHash), + buttonId, + msgId: messageId, + })); + + if (!result) return undefined; + + const authResult = buildApiUrlAuthResult(result); + if (authResult?.type === 'request') { + onUpdate({ + '@type': 'updateUser', + id: authResult.bot.id, + user: authResult.bot, + }); + } + return authResult; +} + +export async function acceptBotUrlAuth({ + chat, + messageId, + buttonId, + isWriteAllowed, +}: { + chat: ApiChat; + messageId: number; + buttonId: number; + isWriteAllowed?: boolean; +}) { + const result = await invokeRequest(new GramJs.messages.AcceptUrlAuth({ + peer: buildInputPeer(chat.id, chat.accessHash), + msgId: messageId, + buttonId, + writeAllowed: isWriteAllowed || undefined, + })); + + if (!result) return undefined; + + const authResult = buildApiUrlAuthResult(result); + if (authResult?.type === 'request') { + onUpdate({ + '@type': 'updateUser', + id: authResult.bot.id, + user: authResult.bot, + }); + } + return authResult; +} + +export async function requestLinkUrlAuth({ url }: { url: string }) { + const result = await invokeRequest(new GramJs.messages.RequestUrlAuth({ + url, + })); + + if (!result) return undefined; + + const authResult = buildApiUrlAuthResult(result); + if (authResult?.type === 'request') { + onUpdate({ + '@type': 'updateUser', + id: authResult.bot.id, + user: authResult.bot, + }); + } + return authResult; +} + +export async function acceptLinkUrlAuth({ url, isWriteAllowed }: { url: string; isWriteAllowed?: boolean }) { + const result = await invokeRequest(new GramJs.messages.AcceptUrlAuth({ + url, + writeAllowed: isWriteAllowed || undefined, + })); + + if (!result) return undefined; + + const authResult = buildApiUrlAuthResult(result); + if (authResult?.type === 'request') { + onUpdate({ + '@type': 'updateUser', + id: authResult.bot.id, + user: authResult.bot, + }); + } + return authResult; +} + function processInlineBotResult(queryId: string, results: GramJs.TypeBotInlineResult[]) { return results.map((result) => { if (result instanceof GramJs.BotInlineMediaResult) { diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 398623d99..115371ca6 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -54,6 +54,7 @@ export { updateProfile, checkUsername, updateUsername, fetchBlockedContacts, blockContact, unblockContact, updateProfilePhoto, uploadProfilePhoto, fetchWallpapers, uploadWallpaper, fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations, + fetchWebAuthorizations, terminateWebAuthorization, terminateAllWebAuthorizations, fetchNotificationExceptions, fetchNotificationSettings, updateContactSignUpNotification, updateNotificationSettings, fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, registerDevice, unregisterDevice, updateIsOnline, fetchContentSettings, updateContentSettings, fetchLangStrings, fetchCountryList, fetchAppConfig, @@ -66,6 +67,7 @@ export { export { answerCallbackButton, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, sendInlineBotResult, startBot, requestWebView, requestSimpleWebView, sendWebViewData, prolongWebView, loadAttachMenuBots, toggleBotInAttachMenu, + requestBotUrlAuth, requestLinkUrlAuth, acceptBotUrlAuth, acceptLinkUrlAuth, } from './bots'; export { @@ -86,7 +88,7 @@ export { export { fetchChannelStatistics, fetchGroupStatistics, fetchMessageStatistics, - fetchMessagePublicForwards, fetchStatisticsAsyncGraph, + fetchMessagePublicForwards, fetchStatisticsAsyncGraph, } from './statistics'; export { diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index f8200280c..38e1cb1a5 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -19,20 +19,21 @@ import { buildApiNotifyException, buildApiSession, buildApiWallpaper, + buildApiWebSession, buildPrivacyRules, } from '../apiBuilders/misc'; import { buildApiUser } from '../apiBuilders/users'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; +import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; +import { buildAppConfig } from '../apiBuilders/appConfig'; +import { omitVirtualClassFields } from '../apiBuilders/helpers'; import { buildInputEntity, buildInputPeer, buildInputPrivacyKey } from '../gramjsBuilders'; import { getClient, invokeRequest, uploadFile } from './client'; -import { omitVirtualClassFields } from '../apiBuilders/helpers'; import { buildCollectionByKey } from '../../../util/iteratees'; import { getServerTime } from '../../../util/serverTime'; -import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; -import localDb from '../localDb'; -import { buildApiConfig } from '../apiBuilders/appConfig'; import { addEntitiesWithPhotosToLocalDb } from '../helpers'; +import localDb from '../localDb'; const MAX_INT_32 = 2 ** 31 - 1; const BETA_LANG_CODES = ['ar', 'fa', 'id', 'ko', 'uz', 'en']; @@ -175,6 +176,23 @@ export function terminateAllAuthorizations() { return invokeRequest(new GramJs.auth.ResetAuthorizations()); } +export async function fetchWebAuthorizations() { + const result = await invokeRequest(new GramJs.account.GetWebAuthorizations()); + if (!result) { + return undefined; + } + + return buildCollectionByKey(result.authorizations.map(buildApiWebSession), 'hash'); +} + +export function terminateWebAuthorization(hash: string) { + return invokeRequest(new GramJs.account.ResetWebAuthorization({ hash: BigInt(hash) })); +} + +export function terminateAllWebAuthorizations() { + return invokeRequest(new GramJs.account.ResetWebAuthorizations()); +} + export async function fetchNotificationExceptions({ serverTimeOffset, }: { serverTimeOffset: number }) { @@ -451,7 +469,7 @@ export async function fetchAppConfig(): Promise { const result = await invokeRequest(new GramJs.help.GetAppConfig()); if (!result) return undefined; - return buildApiConfig(result); + return buildAppConfig(result); } function updateLocalDb( diff --git a/src/api/gramjs/provider.ts b/src/api/gramjs/provider.ts index 08cae2967..231714782 100644 --- a/src/api/gramjs/provider.ts +++ b/src/api/gramjs/provider.ts @@ -17,6 +17,7 @@ import { init as initClient } from './methods/client'; import { init as initStickers } from './methods/symbols'; import { init as initManagement } from './methods/management'; import { init as initTwoFaSettings } from './methods/twoFaSettings'; +import { init as initBots } from './methods/bots'; import { init as initCalls } from './methods/calls'; import { init as initPayments } from './methods/payments'; import * as methods from './methods'; @@ -34,6 +35,7 @@ export async function initApi(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArg initStickers(handleUpdate); initManagement(handleUpdate); initTwoFaSettings(handleUpdate); + initBots(handleUpdate); initCalls(handleUpdate); initPayments(handleUpdate); diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 921c0ffb5..05d759c4b 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -438,6 +438,13 @@ interface ApiKeyboardButtonUserProfile { userId: string; } +interface ApiKeyboardButtonUrlAuth { + type: 'urlAuth'; + text: string; + url: string; + buttonId: number; +} + export type ApiKeyboardButton = ( ApiKeyboardButtonSimple | ApiKeyboardButtonReceipt @@ -448,6 +455,7 @@ export type ApiKeyboardButton = ( | ApiKeyboardButtonUserProfile | ApiKeyboardButtonWebView | ApiKeyboardButtonSimpleWebView + | ApiKeyboardButtonUrlAuth ); export type ApiKeyboardButtons = ApiKeyboardButton[][]; diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 0edc5441c..345f7d4c4 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -1,4 +1,5 @@ import type { ApiDocument, ApiPhoto } from './messages'; +import type { ApiUser } from './users'; export interface ApiInitialArgs { userAgent: string; @@ -65,6 +66,18 @@ export interface ApiSession { areSecretChatsEnabled: boolean; } +export interface ApiWebSession { + hash: string; + botId: string; + domain: string; + browser: string; + platform: string; + dateCreated: number; + dateActive: number; + ip: string; + region: string; +} + export interface ApiSessionData { mainDcId: number; keys: Record; @@ -145,6 +158,9 @@ export interface ApiAppConfig { defaultReaction: string; seenByMaxChatMembers: number; seenByExpiresAt: number; + autologinDomains: string[]; + autologinToken: string; + urlAuthDomains: string[]; } export interface GramJsEmojiInteraction { @@ -158,3 +174,21 @@ export interface GramJsEmojiInteraction { export interface ApiEmojiInteraction { timestamps: number[]; } + +type ApiUrlAuthResultRequest = { + type: 'request'; + bot: ApiUser; + domain: string; + shouldRequestWriteAccess?: boolean; +}; + +type ApiUrlAuthResultAccepted = { + type: 'accepted'; + url: string; +}; + +type ApiUrlAuthResultDefault = { + type: 'default'; +}; + +export type ApiUrlAuthResult = ApiUrlAuthResultRequest | ApiUrlAuthResultAccepted | ApiUrlAuthResultDefault; diff --git a/src/api/types/statistics.ts b/src/api/types/statistics.ts index e989e966a..3da5c8cf0 100644 --- a/src/api/types/statistics.ts +++ b/src/api/types/statistics.ts @@ -1,5 +1,5 @@ import type { ApiChat } from './chats'; -import type { ApiMessage, ApiPhoto } from './messages'; +import type { ApiMessage } from './messages'; export interface ApiChannelStatistics { growthGraph?: StatisticsGraph | string; diff --git a/src/assets/fonts/icomoon.woff b/src/assets/fonts/icomoon.woff index 524f03810375cece469c4a29cc2b343e3547d608..ff124f775dfd14bc8ecfdd74a11c6a387046770b 100644 GIT binary patch delta 1180 zcmYjRU5Fc16u#%)`P)o3$tKMtnfz^&ZGLvQyUENZSz)*Mur6&uwArGCDiXCe%C4=n zV!9L}NPUrFOJB4g{?%$ri(pm|scb2I6~tQDhf)L|N?$}2eQCS#yA!qEx#!$-zWL5~ z&K*duZ1DTHx%I{qi;IjH)|bbb>75luXSQClPETgLdjaf?J$0jSuyy1xV{`?%@sEWL zE+2ad*mcG}|3=#HuXHZ9542un?80$y>e5EfuAgiJ1h$5JOqzaOdA_}Jv;%B|F@8$g ziH(IXmJhuE-laA)cS~DHI`-O1tAoauW$~zIU(u~sTPp{E{RGn6(%#=abnoz?BS&$E zXHMULXJQsaALitEh>hO;u>YXn2L_3|%!{f2OH5&cQSV#QKkg2*iT+J-Q@qJK>;!v{ zea3q1D*K-O%zkC}$RLX_I89NpQ2f{B3i+It50L)99o=HZG8B909Lsg;?hb0e4|c_I zXQ^75ksj8VCAU)V!m0p3sW{cTBk3sec5S9mRP!_SMus2(FC0%lK>1vu=uuo12gbl? z2y_^RK%Pew0f_B-NNm7Bj6fYIAac5x&T>)F7~E=#YBGy}I8Xv90^mR8hutJkHW<7D zJ1^FTw)o@e^kc>1cx*bM2Ms--C#GY*uy1Fl?2j6uk#Km(^2bx@g+ie$DcztBT6{W2 zoaFL69QPhC7qs|#dDt?-k&)2m*FOpocj5^rA;impRKN^r)^-b98}T9C{4F_`)BS38 zM^+2yO%haYmbsvH5ip0i%FTdSCm~9EwyLI@nae0$Q_syMY3Fb{fO20*jZRwDq@{&? z6Nzd9n=3a+;WHrV0S&UMUq2>$maUeHN|c{8Yx9fPfu0%k9WbyWir)p#2hdT&HD}mXFod+=J@{V_I;(d64kgw<5imH zy!d!gCO9~M%`OKq8P|jn(pQDXRYN3PJ5{qcuPHv&t|wECl+c$oQxhsrKaviX?PZQN zwrkr0(Y9S^hCb4pVH#Y`G1|V2t!@v!qSASq&oSe8`Y@&{QeGHHv^YZqqXX%N>1q=*~ zM}T||C>BW1sZ0Zkonc^DQUSu91^fSGq$Z{?Ff4Ncsxbp$h17oe44@!TtOm$e0b%wd zEQuMpB|tsP7BDa{^?-2W4VT)S{A8fIl{!E(JwW&!w~>TaZej({;#DO;jS67Ak?~Gm zVs0u>Yzk0O8wk(z;{08ZUkp^UMrPulZes_aLLQ(ynHX4rUQ>8E>*f6afB*jniUJLJ z$?$Rxn8*14u{eu3GXpbF4a4S5jO+_H|6JP5EORw>(&Bi2o39MqEMOh?7A*yOlYs$5 pPhPOnoN@8y8!PLXHb<}VW#kL^6_^tEA;<<~2E*dbyVf3J1OWZLXgL4? diff --git a/src/assets/fonts/icomoon.woff2 b/src/assets/fonts/icomoon.woff2 index 30e6da39d6877016e2a3c44b084f3fe8f01b313a..7b711129fd9dad683efe4885f08e8191c155fa7f 100644 GIT binary patch literal 21236 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 literal 20660 zcmV)8K*qm!Pew8T0RR9108q354FCWD0JFFN08m%}0RR9100000000000000000000 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 = ({ return () => setRating(rating === index ? undefined : index); } + const handleCancelClick = useCallback(() => { + closeCallRatingModal(); + }, [closeCallRatingModal]); + return (
@@ -68,7 +75,7 @@ const RatePhoneCallModal: FC = ({ - + ); }; diff --git a/src/components/common/SafeLink.tsx b/src/components/common/SafeLink.tsx index a301c6911..5a9c44685 100644 --- a/src/components/common/SafeLink.tsx +++ b/src/components/common/SafeLink.tsx @@ -4,7 +4,7 @@ import { getActions } from '../../global'; import convertPunycode from '../../lib/punycode'; import { - DEBUG, RE_TG_LINK, RE_TME_LINK, + DEBUG, } from '../../config'; import buildClassName from '../../util/buildClassName'; import { ensureProtocol } from '../../util/ensureProtocol'; @@ -24,31 +24,18 @@ const SafeLink: FC = ({ children, isRtl, }) => { - const { toggleSafeLinkModal, openTelegramLink } = getActions(); + const { openUrl } = getActions(); const content = children || text; - const isNotSafe = url !== content; + const isSafe = url === content; const handleClick = useCallback((e: React.MouseEvent) => { - if ( - e.ctrlKey || e.altKey || e.shiftKey || e.metaKey - || !url || (!url.match(RE_TME_LINK) && !url.match(RE_TG_LINK)) - ) { - if (isNotSafe) { - toggleSafeLinkModal({ url }); - - e.preventDefault(); - return false; - } - - return true; - } - + if (!url) return true; e.preventDefault(); - openTelegramLink({ url }); + openUrl({ url, shouldSkipModal: isSafe }); return false; - }, [isNotSafe, openTelegramLink, toggleSafeLinkModal, url]); + }, [isSafe, openUrl, url]); if (!url) { return undefined; diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index 872edc766..7bb24ec1d 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -161,6 +161,7 @@ const LeftColumn: FC = ({ case SettingsScreens.PrivacyForwarding: case SettingsScreens.PrivacyGroupChats: case SettingsScreens.PrivacyBlockedUsers: + case SettingsScreens.ActiveWebsites: case SettingsScreens.TwoFaDisabled: case SettingsScreens.TwoFaEnabled: case SettingsScreens.TwoFaCongratulations: diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index 7be57556e..e2b007ae8 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -208,7 +208,7 @@ const LeftMainHeader: FC = ({ }, [animationLevel, setSettingOption, theme]); const handleChangelogClick = useCallback(() => { - window.open(BETA_CHANGELOG_URL, '_blank'); + window.open(BETA_CHANGELOG_URL, '_blank', 'noopener'); }, []); const handleRuDiscussionClick = useCallback(() => { diff --git a/src/components/left/settings/Settings.scss b/src/components/left/settings/Settings.scss index 01423a0fd..54f2fb9f8 100644 --- a/src/components/left/settings/Settings.scss +++ b/src/components/left/settings/Settings.scss @@ -182,6 +182,7 @@ &.full-size { width: 100%; + overflow: hidden; } .date { diff --git a/src/components/left/settings/Settings.tsx b/src/components/left/settings/Settings.tsx index bb788d42e..6be8f2bfa 100644 --- a/src/components/left/settings/Settings.tsx +++ b/src/components/left/settings/Settings.tsx @@ -21,6 +21,7 @@ import SettingsPrivacy from './SettingsPrivacy'; import SettingsLanguage from './SettingsLanguage'; import SettingsPrivacyVisibility from './SettingsPrivacyVisibility'; import SettingsActiveSessions from './SettingsActiveSessions'; +import SettingsActiveWebsites from './SettingsActiveWebsites'; import SettingsPrivacyBlockedUsers from './SettingsPrivacyBlockedUsers'; import SettingsTwoFa from './twoFa/SettingsTwoFa'; import SettingsPrivacyVisibilityExceptionList from './SettingsPrivacyVisibilityExceptionList'; @@ -69,7 +70,7 @@ const FOLDERS_SCREENS = [ const PRIVACY_SCREENS = [ SettingsScreens.PrivacyBlockedUsers, - SettingsScreens.ActiveSessions, + SettingsScreens.ActiveWebsites, ]; const PRIVACY_PHONE_NUMBER_SCREENS = [ @@ -258,6 +259,13 @@ const Settings: FC = ({ onReset={handleReset} /> ); + case SettingsScreens.ActiveWebsites: + return ( + + ); case SettingsScreens.PrivacyBlockedUsers: return ( = ({ contextActions={[{ title: 'Terminate', icon: 'stop', + destructive: true, handler: () => { handleTerminateSessionClick(session.hash); }, diff --git a/src/components/left/settings/SettingsActiveWebsite.module.scss b/src/components/left/settings/SettingsActiveWebsite.module.scss new file mode 100644 index 000000000..cf5796aa3 --- /dev/null +++ b/src/components/left/settings/SettingsActiveWebsite.module.scss @@ -0,0 +1,48 @@ +.root { + :global(.modal-dialog) { + max-width: 28rem; + } +} + +.avatar { + width: 5rem; + height: 5rem; + font-size: 3.5rem; + margin: 0 auto 1rem; + border-radius: 1rem; + + :global(.Avatar__img) { + border-radius: 1rem; + } +} + +.title { + text-align: center; + margin-bottom: 0.25rem; +} + +.note, +.date { + color: var(--color-text-secondary); + font-size: 0.875rem; + text-align: center; +} + +.box { + background: var(--color-background-secondary); + padding: 1rem 1rem 0.5rem; + border-radius: var(--border-radius-default); + margin: 1rem 0; +} + +.action-header { + margin-top: 1px; +} + +.action-name { + margin-right: auto; +} + +.header-button { + margin-right: -0.5rem; +} diff --git a/src/components/left/settings/SettingsActiveWebsite.tsx b/src/components/left/settings/SettingsActiveWebsite.tsx new file mode 100644 index 000000000..1cd74c887 --- /dev/null +++ b/src/components/left/settings/SettingsActiveWebsite.tsx @@ -0,0 +1,103 @@ +import React, { memo, useCallback } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import type { FC } from '../../../lib/teact/teact'; +import type { ApiUser, ApiWebSession } from '../../../api/types'; + +import { getUserFullName } from '../../../global/helpers'; +import buildClassName from '../../../util/buildClassName'; + +import useLang from '../../../hooks/useLang'; +import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; + +import Modal from '../../ui/Modal'; +import Button from '../../ui/Button'; +import Avatar from '../../common/Avatar'; + +import styles from './SettingsActiveWebsite.module.scss'; + +type OwnProps = { + isOpen: boolean; + hash?: string; + onClose: () => void; +}; + +type StateProps = { + session?: ApiWebSession; + bot?: ApiUser; +}; + +const SettingsActiveWebsite: FC = ({ + isOpen, session, bot, onClose, +}) => { + const { terminateWebAuthorization } = getActions(); + const lang = useLang(); + + const renderingSession = useCurrentOrPrev(session, true); + const renderingBot = useCurrentOrPrev(bot, true); + + const handleTerminateSessionClick = useCallback(() => { + terminateWebAuthorization({ hash: session!.hash }); + onClose(); + }, [onClose, session, terminateWebAuthorization]); + + if (!renderingSession) { + return undefined; + } + + function renderHeader() { + return ( +
+ +
{lang('WebSessionsTitle')}
+ +
+ ); + } + return ( + + +

{getUserFullName(renderingBot)}

+
+ {renderingSession?.domain} +
+ +
+
{lang('AuthSessions.View.Browser')}
+
+ {renderingSession?.browser} +
+ +
{lang('SessionPreview.Ip')}
+
{renderingSession?.ip}
+ +
{lang('SessionPreview.Location')}
+
{renderingSession?.region}
+
+

{lang('AuthSessions.View.LocationInfo')}

+
+ ); +}; + +export default memo(withGlobal((global, { hash }) => { + const session = hash ? global.activeWebSessions.byHash[hash] : undefined; + const bot = session ? global.users.byId[session.botId] : undefined; + return { + session, + bot, + }; +})(SettingsActiveWebsite)); diff --git a/src/components/left/settings/SettingsActiveWebsites.module.scss b/src/components/left/settings/SettingsActiveWebsites.module.scss new file mode 100644 index 000000000..3f41f1ebf --- /dev/null +++ b/src/components/left/settings/SettingsActiveWebsites.module.scss @@ -0,0 +1,21 @@ +.avatar { + width: 2rem; + height: 2rem; + margin-inline-end: 1.5rem; + + border-radius: 0.5rem; + :global(.Avatar__img) { + border-radius: 0.5rem; + } +} + +.clear-help { + margin-top: 0.5rem !important; + margin-bottom: 0 !important; +} + +:global(.subtitle) { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/src/components/left/settings/SettingsActiveWebsites.tsx b/src/components/left/settings/SettingsActiveWebsites.tsx new file mode 100644 index 000000000..6356a9d1a --- /dev/null +++ b/src/components/left/settings/SettingsActiveWebsites.tsx @@ -0,0 +1,166 @@ +import React, { + memo, useCallback, useEffect, useState, +} from '../../../lib/teact/teact'; +import { getActions, getGlobal, withGlobal } from '../../../global'; + +import type { FC } from '../../../lib/teact/teact'; +import type { ApiWebSession } from '../../../api/types'; + +import { formatPastTimeShort } from '../../../util/dateFormat'; +import { getUserFullName } from '../../../global/helpers'; +import buildClassName from '../../../util/buildClassName'; + +import useFlag from '../../../hooks/useFlag'; +import useLang from '../../../hooks/useLang'; +import useHistoryBack from '../../../hooks/useHistoryBack'; + +import ListItem from '../../ui/ListItem'; +import ConfirmDialog from '../../ui/ConfirmDialog'; +import SettingsActiveWebsite from './SettingsActiveWebsite'; +import Avatar from '../../common/Avatar'; + +import styles from './SettingsActiveWebsites.module.scss'; + +type OwnProps = { + isActive?: boolean; + onReset: () => void; +}; + +type StateProps = { + byHash: Record; + orderedHashes: string[]; +}; + +const SettingsActiveWebsites: FC = ({ + isActive, + byHash, + orderedHashes, + onReset, +}) => { + const { + terminateWebAuthorization, + terminateAllWebAuthorizations, + } = getActions(); + + const lang = useLang(); + const [isConfirmTerminateAllDialogOpen, openConfirmTerminateAllDialog, closeConfirmTerminateAllDialog] = useFlag(); + const [openedWebsiteHash, setOpenedWebsiteHash] = useState(); + const [isModalOpen, openModal, closeModal] = useFlag(); + + const handleTerminateAuthClick = useCallback((hash: string) => { + terminateWebAuthorization({ hash }); + }, [terminateWebAuthorization]); + + const handleTerminateAllAuth = useCallback(() => { + closeConfirmTerminateAllDialog(); + terminateAllWebAuthorizations(); + }, [closeConfirmTerminateAllDialog, terminateAllWebAuthorizations]); + + const handleOpenSessionModal = useCallback((hash: string) => { + setOpenedWebsiteHash(hash); + openModal(); + }, [openModal]); + + const handleCloseWebsiteModal = useCallback(() => { + setOpenedWebsiteHash(undefined); + closeModal(); + }, [closeModal]); + + // Close when empty + useEffect(() => { + if (!orderedHashes.length) { + onReset(); + } + }, [onReset, orderedHashes]); + + useHistoryBack({ + isActive, + onBack: onReset, + }); + + function renderSessions(sessionHashes: string[]) { + return ( +
+

+ {lang('WebSessionsTitle')} +

+ + {sessionHashes.map(renderSession)} +
+ ); + } + + function renderSession(sessionHash: string) { + const session = byHash[sessionHash]; + const bot = getGlobal().users.byId[session.botId]; + + return ( + { + handleTerminateAuthClick(session.hash); + }, + }]} + // eslint-disable-next-line react/jsx-no-bind + onClick={() => handleOpenSessionModal(session.hash)} + > + +
+ {formatPastTimeShort(lang, session.dateActive * 1000)} + {getUserFullName(bot)} + + {session.domain}, {session.browser}, {session.platform} + + {session.ip} {session.region} +
+
+ ); + } + + if (!orderedHashes.length) return undefined; + + return ( +
+
+ + {lang('AuthSessions.LogOutApplications')} + +

+ {lang('ClearOtherWebSessionsHelp')} +

+
+ {renderSessions(orderedHashes)} + + +
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const { byHash, orderedHashes } = global.activeWebSessions; + return { + byHash, + orderedHashes, + }; + }, +)(SettingsActiveWebsites)); diff --git a/src/components/left/settings/SettingsHeader.tsx b/src/components/left/settings/SettingsHeader.tsx index a2415bd77..8871464e1 100644 --- a/src/components/left/settings/SettingsHeader.tsx +++ b/src/components/left/settings/SettingsHeader.tsx @@ -129,6 +129,8 @@ const SettingsHeader: FC = ({ case SettingsScreens.ActiveSessions: return

{lang('SessionsTitle')}

; + case SettingsScreens.ActiveWebsites: + return

{lang('OtherWebSessions')}

; case SettingsScreens.PrivacyBlockedUsers: return

{lang('BlockedUsers')}

; diff --git a/src/components/left/settings/SettingsMain.tsx b/src/components/left/settings/SettingsMain.tsx index 52dc21cbd..07c080a20 100644 --- a/src/components/left/settings/SettingsMain.tsx +++ b/src/components/left/settings/SettingsMain.tsx @@ -33,7 +33,7 @@ const SettingsMain: FC = ({ sessionCount, lastSyncTime, }) => { - const { loadProfilePhotos, loadAuthorizations } = getActions(); + const { loadProfilePhotos, loadAuthorizations, loadWebAuthorizations } = getActions(); const lang = useLang(); const profileId = currentUser?.id; @@ -52,8 +52,9 @@ const SettingsMain: FC = ({ useEffect(() => { if (lastSyncTime) { loadAuthorizations(); + loadWebAuthorizations(); } - }, [lastSyncTime, loadAuthorizations]); + }, [lastSyncTime, loadAuthorizations, loadWebAuthorizations]); return (
diff --git a/src/components/left/settings/SettingsPrivacy.tsx b/src/components/left/settings/SettingsPrivacy.tsx index 2aaf0912e..136905c62 100644 --- a/src/components/left/settings/SettingsPrivacy.tsx +++ b/src/components/left/settings/SettingsPrivacy.tsx @@ -21,6 +21,7 @@ type StateProps = { hasPassword?: boolean; hasPasscode?: boolean; blockedCount: number; + webAuthCount: number; isSensitiveEnabled?: boolean; canChangeSensitive?: boolean; privacyPhoneNumber?: ApiPrivacySettings; @@ -34,11 +35,10 @@ type StateProps = { const SettingsPrivacy: FC = ({ isActive, - onScreenSelect, - onReset, hasPassword, hasPasscode, blockedCount, + webAuthCount, isSensitiveEnabled, canChangeSensitive, privacyPhoneNumber, @@ -48,7 +48,8 @@ const SettingsPrivacy: FC = ({ privacyGroupChats, privacyPhoneCall, privacyPhoneP2P, - + onScreenSelect, + onReset, }) => { const { loadPrivacySettings, @@ -114,6 +115,16 @@ const SettingsPrivacy: FC = ({ )}
+ {webAuthCount > 0 && ( + onScreenSelect(SettingsScreens.ActiveWebsites)} + > + {lang('PrivacySettings.WebSessions')} + {webAuthCount} + + )} ( hasPassword, hasPasscode: Boolean(hasPasscode), blockedCount: blocked.totalCount, + webAuthCount: global.activeWebSessions.orderedHashes.length, isSensitiveEnabled, canChangeSensitive, privacyPhoneNumber: privacy.phoneNumber, diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index dc7d0b7e4..500e4c0bc 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -6,7 +6,7 @@ import { getActions, withGlobal } from '../../global'; import type { LangCode } from '../../types'; import type { - ApiChat, ApiMessage, ApiUpdateAuthorizationStateType, ApiUpdateConnectionStateType, + ApiChat, ApiMessage, ApiUpdateAuthorizationStateType, ApiUpdateConnectionStateType, ApiUser, } from '../../api/types'; import type { GlobalState } from '../../global/types'; @@ -21,6 +21,7 @@ import { selectIsMediaViewerOpen, selectIsRightColumnShown, selectIsServiceChatReady, + selectUser, } from '../../global/selectors'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import buildClassName from '../../util/buildClassName'; @@ -61,6 +62,7 @@ import RatePhoneCallModal from '../calls/phone/RatePhoneCallModal.async'; import WebAppModal from './WebAppModal.async'; import BotTrustModal from './BotTrustModal.async'; import BotAttachModal from './BotAttachModal.async'; +import UrlAuthModal from './UrlAuthModal.async'; import './Main.scss'; @@ -95,6 +97,8 @@ type StateProps = { webApp?: GlobalState['webApp']; botTrustRequest?: GlobalState['botTrustRequest']; botAttachRequest?: GlobalState['botAttachRequest']; + currentUser?: ApiUser; + urlAuth?: GlobalState['urlAuth']; }; const NOTIFICATION_INTERVAL = 1000; @@ -134,6 +138,8 @@ const Main: FC = ({ botTrustRequest, botAttachRequest, webApp, + currentUser, + urlAuth, }) => { const { sync, @@ -369,6 +375,7 @@ const Main: FC = ({ {audioMessage && } + = ({ url }) => { const lang = useLang(); const handleOpen = useCallback(() => { - window.open(ensureProtocol(url)); + window.open(ensureProtocol(url), '_blank', 'noopener'); toggleSafeLinkModal({ url: undefined }); }, [toggleSafeLinkModal, url]); diff --git a/src/components/main/UrlAuthModal.async.tsx b/src/components/main/UrlAuthModal.async.tsx new file mode 100644 index 000000000..a3b131eb7 --- /dev/null +++ b/src/components/main/UrlAuthModal.async.tsx @@ -0,0 +1,17 @@ +import React, { memo } from '../../lib/teact/teact'; +import { Bundles } from '../../util/moduleLoader'; + +import type { FC } from '../../lib/teact/teact'; +import type { OwnProps } from './UrlAuthModal'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const UrlAuthModalAsync: FC = (props) => { + const { urlAuth } = props; + const UrlAuthModal = useModuleLoader(Bundles.Extra, 'UrlAuthModal', !urlAuth); + + // eslint-disable-next-line react/jsx-props-no-spreading + return UrlAuthModal ? : undefined; +}; + +export default memo(UrlAuthModalAsync); diff --git a/src/components/main/UrlAuthModal.module.scss b/src/components/main/UrlAuthModal.module.scss new file mode 100644 index 000000000..85b5d4172 --- /dev/null +++ b/src/components/main/UrlAuthModal.module.scss @@ -0,0 +1,3 @@ +.checkbox { + margin: 1rem 0; +} diff --git a/src/components/main/UrlAuthModal.tsx b/src/components/main/UrlAuthModal.tsx new file mode 100644 index 000000000..74a0bda6a --- /dev/null +++ b/src/components/main/UrlAuthModal.tsx @@ -0,0 +1,114 @@ +import React, { + memo, useCallback, useEffect, useState, +} from '../../lib/teact/teact'; +import { getActions, getGlobal } from '../../global'; + +import type { FC } from '../../lib/teact/teact'; +import type { ApiUser } from '../../api/types'; +import type { GlobalState } from '../../global/types'; + +import { ensureProtocol } from '../../util/ensureProtocol'; +import renderText from '../common/helpers/renderText'; +import { getUserFullName } from '../../global/helpers'; + +import useLang from '../../hooks/useLang'; +import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; + +import ConfirmDialog from '../ui/ConfirmDialog'; +import Checkbox from '../ui/Checkbox'; + +import styles from './UrlAuthModal.module.scss'; + +export type OwnProps = { + urlAuth?: GlobalState['urlAuth']; + currentUser?: ApiUser; +}; + +const UrlAuthModal: FC = ({ + urlAuth, currentUser, +}) => { + const { closeUrlAuthModal, acceptBotUrlAuth, acceptLinkUrlAuth } = getActions(); + const [isLoginChecked, setLoginChecked] = useState(true); + const [isWriteAccessChecked, setWriteAccessChecked] = useState(true); + const currentAuth = useCurrentOrPrev(urlAuth, false); + const { domain, botId, shouldRequestWriteAccess } = currentAuth?.request || {}; + const bot = botId ? getGlobal().users.byId[botId] : undefined; + + const lang = useLang(); + + const handleOpen = useCallback(() => { + if (urlAuth?.url && isLoginChecked) { + const acceptAction = urlAuth.button ? acceptBotUrlAuth : acceptLinkUrlAuth; + acceptAction({ + isWriteAllowed: isWriteAccessChecked, + }); + } else { + window.open(ensureProtocol(currentAuth?.url), '_blank', 'noopener'); + } + closeUrlAuthModal(); + }, [ + urlAuth, isLoginChecked, closeUrlAuthModal, acceptBotUrlAuth, acceptLinkUrlAuth, isWriteAccessChecked, currentAuth, + ]); + + const handleDismiss = useCallback(() => { + closeUrlAuthModal(); + }, [closeUrlAuthModal]); + + const handleLoginChecked = useCallback((value: boolean) => { + setLoginChecked(value); + setWriteAccessChecked(value); + }, [setLoginChecked]); + + // Reset on re-open + useEffect(() => { + if (domain) { + setLoginChecked(true); + setWriteAccessChecked(Boolean(shouldRequestWriteAccess)); + } + }, [shouldRequestWriteAccess, domain]); + + return ( + + {renderText(lang('OpenUrlAlert2', currentAuth?.url), ['links'])} + {domain && ( + + {renderText( + lang('Conversation.OpenBotLinkLogin', [domain, getUserFullName(currentUser)]), + ['simple_markdown'], + )} + + )} + onCheck={handleLoginChecked} + className={styles.checkbox} + /> + )} + {shouldRequestWriteAccess && ( + + {renderText( + lang('Conversation.OpenBotLinkAllowMessages', getUserFullName(bot)), + ['simple_markdown'], + )} + + )} + onCheck={setWriteAccessChecked} + disabled={!isLoginChecked} + className={styles.checkbox} + /> + )} + + ); +}; + +export default memo(UrlAuthModal); diff --git a/src/components/main/WebAppModal.tsx b/src/components/main/WebAppModal.tsx index 270c2abab..7157be965 100644 --- a/src/components/main/WebAppModal.tsx +++ b/src/components/main/WebAppModal.tsx @@ -166,6 +166,10 @@ const WebAppModal: FC = ({ }); }, [bot, isInstalled, toggleBotInAttachMenu]); + const handleCloseClick = useCallback(() => { + closeWebApp(); + }, [closeWebApp]); + const openBotChat = useCallback(() => { openChat({ id: bot!.id, @@ -197,7 +201,7 @@ const WebAppModal: FC = ({ color="translucent" size="smaller" ariaLabel={lang('Close')} - onClick={closeWebApp} + onClick={handleCloseClick} > @@ -219,7 +223,9 @@ const WebAppModal: FC = ({
); - }, [lang, closeWebApp, bot, MoreMenuButton, handleRefreshClick, isInstalled, handleToggleClick, chat, openBotChat]); + }, [ + lang, handleCloseClick, bot, MoreMenuButton, chat, openBotChat, handleRefreshClick, isInstalled, handleToggleClick, + ]); const prevMainButtonColor = usePrevious(mainButton?.color, true); const prevMainButtonTextColor = usePrevious(mainButton?.textColor, true); diff --git a/src/components/middle/message/InlineButtons.scss b/src/components/middle/message/InlineButtons.scss index de45de40c..f47b061df 100644 --- a/src/components/middle/message/InlineButtons.scss +++ b/src/components/middle/message/InlineButtons.scss @@ -4,8 +4,9 @@ max-width: var(--max-width); .row { - display: flex; - flex-direction: row; + display: grid; + grid-auto-columns: minmax(0, 1fr); + grid-auto-flow: column; } .Button { diff --git a/src/components/middle/message/InlineButtons.tsx b/src/components/middle/message/InlineButtons.tsx index c3f3247d1..342e18df6 100644 --- a/src/components/middle/message/InlineButtons.tsx +++ b/src/components/middle/message/InlineButtons.tsx @@ -1,6 +1,6 @@ -import type { FC } from '../../../lib/teact/teact'; import React from '../../../lib/teact/teact'; +import type { FC } from '../../../lib/teact/teact'; import type { ApiKeyboardButton, ApiMessage } from '../../../api/types'; import { RE_TME_LINK } from '../../../config'; @@ -18,6 +18,30 @@ type OwnProps = { const InlineButtons: FC = ({ message, onClick }) => { const lang = useLang(); + + const renderIcon = (button: ApiKeyboardButton) => { + const { type } = button; + switch (type) { + case 'url': { + if (!RE_TME_LINK.test(button.url)) { + return ; + } + break; + } + case 'urlAuth': + return ; + case 'buy': + case 'receipt': + return ; + case 'switchBotInline': + return ; + case 'webView': + case 'simpleWebView': + return ; + } + return undefined; + }; + return (
{message.inlineButtons!.map((row) => ( @@ -31,10 +55,7 @@ const InlineButtons: FC = ({ message, onClick }) => { onClick={() => onClick({ messageId: message.id, button })} > {renderText(lang(button.text))} - {['buy', 'receipt'].includes(button.type) && } - {button.type === 'url' && !RE_TME_LINK.test(button.url) && } - {button.type === 'switchBotInline' && } - {['webView', 'simpleWebView'].includes(button.type) && } + {renderIcon(button)} ))}
diff --git a/src/components/middle/message/Location.tsx b/src/components/middle/message/Location.tsx index fb5e91d30..9281da4ab 100644 --- a/src/components/middle/message/Location.tsx +++ b/src/components/middle/message/Location.tsx @@ -108,7 +108,7 @@ const Location: FC = ({ const handleClick = () => { const url = prepareMapUrl(point.lat, point.long, zoom); - window.open(url, '_blank')?.focus(); + window.open(url, '_blank', 'noopener')?.focus(); }; const updateCountdown = useCallback((countdownEl: HTMLDivElement) => { diff --git a/src/components/ui/Checkbox.tsx b/src/components/ui/Checkbox.tsx index b644c67b3..ddf04a333 100644 --- a/src/components/ui/Checkbox.tsx +++ b/src/components/ui/Checkbox.tsx @@ -23,6 +23,7 @@ type OwnProps = { blocking?: boolean; isLoading?: boolean; withCheckedCallback?: boolean; + className?: string; onChange?: (e: ChangeEvent) => void; onCheck?: (isChecked: boolean) => void; }; @@ -39,6 +40,7 @@ const Checkbox: FC = ({ round, blocking, isLoading, + className, onChange, onCheck, }) => { @@ -53,16 +55,17 @@ const Checkbox: FC = ({ } }, [onChange, onCheck]); - const className = buildClassName( + const labelClassName = buildClassName( 'Checkbox', disabled && 'disabled', round && 'round', isLoading && 'loading', blocking && 'blocking', + className, ); return ( -