From 0d843112fa6fe559d12a3777c490e4a29db9e0fd Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 10 Oct 2023 13:35:14 +0200 Subject: [PATCH] ChatList: Display unconfirmed sessions (#3887) --- src/api/gramjs/apiBuilders/appConfig.ts | 1 + src/api/gramjs/apiBuilders/misc.ts | 1 + src/api/gramjs/methods/account.ts | 5 +- src/api/gramjs/updater.ts | 9 +++ src/api/types/misc.ts | 1 + src/api/types/updates.ts | 11 ++- src/components/left/main/ChatFolders.tsx | 9 ++- src/components/left/main/ChatList.tsx | 38 ++++++++-- .../left/main/UnconfirmedSession.module.scss | 32 +++++++++ .../left/main/UnconfirmedSession.tsx | 72 +++++++++++++++++++ src/components/left/settings/SettingsMain.tsx | 5 -- .../left/settings/SettingsPrivacy.tsx | 2 - src/components/main/Main.tsx | 2 + src/config.ts | 3 +- src/global/actions/api/accounts.ts | 6 +- src/global/actions/apiUpdaters/misc.ts | 6 ++ src/global/types.ts | 1 + 17 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 src/components/left/main/UnconfirmedSession.module.scss create mode 100644 src/components/left/main/UnconfirmedSession.tsx diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts index bbc0b7288..ac688885f 100644 --- a/src/api/gramjs/apiBuilders/appConfig.ts +++ b/src/api/gramjs/apiBuilders/appConfig.ts @@ -44,6 +44,7 @@ export interface GramJsAppConfig extends LimitsConfig { default_emoji_statuses_stickerset_id: string; hidden_members_group_size_min: number; autoarchive_setting_available: boolean; + authorization_autoconfirm_period: number; // Forums topics_pinned_limit: number; // Stories diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index 6335a4815..fb178e321 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -43,6 +43,7 @@ export function buildApiSession(session: GramJs.Authorization): ApiSession { hash: String(session.hash), areCallsEnabled: !session.callRequestsDisabled, areSecretChatsEnabled: !session.encryptedRequestsDisabled, + isUnconfirmed: session.unconfirmed, ...pick(session, [ 'deviceModel', 'platform', 'systemVersion', 'appName', 'appVersion', 'dateCreated', 'dateActive', 'ip', 'country', 'region', diff --git a/src/api/gramjs/methods/account.ts b/src/api/gramjs/methods/account.ts index 45c86df91..88602c189 100644 --- a/src/api/gramjs/methods/account.ts +++ b/src/api/gramjs/methods/account.ts @@ -46,14 +46,15 @@ export async function reportProfilePhoto({ } export async function changeSessionSettings({ - hash, areCallsEnabled, areSecretChatsEnabled, + hash, areCallsEnabled, areSecretChatsEnabled, isConfirmed, }: { - hash: string; areCallsEnabled?: boolean; areSecretChatsEnabled?: boolean; + hash: string; areCallsEnabled?: boolean; areSecretChatsEnabled?: boolean; isConfirmed?: boolean; }) { const result = await invokeRequest(new GramJs.account.ChangeAuthorizationSettings({ hash: BigInt(hash), ...(areCallsEnabled !== undefined ? { callRequestsDisabled: !areCallsEnabled } : undefined), ...(areSecretChatsEnabled !== undefined ? { encryptedRequestsDisabled: !areSecretChatsEnabled } : undefined), + ...(isConfirmed && { confirmed: isConfirmed }), })); return result; diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index caaba42e7..3f9e3404f 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -1108,6 +1108,15 @@ export function updater(update: Update) { onUpdate({ '@type': 'updateAttachMenuBots', }); + } else if (update instanceof GramJs.UpdateNewAuthorization) { + onUpdate({ + '@type': 'updateNewAuthorization', + hash: update.hash.toString(), + date: update.date, + device: update.device, + location: update.location, + isUnconfirmed: update.unconfirmed, + }); } else if (DEBUG) { const params = typeof update === 'object' && 'className' in update ? update.className : update; log('UNEXPECTED UPDATE', params); diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index ef2536ea5..6b0301cfb 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -76,6 +76,7 @@ export interface ApiSession { region: string; areCallsEnabled: boolean; areSecretChatsEnabled: boolean; + isUnconfirmed?: true; } export interface ApiWebSession { diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 80bec10b7..f54dcd5a4 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -665,6 +665,15 @@ export type ApiUpdateAttachMenuBots = { '@type': 'updateAttachMenuBots'; }; +export type ApiUpdateNewAuthorization = { + '@type': 'updateNewAuthorization'; + hash: string; + isUnconfirmed?: true; + date?: number; + device?: string; + location?: string; +}; + export type ApiUpdate = ( ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | @@ -693,7 +702,7 @@ export type ApiUpdate = ( ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses | ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory | ApiUpdateSentStoryReaction | ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages | - ApiUpdateStealthMode | ApiUpdateAttachMenuBots + ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization ); export type OnApiUpdate = (update: ApiUpdate) => void; diff --git a/src/components/left/main/ChatFolders.tsx b/src/components/left/main/ChatFolders.tsx index 0fe6d8f01..21f19d8c4 100644 --- a/src/components/left/main/ChatFolders.tsx +++ b/src/components/left/main/ChatFolders.tsx @@ -4,7 +4,7 @@ import React, { } from '../../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../../global'; -import type { ApiChatFolder, ApiChatlistExportedInvite } from '../../../api/types'; +import type { ApiChatFolder, ApiChatlistExportedInvite, ApiSession } from '../../../api/types'; import type { GlobalState } from '../../../global/types'; import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer'; import type { LeftColumnContent, SettingsScreens } from '../../../types'; @@ -53,6 +53,7 @@ type StateProps = { hasArchivedStories?: boolean; archiveSettings: GlobalState['archiveSettings']; isStoryRibbonShown?: boolean; + sessions?: Record; }; const SAVED_MESSAGES_HOTKEY = '0'; @@ -77,6 +78,7 @@ const ChatFolders: FC = ({ hasArchivedStories, archiveSettings, isStoryRibbonShown, + sessions, }) => { const { loadChatFolders, @@ -299,6 +301,7 @@ const ChatFolders: FC = ({ onLeftColumnContentChange={onLeftColumnContentChange} canDisplayArchive={(hasArchivedChats || hasArchivedStories) && !archiveSettings.isHidden} archiveSettings={archiveSettings} + sessions={sessions} /> ); } @@ -356,6 +359,9 @@ export default memo(withGlobal( archived: archivedStories, }, }, + activeSessions: { + byHash: sessions, + }, currentUserId, archiveSettings, } = global; @@ -376,6 +382,7 @@ export default memo(withGlobal( maxChatLists: selectCurrentLimit(global, 'chatlistJoined'), archiveSettings, isStoryRibbonShown, + sessions, }; }, )(ChatFolders)); diff --git a/src/components/left/main/ChatList.tsx b/src/components/left/main/ChatList.tsx index 7779b5ddf..49d46f69f 100644 --- a/src/components/left/main/ChatList.tsx +++ b/src/components/left/main/ChatList.tsx @@ -1,9 +1,10 @@ import type { FC } from '../../../lib/teact/teact'; import React, { - memo, useEffect, useRef, + memo, useEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; +import type { ApiSession } from '../../../api/types'; import type { GlobalState } from '../../../global/types'; import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer'; import type { SettingsScreens } from '../../../types'; @@ -15,9 +16,11 @@ import { ARCHIVED_FOLDER_ID, CHAT_HEIGHT_PX, CHAT_LIST_SLICE, + FRESH_AUTH_PERIOD, } from '../../../config'; import buildClassName from '../../../util/buildClassName'; import { getOrderKey, getPinnedChatsCount } from '../../../util/folderManager'; +import { getServerTime } from '../../../util/serverTime'; import { IS_APP, IS_MAC_OS } from '../../../util/windowEnvironment'; import useUserStoriesPolling from '../../../hooks/polling/useUserStoriesPolling'; @@ -35,6 +38,7 @@ import Loading from '../../ui/Loading'; import Archive from './Archive'; import Chat from './Chat'; import EmptyFolder from './EmptyFolder'; +import UnconfirmedSession from './UnconfirmedSession'; type OwnProps = { folderType: 'all' | 'archived' | 'folder'; @@ -43,7 +47,7 @@ type OwnProps = { canDisplayArchive?: boolean; archiveSettings: GlobalState['archiveSettings']; isForumPanelOpen?: boolean; - className?: string; + sessions?: Record; foldersDispatch: FolderEditDispatch; onSettingsScreenSelect: (screen: SettingsScreens) => void; onLeftColumnContentChange: (content: LeftColumnContent) => void; @@ -60,6 +64,7 @@ const ChatList: FC = ({ isForumPanelOpen, canDisplayArchive, archiveSettings, + sessions, foldersDispatch, onSettingsScreenSelect, onLeftColumnContentChange, @@ -73,13 +78,15 @@ const ChatList: FC = ({ // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); const shouldIgnoreDragRef = useRef(false); + const [unconfirmedSessionHeight, setUnconfirmedSessionHeight] = useState(0); const isArchived = folderType === 'archived'; + const isAllFolder = folderType === 'all'; const resolvedFolderId = ( - folderType === 'all' ? ALL_FOLDER_ID : isArchived ? ARCHIVED_FOLDER_ID : folderId! + isAllFolder ? ALL_FOLDER_ID : isArchived ? ARCHIVED_FOLDER_ID : folderId! ); - const shouldDisplayArchive = folderType === 'all' && canDisplayArchive; + const shouldDisplayArchive = isAllFolder && canDisplayArchive; const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId); useUserStoriesPolling(orderedIds); @@ -92,6 +99,18 @@ const ChatList: FC = ({ const [viewportIds, getMore] = useInfiniteScroll(undefined, orderedIds, undefined, CHAT_LIST_SLICE); + const shouldShowUnconfirmedSessions = useMemo(() => { + const sessionsArray = Object.values(sessions || {}); + const current = sessionsArray.find((session) => session.isCurrent); + if (!current || getServerTime() - current.dateCreated < FRESH_AUTH_PERIOD) return false; + + return isAllFolder && sessionsArray.some((session) => session.isUnconfirmed); + }, [isAllFolder, sessions]); + + useEffect(() => { + if (!shouldShowUnconfirmedSessions) setUnconfirmedSessionHeight(0); + }, [shouldShowUnconfirmedSessions]); + // Support + to navigate between chats useHotkeys(isActive && orderedIds?.length ? { 'Alt+ArrowUp': (e: KeyboardEvent) => { @@ -189,7 +208,7 @@ const ChatList: FC = ({ return viewportIds!.map((id, i) => { const isPinned = viewportOffset + i < pinnedCount; - const offsetTop = archiveHeight + (viewportOffset + i) * CHAT_HEIGHT_PX; + const offsetTop = unconfirmedSessionHeight + archiveHeight + (viewportOffset + i) * CHAT_HEIGHT_PX; return ( = ({ preloadBackwards={CHAT_LIST_SLICE} withAbsolutePositioning beforeChildren={renderedOverflowTrigger} - maxHeight={chatsHeight + archiveHeight} + maxHeight={chatsHeight + archiveHeight + unconfirmedSessionHeight} onLoadMore={getMore} onDragLeave={handleDragLeave} > + {shouldShowUnconfirmedSessions && ( + + )} {shouldDisplayArchive && ( ; + onHeightChange: (height: number) => void; +}; + +const UnconfirmedSession = ({ sessions, onHeightChange } : OwnProps) => { + const { changeSessionSettings, terminateAuthorization, showNotification } = getActions(); + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + const lang = useLang(); + + useResizeObserver(ref, (entry) => { + const height = entry.borderBoxSize?.[0]?.blockSize || entry.contentRect.height; + onHeightChange(height); + }); + + const firstUnconfirmed = useMemo(() => { + return Object.values(sessions).sort((a, b) => b.dateCreated - a.dateCreated) + .find((session) => session.isUnconfirmed)!; + }, [sessions]); + + const locationString = useMemo(() => { + return [firstUnconfirmed.deviceModel, firstUnconfirmed.region, firstUnconfirmed.country].filter(Boolean).join(', '); + }, [firstUnconfirmed]); + + const handleAccept = useLastCallback(() => { + changeSessionSettings({ + hash: firstUnconfirmed.hash, + isConfirmed: true, + }); + }); + + const handleReject = useLastCallback(() => { + terminateAuthorization({ hash: firstUnconfirmed.hash }); + showNotification({ + title: lang('UnconfirmedAuthDeniedTitle', 1), + message: lang('UnconfirmedAuthDeniedMessageSingle', locationString), + }); + }); + + return ( +
+

{lang('UnconfirmedAuthTitle')}

+

+ {lang('UnconfirmedAuthSingle', locationString)} +

+
+ + +
+
+ ); +}; + +export default memo(UnconfirmedSession); diff --git a/src/components/left/settings/SettingsMain.tsx b/src/components/left/settings/SettingsMain.tsx index 0371fdd14..77dd122ee 100644 --- a/src/components/left/settings/SettingsMain.tsx +++ b/src/components/left/settings/SettingsMain.tsx @@ -40,7 +40,6 @@ const SettingsMain: FC = ({ }) => { const { loadProfilePhotos, - loadAuthorizations, openPremiumModal, openSupportChat, openUrl, @@ -61,10 +60,6 @@ const SettingsMain: FC = ({ onBack: onReset, }); - useEffect(() => { - loadAuthorizations(); - }, []); - const handleOpenSupport = useLastCallback(() => { openSupportChat(); closeSupportDialog(); diff --git a/src/components/left/settings/SettingsPrivacy.tsx b/src/components/left/settings/SettingsPrivacy.tsx index 7be17e810..980774b80 100644 --- a/src/components/left/settings/SettingsPrivacy.tsx +++ b/src/components/left/settings/SettingsPrivacy.tsx @@ -68,7 +68,6 @@ const SettingsPrivacy: FC = ({ const { loadPrivacySettings, loadBlockedUsers, - loadAuthorizations, loadContentSettings, updateContentSettings, loadGlobalPrivacySettings, @@ -80,7 +79,6 @@ const SettingsPrivacy: FC = ({ useEffect(() => { loadBlockedUsers(); - loadAuthorizations(); loadPrivacySettings(); loadContentSettings(); loadWebAuthorizations(); diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 71fa88656..08a44be77 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -254,6 +254,7 @@ const Main: FC = ({ loadFeaturedEmojiStickers, setIsElectronUpdateAvailable, loadPremiumSetStickers, + loadAuthorizations, } = getActions(); if (DEBUG && !DEBUG_isLogged) { @@ -327,6 +328,7 @@ const Main: FC = ({ loadTopReactions(); loadRecentReactions(); loadFeaturedEmojiStickers(); + loadAuthorizations(); } }, [isMasterTab, isSynced]); diff --git a/src/config.ts b/src/config.ts index 7c8a359fc..065faeff5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -51,7 +51,7 @@ export const CUSTOM_EMOJI_PREVIEW_CACHE_DISABLED = false; export const CUSTOM_EMOJI_PREVIEW_CACHE_NAME = 'tt-custom-emoji-preview'; export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg'; -export const LANG_CACHE_NAME = 'tt-lang-packs-v23'; +export const LANG_CACHE_NAME = 'tt-lang-packs-v24'; export const ASSET_CACHE_NAME = 'tt-assets'; export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500]; export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global'; @@ -301,6 +301,7 @@ export const MINI_APP_TOS_URL = 'https://telegram.org/tos/mini-apps'; export const GENERAL_TOPIC_ID = 1; export const STORY_EXPIRE_PERIOD = 86400; // 1 day export const STORY_VIEWERS_EXPIRE_PERIOD = 86400; // 1 day +export const FRESH_AUTH_PERIOD = 86400; // 1 day export const LIGHT_THEME_BG_COLOR = '#99BA92'; export const DARK_THEME_BG_COLOR = '#0F0F0F'; diff --git a/src/global/actions/api/accounts.ts b/src/global/actions/api/accounts.ts index a73ebaadf..86cc8e863 100644 --- a/src/global/actions/api/accounts.ts +++ b/src/global/actions/api/accounts.ts @@ -135,11 +135,14 @@ addActionHandler('terminateAllAuthorizations', async (global): Promise => }); addActionHandler('changeSessionSettings', async (global, actions, payload): Promise => { - const { hash, areCallsEnabled, areSecretChatsEnabled } = payload; + const { + hash, areCallsEnabled, areSecretChatsEnabled, isConfirmed, + } = payload; const result = await callApi('changeSessionSettings', { hash, areCallsEnabled, areSecretChatsEnabled, + isConfirmed, }); if (!result) { @@ -157,6 +160,7 @@ addActionHandler('changeSessionSettings', async (global, actions, payload): Prom ...global.activeSessions.byHash[hash], ...(areCallsEnabled !== undefined ? { areCallsEnabled } : undefined), ...(areSecretChatsEnabled !== undefined ? { areSecretChatsEnabled } : undefined), + ...(isConfirmed && { isUnconfirmed: undefined }), }, }, }, diff --git a/src/global/actions/apiUpdaters/misc.ts b/src/global/actions/apiUpdaters/misc.ts index f1cdad2e0..18f5cfaf1 100644 --- a/src/global/actions/apiUpdaters/misc.ts +++ b/src/global/actions/apiUpdaters/misc.ts @@ -41,6 +41,12 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { actions.loadConfig(); break; + case 'updateNewAuthorization': { + // Load more info about this session + actions.loadAuthorizations(); + break; + } + case 'updateFavoriteStickers': actions.loadFavoriteStickers(); break; diff --git a/src/global/types.ts b/src/global/types.ts index 27ccd19c2..172a80e75 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -1682,6 +1682,7 @@ export interface ActionPayloads { hash: string; areCallsEnabled?: boolean; areSecretChatsEnabled?: boolean; + isConfirmed?: boolean; }; changeSessionTtl: { days: number;