Implement Seamless Login (#1865)

This commit is contained in:
Alexander Zinchuk 2022-06-06 01:44:25 +04:00
parent 52fa0c8fca
commit b39ae6f0a1
49 changed files with 1317 additions and 242 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ApiAppConfig | undefined> {
const result = await invokeRequest(new GramJs.help.GetAppConfig());
if (!result) return undefined;
return buildApiConfig(result);
return buildAppConfig(result);
}
function updateLocalDb(

View File

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

View File

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

View File

@ -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<number, string | number[]>;
@ -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;

View File

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

Binary file not shown.

Binary file not shown.

View File

@ -5,6 +5,7 @@ export { default as ForwardPicker } from '../components/main/ForwardPicker';
export { default as Dialogs } from '../components/main/Dialogs';
export { default as Notifications } from '../components/main/Notifications';
export { default as SafeLinkModal } from '../components/main/SafeLinkModal';
export { default as UrlAuthModal } from '../components/main/UrlAuthModal';
export { default as HistoryCalendar } from '../components/main/HistoryCalendar';
export { default as NewContactModal } from '../components/main/NewContactModal';
export { default as WebAppModal } from '../components/main/WebAppModal';

View File

@ -1,7 +1,10 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useRef, useState } from '../../../lib/teact/teact';
import React, {
memo, useRef, useState, useCallback,
} from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type { FC } from '../../../lib/teact/teact';
import useLang from '../../../hooks/useLang';
import buildClassName from '../../../util/buildClassName';
@ -41,6 +44,10 @@ const RatePhoneCallModal: FC<OwnProps> = ({
return () => setRating(rating === index ? undefined : index);
}
const handleCancelClick = useCallback(() => {
closeCallRatingModal();
}, [closeCallRatingModal]);
return (
<Modal title={lang('lng_call_rate_label')} className="narrow" onClose={closeCallRatingModal} isOpen={isOpen}>
<div className={styles.stars}>
@ -68,7 +75,7 @@ const RatePhoneCallModal: FC<OwnProps> = ({
<Button className="confirm-dialog-button" isText onClick={handleSend}>
{lang('Send')}
</Button>
<Button className="confirm-dialog-button" isText onClick={closeCallRatingModal}>{lang('Cancel')}</Button>
<Button className="confirm-dialog-button" isText onClick={handleCancelClick}>{lang('Cancel')}</Button>
</Modal>
);
};

View File

@ -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<OwnProps> = ({
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<HTMLAnchorElement, 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;

View File

@ -161,6 +161,7 @@ const LeftColumn: FC<StateProps> = ({
case SettingsScreens.PrivacyForwarding:
case SettingsScreens.PrivacyGroupChats:
case SettingsScreens.PrivacyBlockedUsers:
case SettingsScreens.ActiveWebsites:
case SettingsScreens.TwoFaDisabled:
case SettingsScreens.TwoFaEnabled:
case SettingsScreens.TwoFaCongratulations:

View File

@ -208,7 +208,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
}, [animationLevel, setSettingOption, theme]);
const handleChangelogClick = useCallback(() => {
window.open(BETA_CHANGELOG_URL, '_blank');
window.open(BETA_CHANGELOG_URL, '_blank', 'noopener');
}, []);
const handleRuDiscussionClick = useCallback(() => {

View File

@ -182,6 +182,7 @@
&.full-size {
width: 100%;
overflow: hidden;
}
.date {

View File

@ -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<OwnProps> = ({
onReset={handleReset}
/>
);
case SettingsScreens.ActiveWebsites:
return (
<SettingsActiveWebsites
isActive={isScreenActive}
onReset={handleReset}
/>
);
case SettingsScreens.PrivacyBlockedUsers:
return (
<SettingsPrivacyBlockedUsers

View File

@ -9,7 +9,7 @@ $icons: "android", "apple", "brave", "chrome", "edge", "firefox", "linux", "oper
.SettingsActiveSessions {
.icon-device {
width: 2rem;
height:2rem;
height: 2rem;
background-repeat: no-repeat;
background-size: 2rem;
flex: 0 0 2rem;

View File

@ -195,6 +195,7 @@ const SettingsActiveSessions: FC<OwnProps & StateProps> = ({
contextActions={[{
title: 'Terminate',
icon: 'stop',
destructive: true,
handler: () => {
handleTerminateSessionClick(session.hash);
},

View File

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

View File

@ -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<OwnProps & StateProps> = ({
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 (
<div className="modal-header-condensed" dir={lang.isRtl ? 'rtl' : undefined}>
<Button round color="translucent" size="smaller" ariaLabel={lang('Close')} onClick={onClose}>
<i className="icon-close" />
</Button>
<div className="modal-title">{lang('WebSessionsTitle')}</div>
<Button
color="danger"
onClick={handleTerminateSessionClick}
className={buildClassName('modal-action-button', styles.headerButton)}
>
{lang('AuthSessions.LogOut')}
</Button>
</div>
);
}
return (
<Modal
header={renderHeader()}
isOpen={isOpen}
hasCloseButton
onClose={onClose}
className={styles.root}
>
<Avatar className={styles.avatar} user={renderingBot} size="large" />
<h3 className={styles.title} dir="auto">{getUserFullName(renderingBot)}</h3>
<div className={styles.date} aria-label={lang('PrivacySettings.LastSeen')}>
{renderingSession?.domain}
</div>
<dl className={styles.box}>
<dt>{lang('AuthSessions.View.Browser')}</dt>
<dd>
{renderingSession?.browser}
</dd>
<dt>{lang('SessionPreview.Ip')}</dt>
<dd>{renderingSession?.ip}</dd>
<dt>{lang('SessionPreview.Location')}</dt>
<dd>{renderingSession?.region}</dd>
</dl>
<p className={styles.note}>{lang('AuthSessions.View.LocationInfo')}</p>
</Modal>
);
};
export default memo(withGlobal<OwnProps>((global, { hash }) => {
const session = hash ? global.activeWebSessions.byHash[hash] : undefined;
const bot = session ? global.users.byId[session.botId] : undefined;
return {
session,
bot,
};
})(SettingsActiveWebsite));

View File

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

View File

@ -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<string, ApiWebSession>;
orderedHashes: string[];
};
const SettingsActiveWebsites: FC<OwnProps & StateProps> = ({
isActive,
byHash,
orderedHashes,
onReset,
}) => {
const {
terminateWebAuthorization,
terminateAllWebAuthorizations,
} = getActions();
const lang = useLang();
const [isConfirmTerminateAllDialogOpen, openConfirmTerminateAllDialog, closeConfirmTerminateAllDialog] = useFlag();
const [openedWebsiteHash, setOpenedWebsiteHash] = useState<string | undefined>();
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 (
<div className="settings-item">
<h4 className="settings-item-header mb-4" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('WebSessionsTitle')}
</h4>
{sessionHashes.map(renderSession)}
</div>
);
}
function renderSession(sessionHash: string) {
const session = byHash[sessionHash];
const bot = getGlobal().users.byId[session.botId];
return (
<ListItem
key={session.hash}
ripple
narrow
contextActions={[{
title: 'Terminate',
icon: 'stop',
destructive: true,
handler: () => {
handleTerminateAuthClick(session.hash);
},
}]}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => handleOpenSessionModal(session.hash)}
>
<Avatar className={styles.avatar} user={bot} size="tiny" />
<div className="multiline-menu-item full-size" dir="auto">
<span className="date">{formatPastTimeShort(lang, session.dateActive * 1000)}</span>
<span className="title">{getUserFullName(bot)}</span>
<span className={buildClassName('subtitle', 'black', 'tight', styles.platform)}>
{session.domain}, {session.browser}, {session.platform}
</span>
<span className="subtitle">{session.ip} {session.region}</span>
</div>
</ListItem>
);
}
if (!orderedHashes.length) return undefined;
return (
<div className="settings-content custom-scroll">
<div className="settings-item">
<ListItem
className="destructive mb-0 no-icon"
icon="stop"
ripple
narrow
onClick={openConfirmTerminateAllDialog}
>
{lang('AuthSessions.LogOutApplications')}
</ListItem>
<p className={buildClassName('settings-item-description', styles.clearHelp)}>
{lang('ClearOtherWebSessionsHelp')}
</p>
</div>
{renderSessions(orderedHashes)}
<ConfirmDialog
isOpen={isConfirmTerminateAllDialogOpen}
onClose={closeConfirmTerminateAllDialog}
title={lang('AuthSessions.LogOutApplications')}
text={lang('AreYouSureWebSessions')}
confirmHandler={handleTerminateAllAuth}
confirmIsDestructive
/>
<SettingsActiveWebsite isOpen={isModalOpen} hash={openedWebsiteHash} onClose={handleCloseWebsiteModal} />
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { byHash, orderedHashes } = global.activeWebSessions;
return {
byHash,
orderedHashes,
};
},
)(SettingsActiveWebsites));

View File

@ -129,6 +129,8 @@ const SettingsHeader: FC<OwnProps> = ({
case SettingsScreens.ActiveSessions:
return <h3>{lang('SessionsTitle')}</h3>;
case SettingsScreens.ActiveWebsites:
return <h3>{lang('OtherWebSessions')}</h3>;
case SettingsScreens.PrivacyBlockedUsers:
return <h3>{lang('BlockedUsers')}</h3>;

View File

@ -33,7 +33,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
useEffect(() => {
if (lastSyncTime) {
loadAuthorizations();
loadWebAuthorizations();
}
}, [lastSyncTime, loadAuthorizations]);
}, [lastSyncTime, loadAuthorizations, loadWebAuthorizations]);
return (
<div className="settings-content custom-scroll">

View File

@ -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<OwnProps & StateProps> = ({
isActive,
onScreenSelect,
onReset,
hasPassword,
hasPasscode,
blockedCount,
webAuthCount,
isSensitiveEnabled,
canChangeSensitive,
privacyPhoneNumber,
@ -48,7 +48,8 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
privacyGroupChats,
privacyPhoneCall,
privacyPhoneP2P,
onScreenSelect,
onReset,
}) => {
const {
loadPrivacySettings,
@ -114,6 +115,16 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
)}
</div>
</ListItem>
{webAuthCount > 0 && (
<ListItem
icon="web"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.ActiveWebsites)}
>
{lang('PrivacySettings.WebSessions')}
<span className="settings-item__current-value">{webAuthCount}</span>
</ListItem>
)}
<ListItem
icon="key"
narrow
@ -279,6 +290,7 @@ export default memo(withGlobal<OwnProps>(
hasPassword,
hasPasscode: Boolean(hasPasscode),
blockedCount: blocked.totalCount,
webAuthCount: global.activeWebSessions.orderedHashes.length,
isSensitiveEnabled,
canChangeSensitive,
privacyPhoneNumber: privacy.phoneNumber,

View File

@ -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<StateProps> = ({
botTrustRequest,
botAttachRequest,
webApp,
currentUser,
urlAuth,
}) => {
const {
sync,
@ -369,6 +375,7 @@ const Main: FC<StateProps> = ({
<Dialogs isOpen={hasDialogs} />
{audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />}
<SafeLinkModal url={safeLinkModalUrl} />
<UrlAuthModal urlAuth={urlAuth} currentUser={currentUser} />
<HistoryCalendar isOpen={isHistoryCalendarOpen} />
<StickerSetModal
isOpen={Boolean(openedStickerSetShortName)}
@ -432,6 +439,7 @@ export default memo(withGlobal(
const openedGame = global.openedGame;
const gameMessage = openedGame && selectChatMessage(global, openedGame.chatId, openedGame.messageId);
const gameTitle = gameMessage?.content.game?.title;
const currentUser = global.currentUserId ? selectUser(global, global.currentUserId) : undefined;
return {
connectionState: global.connectionState,
@ -463,6 +471,8 @@ export default memo(withGlobal(
botTrustRequest: global.botTrustRequest,
botAttachRequest: global.botAttachRequest,
webApp: global.webApp,
currentUser,
urlAuth: global.urlAuth,
};
},
)(Main));

View File

@ -19,7 +19,7 @@ const SafeLinkModal: FC<OwnProps> = ({ url }) => {
const lang = useLang();
const handleOpen = useCallback(() => {
window.open(ensureProtocol(url));
window.open(ensureProtocol(url), '_blank', 'noopener');
toggleSafeLinkModal({ url: undefined });
}, [toggleSafeLinkModal, url]);

View File

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

View File

@ -0,0 +1,3 @@
.checkbox {
margin: 1rem 0;
}

View File

@ -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<OwnProps> = ({
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 (
<ConfirmDialog
isOpen={Boolean(urlAuth?.url)}
onClose={handleDismiss}
title={lang('OpenUrlTitle')}
confirmLabel={lang('OpenUrlTitle')}
confirmHandler={handleOpen}
>
{renderText(lang('OpenUrlAlert2', currentAuth?.url), ['links'])}
{domain && (
<Checkbox
checked={isLoginChecked}
label={(
<>
{renderText(
lang('Conversation.OpenBotLinkLogin', [domain, getUserFullName(currentUser)]),
['simple_markdown'],
)}
</>
)}
onCheck={handleLoginChecked}
className={styles.checkbox}
/>
)}
{shouldRequestWriteAccess && (
<Checkbox
checked={isWriteAccessChecked}
label={(
<>
{renderText(
lang('Conversation.OpenBotLinkAllowMessages', getUserFullName(bot)),
['simple_markdown'],
)}
</>
)}
onCheck={setWriteAccessChecked}
disabled={!isLoginChecked}
className={styles.checkbox}
/>
)}
</ConfirmDialog>
);
};
export default memo(UrlAuthModal);

View File

@ -166,6 +166,10 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
});
}, [bot, isInstalled, toggleBotInAttachMenu]);
const handleCloseClick = useCallback(() => {
closeWebApp();
}, [closeWebApp]);
const openBotChat = useCallback(() => {
openChat({
id: bot!.id,
@ -197,7 +201,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
color="translucent"
size="smaller"
ariaLabel={lang('Close')}
onClick={closeWebApp}
onClick={handleCloseClick}
>
<i className="icon-close" />
</Button>
@ -219,7 +223,9 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
</DropdownMenu>
</div>
);
}, [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);

View File

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

View File

@ -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<OwnProps> = ({ message, onClick }) => {
const lang = useLang();
const renderIcon = (button: ApiKeyboardButton) => {
const { type } = button;
switch (type) {
case 'url': {
if (!RE_TME_LINK.test(button.url)) {
return <i className="icon-arrow-right" />;
}
break;
}
case 'urlAuth':
return <i className="icon-arrow-right" />;
case 'buy':
case 'receipt':
return <i className="icon-cart" />;
case 'switchBotInline':
return <i className="icon-share-filled" />;
case 'webView':
case 'simpleWebView':
return <i className="icon-webapp" />;
}
return undefined;
};
return (
<div className="InlineButtons">
{message.inlineButtons!.map((row) => (
@ -31,10 +55,7 @@ const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
onClick={() => onClick({ messageId: message.id, button })}
>
<span className="inline-button-text">{renderText(lang(button.text))}</span>
{['buy', 'receipt'].includes(button.type) && <i className="icon-card" />}
{button.type === 'url' && !RE_TME_LINK.test(button.url) && <i className="icon-arrow-right" />}
{button.type === 'switchBotInline' && <i className="icon-share-filled" />}
{['webView', 'simpleWebView'].includes(button.type) && <i className="icon-webapp" />}
{renderIcon(button)}
</Button>
))}
</div>

View File

@ -108,7 +108,7 @@ const Location: FC<OwnProps> = ({
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) => {

View File

@ -23,6 +23,7 @@ type OwnProps = {
blocking?: boolean;
isLoading?: boolean;
withCheckedCallback?: boolean;
className?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
onCheck?: (isChecked: boolean) => void;
};
@ -39,6 +40,7 @@ const Checkbox: FC<OwnProps> = ({
round,
blocking,
isLoading,
className,
onChange,
onCheck,
}) => {
@ -53,16 +55,17 @@ const Checkbox: FC<OwnProps> = ({
}
}, [onChange, onCheck]);
const className = buildClassName(
const labelClassName = buildClassName(
'Checkbox',
disabled && 'disabled',
round && 'round',
isLoading && 'loading',
blocking && 'blocking',
className,
);
return (
<label className={className} dir={lang.isRtl ? 'rtl' : undefined}>
<label className={labelClassName} dir={lang.isRtl ? 'rtl' : undefined}>
<input
type="checkbox"
id={id}
@ -74,7 +77,7 @@ const Checkbox: FC<OwnProps> = ({
onChange={handleChange}
/>
<div className="Checkbox-main">
<span className="label" dir="auto">{renderText(label)}</span>
<span className="label" dir="auto">{typeof label === 'string' ? renderText(label) : label}</span>
{subLabel && <span className="subLabel" dir="auto">{renderText(subLabel)}</span>}
</div>
{isLoading && <Spinner />}

View File

@ -170,3 +170,56 @@ addActionHandler('changeSessionTtl', async (global, actions, payload) => {
},
});
});
addActionHandler('loadWebAuthorizations', async () => {
const result = await callApi('fetchWebAuthorizations');
if (!result) {
return;
}
setGlobal({
...getGlobal(),
activeWebSessions: {
byHash: result,
orderedHashes: Object.keys(result),
},
});
});
addActionHandler('terminateWebAuthorization', async (global, actions, payload) => {
const { hash } = payload!;
const result = await callApi('terminateWebAuthorization', hash);
if (!result) {
return;
}
global = getGlobal();
const { [hash]: removedSessions, ...newSessions } = global.activeWebSessions.byHash;
setGlobal({
...global,
activeWebSessions: {
byHash: newSessions,
orderedHashes: global.activeWebSessions.orderedHashes.filter((el) => el !== hash),
},
});
});
addActionHandler('terminateAllWebAuthorizations', async (global) => {
const result = await callApi('terminateAllWebAuthorizations');
if (!result) {
return;
}
global = getGlobal();
setGlobal({
...global,
activeWebSessions: {
byHash: {},
orderedHashes: [],
},
});
});

View File

@ -2,12 +2,11 @@ import {
addActionHandler, getActions, getGlobal, setGlobal,
} from '../../index';
import type { ApiChat, ApiContact, ApiUser } from '../../../api/types';
import type {
ApiChat, ApiContact, ApiUrlAuthResult, ApiUser,
} from '../../../api/types';
import type { InlineBotSettings } from '../../../types';
import {
RE_TG_LINK, RE_TME_LINK,
} from '../../../config';
import { callApi } from '../../../api/gramjs';
import {
selectBot,
@ -35,11 +34,7 @@ addActionHandler('clickBotInlineButton', (global, actions, payload) => {
break;
case 'url': {
const { url } = button;
if (url.match(RE_TME_LINK) || url.match(RE_TG_LINK)) {
actions.openTelegramLink({ url });
} else {
actions.toggleSafeLinkModal({ url });
}
actions.openUrl({ url });
break;
}
case 'callback': {
@ -154,6 +149,20 @@ addActionHandler('clickBotInlineButton', (global, actions, payload) => {
});
break;
}
case 'urlAuth': {
const { url } = button;
const chat = selectCurrentChat(global);
if (!chat) {
return;
}
actions.requestBotUrlAuth({
chatId: chat.id,
messageId,
buttonId: button.buttonId,
url,
});
break;
}
}
});
@ -615,6 +624,117 @@ addActionHandler('closeBotAttachRequestModal', (global) => {
};
});
addActionHandler('requestBotUrlAuth', async (global, actions, payload) => {
const {
chatId, buttonId, messageId, url,
} = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const result = await callApi('requestBotUrlAuth', {
chat,
buttonId,
messageId,
});
if (!result) return;
global = getGlobal();
setGlobal({
...global,
urlAuth: {
url,
button: {
buttonId,
messageId,
chatId: chat.id,
},
},
});
handleUrlAuthResult(url, result);
});
addActionHandler('acceptBotUrlAuth', async (global, actions, payload) => {
const { isWriteAllowed } = payload;
if (!global.urlAuth?.button) return;
const {
button, url,
} = global.urlAuth;
const { chatId, messageId, buttonId } = button;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const result = await callApi('acceptBotUrlAuth', {
chat,
messageId,
buttonId,
isWriteAllowed,
});
if (!result) return;
handleUrlAuthResult(url, result);
});
addActionHandler('requestLinkUrlAuth', async (global, actions, payload) => {
const { url } = payload;
const result = await callApi('requestLinkUrlAuth', { url });
if (!result) return;
global = getGlobal();
setGlobal({
...global,
urlAuth: {
url,
},
});
handleUrlAuthResult(url, result);
});
addActionHandler('acceptLinkUrlAuth', async (global, actions, payload) => {
const { isWriteAllowed } = payload;
if (!global.urlAuth?.url) return;
const { url } = global.urlAuth;
const result = await callApi('acceptLinkUrlAuth', { url, isWriteAllowed });
if (!result) return;
handleUrlAuthResult(url, result);
});
addActionHandler('closeUrlAuthModal', (global) => {
return {
...global,
urlAuth: undefined,
};
});
function handleUrlAuthResult(url: string, result: ApiUrlAuthResult) {
if (result.type === 'request') {
const global = getGlobal();
if (!global.urlAuth) return;
const { domain, bot, shouldRequestWriteAccess } = result;
setGlobal({
...global,
urlAuth: {
...global.urlAuth,
request: {
domain,
botId: bot.id,
shouldRequestWriteAccess,
},
},
});
return;
}
const siteUrl = result.type === 'accepted' ? result.url : url;
window.open(siteUrl, '_blank', 'noopener');
getActions().closeUrlAuthModal();
}
async function searchInlineBot({
username,
inlineBotData,
@ -691,7 +811,7 @@ let gameePopups: PopupManager | undefined;
async function answerCallbackButton(chat: ApiChat, messageId: number, data?: string, isGame = false) {
const {
showDialog, showNotification, toggleSafeLinkModal, openGame,
showDialog, showNotification, openUrl, openGame,
} = getActions();
if (isGame) {
@ -731,7 +851,7 @@ async function answerCallbackButton(chat: ApiChat, messageId: number, data?: str
openGame({ url, chatId: chat.id, messageId });
}
} else {
toggleSafeLinkModal({ url });
openUrl({ url });
}
}
}

View File

@ -21,6 +21,8 @@ import { LoadMoreDirection } from '../../../types';
import {
MAX_MEDIA_FILES_FOR_ALBUM,
MESSAGE_LIST_SLICE,
RE_TG_LINK,
RE_TME_LINK,
SERVICE_NOTIFICATIONS_USER_ID,
} from '../../../config';
import { IS_IOS } from '../../../util/environment';
@ -70,6 +72,9 @@ import {
import { debounce, onTickEnd, rafPromise } from '../../../util/schedulers';
import { getMessageOriginalId, isServiceNotificationMessage } from '../../helpers';
import { getTranslation } from '../../../util/langProvider';
import { ensureProtocol } from '../../../util/ensureProtocol';
const AUTOLOGIN_TOKEN_KEY = 'autologin_token';
const uploadProgressCallbacks = new Map<number, ApiOnProgress>();
@ -1157,6 +1162,38 @@ addActionHandler('readAllMentions', (global) => {
});
});
addActionHandler('openUrl', (global, actions, payload) => {
const { url, shouldSkipModal } = payload;
const urlWithProtocol = ensureProtocol(url)!;
if (urlWithProtocol.match(RE_TME_LINK) || urlWithProtocol.match(RE_TG_LINK)) {
actions.openTelegramLink({ url });
return;
}
const { appConfig } = global;
if (appConfig) {
const parsedUrl = new URL(urlWithProtocol);
if (appConfig.autologinDomains.includes(parsedUrl.hostname)) {
parsedUrl.searchParams.set(AUTOLOGIN_TOKEN_KEY, appConfig.autologinToken);
window.open(parsedUrl.href, '_blank', 'noopener');
return;
}
if (appConfig.urlAuthDomains.includes(parsedUrl.hostname)) {
actions.requestLinkUrlAuth({ url });
return;
}
}
if (!shouldSkipModal) {
actions.toggleSafeLinkModal({ url: urlWithProtocol });
} else {
window.open(urlWithProtocol, '_blank', 'noopener');
}
});
function countSortedIds(ids: number[], from: number, to: number) {
let count = 0;

View File

@ -252,6 +252,13 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
orderedHashes: [],
};
}
if (!cached.activeWebSessions) {
cached.activeWebSessions = {
byHash: {},
orderedHashes: [],
};
}
}
function updateCache() {

View File

@ -143,6 +143,11 @@ export const INITIAL_STATE: GlobalState = {
orderedHashes: [],
},
activeWebSessions: {
byHash: {},
orderedHashes: [],
},
settings: {
byKey: {
theme: 'light',

View File

@ -37,6 +37,7 @@ import type {
ApiThemeParameters,
ApiAttachMenuBot,
ApiPhoneCall,
ApiWebSession,
} from '../api/types';
import type {
FocusDirection,
@ -490,6 +491,11 @@ export type GlobalState = {
ttlDays?: number;
};
activeWebSessions: {
byHash: Record<string, ApiWebSession>;
orderedHashes: string[];
};
settings: {
byKey: ISettings;
loadedWallpapers?: ApiWallpaper[];
@ -589,6 +595,20 @@ export type GlobalState = {
hash?: string;
bots: Record<string, ApiAttachMenuBot>;
};
urlAuth?: {
button?: {
chatId: string;
messageId: number;
buttonId: number;
};
request?: {
domain: string;
botId: string;
shouldRequestWriteAccess?: boolean;
};
url: string;
};
};
export type CallSound = (
@ -637,7 +657,7 @@ export interface ActionPayloads {
text: string;
};
resetOpenChatWithText: {};
resetOpenChatWithText: never;
// Messages
setEditingDraft: {
@ -657,10 +677,10 @@ export interface ActionPayloads {
animateUnreadReaction: {
messageIds: number[];
};
focusNextReaction: {};
focusNextMention: {};
readAllReactions: {};
readAllMentions: {};
focusNextReaction: never;
focusNextMention: never;
readAllReactions: never;
readAllMentions: never;
markMentionsRead: {
messageIds: number[];
};
@ -677,7 +697,7 @@ export interface ActionPayloads {
playbackRate?: number;
isMuted?: boolean;
};
closeMediaViewer: {};
closeMediaViewer: never;
setMediaViewerVolume: {
volume: number;
};
@ -697,7 +717,7 @@ export interface ActionPayloads {
playbackRate?: number;
isMuted?: boolean;
};
closeAudioPlayer: {};
closeAudioPlayer: never;
setAudioPlayerVolume: {
volume: number;
};
@ -712,7 +732,7 @@ export interface ActionPayloads {
};
// Downloads
downloadSelectedMessages: {};
downloadSelectedMessages: never;
downloadMessageMedia: {
message: ApiMessage;
};
@ -787,14 +807,14 @@ export interface ActionPayloads {
isSamePeer?: boolean;
};
resetSwitchBotInline: {};
resetSwitchBotInline: never;
openGame: {
url: string;
chatId: string;
messageId: number;
};
closeGame: {};
closeGame: never;
requestWebView: {
url?: string;
@ -819,9 +839,9 @@ export interface ActionPayloads {
buttonText: string;
theme?: ApiThemeParameters;
};
closeWebApp: {};
closeWebApp: never;
cancelBotTrustRequest: {};
cancelBotTrustRequest: never;
markBotTrusted: {
botId: string;
};
@ -852,11 +872,52 @@ export interface ActionPayloads {
startParam?: string;
};
requestBotUrlAuth: {
chatId: string;
messageId: number;
buttonId: number;
url: string;
};
acceptBotUrlAuth: {
isWriteAllowed?: boolean;
};
requestLinkUrlAuth: {
url: string;
};
acceptLinkUrlAuth: {
isWriteAllowed?: boolean;
};
// Settings
loadAuthorizations: never;
terminateAuthorization: {
hash: string;
};
terminateAllAuthorizations: never;
loadWebAuthorizations: never;
terminateWebAuthorization: {
hash: string;
};
terminateAllWebAuthorizations: never;
// Misc
openPollModal: {
isQuiz?: boolean;
};
closePollModal: {};
closePollModal: never;
openUrl: {
url: string;
shouldSkipModal?: boolean;
};
toggleSafeLinkModal: {
url?: string;
};
closeUrlAuthModal: never;
// Calls
requestCall: {
@ -864,17 +925,17 @@ export interface ActionPayloads {
isVideo?: boolean;
};
sendSignalingData: P2pMessage;
hangUp: {};
acceptCall: {};
hangUp: never;
acceptCall: never;
setCallRating: {
rating: number;
comment: string;
};
closeCallRatingModal: {};
closeCallRatingModal: never;
playGroupCallSound: {
sound: CallSound;
};
connectToActivePhoneCall: {};
connectToActivePhoneCall: never;
// Passcode
setPasscode: { passcode: string };
@ -959,7 +1020,6 @@ export type NonTypedActionNames = (
'setSettingOption' | 'loadPasswordInfo' | 'clearTwoFaError' |
'updatePassword' | 'updateRecoveryEmail' | 'clearPassword' | 'provideTwoFaEmailCode' | 'checkPassword' |
'loadBlockedContacts' | 'blockContact' | 'unblockContact' |
'loadAuthorizations' | 'terminateAuthorization' | 'terminateAllAuthorizations' |
'loadNotificationSettings' | 'updateContactSignUpNotification' | 'updateNotificationSettings' |
'updateWebNotificationSettings' | 'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' |
'setPrivacySettings' | 'loadNotificationExceptions' | 'setThemeSettings' | 'updateIsOnline' |
@ -987,7 +1047,7 @@ export type NonTypedActionNames = (
'loadMoreGroupCallParticipants' | 'connectToActiveGroupCall' |
// stats
'loadStatistics' | 'loadMessageStatistics' | 'loadStatisticsAsyncGraph'
);
);
const typed = typify<GlobalState, ActionPayloads, NonTypedActionNames>();
export type GlobalActions = ReturnType<typeof typed.getActions>;

View File

@ -1023,6 +1023,9 @@ account.updatePasswordSettings#a59b102f password:InputCheckPasswordSRP new_setti
account.sendConfirmPhoneCode#1b3faa88 hash:string settings:CodeSettings = auth.SentCode;
account.confirmPhone#5f2178c3 phone_code_hash:string phone_code:string = Bool;
account.getTmpPassword#449e0b51 password:InputCheckPasswordSRP period:int = account.TmpPassword;
account.getWebAuthorizations#182e6d6f = account.WebAuthorizations;
account.resetWebAuthorization#2d01b9ef hash:long = Bool;
account.resetWebAuthorizations#682d2594 = Bool;
account.sendVerifyPhoneCode#a5a356f9 phone_number:string settings:CodeSettings = auth.SentCode;
account.confirmPasswordEmail#8fdf1920 code:string = Bool;
account.getContactSignUpNotification#9f07c728 = Bool;
@ -1115,6 +1118,8 @@ messages.getOnlines#6e2be050 peer:InputPeer = ChatOnlines;
messages.editChatAbout#def60797 peer:InputPeer about:string = Bool;
messages.editChatDefaultBannedRights#a5866b41 peer:InputPeer banned_rights:ChatBannedRights = Updates;
messages.getEmojiKeywordsDifference#1508b6af lang_code:string from_version:int = EmojiKeywordsDifference;
messages.requestUrlAuth#198fb446 flags:# peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult;
messages.acceptUrlAuth#b12c7125 flags:# write_allowed:flags.0?true peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult;
messages.hidePeerSettingsBar#4facb138 peer:InputPeer = Bool;
messages.getScheduledHistory#f516760b peer:InputPeer hash:long = messages.Messages;
messages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector<int> = Updates;

View File

@ -35,6 +35,9 @@
"account.sendConfirmPhoneCode",
"account.confirmPhone",
"account.getTmpPassword",
"account.getWebAuthorizations",
"account.resetWebAuthorization",
"account.resetWebAuthorizations",
"account.sendVerifyPhoneCode",
"account.confirmPasswordEmail",
"account.getContactSignUpNotification",
@ -131,6 +134,8 @@
"messages.editChatAbout",
"messages.editChatDefaultBannedRights",
"messages.getEmojiKeywordsDifference",
"messages.requestUrlAuth",
"messages.acceptUrlAuth",
"messages.hidePeerSettingsBar",
"messages.getScheduledHistory",
"messages.sendScheduledMessages",

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,9 @@
.icon-volume-3:before {
content: "\e991";
}
.icon-web:before {
content: "\e99b";
}
.icon-key:before {
content: "\e99a";
}

View File

@ -214,6 +214,7 @@ export enum SettingsScreens {
TwoFaRecoveryEmailCode,
TwoFaCongratulations,
QuickReaction,
ActiveWebsites,
PasscodeDisabled,
PasscodeNewPasscode,
PasscodeNewPasscodeConfirm,