ChatList: Display unconfirmed sessions (#3887)

This commit is contained in:
Alexander Zinchuk 2023-10-10 13:35:14 +02:00
parent a21019c340
commit 0d843112fa
17 changed files with 185 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -76,6 +76,7 @@ export interface ApiSession {
region: string;
areCallsEnabled: boolean;
areSecretChatsEnabled: boolean;
isUnconfirmed?: true;
}
export interface ApiWebSession {

View File

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

View File

@ -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<string, ApiSession>;
};
const SAVED_MESSAGES_HOTKEY = '0';
@ -77,6 +78,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
hasArchivedStories,
archiveSettings,
isStoryRibbonShown,
sessions,
}) => {
const {
loadChatFolders,
@ -299,6 +301,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
onLeftColumnContentChange={onLeftColumnContentChange}
canDisplayArchive={(hasArchivedChats || hasArchivedStories) && !archiveSettings.isHidden}
archiveSettings={archiveSettings}
sessions={sessions}
/>
);
}
@ -356,6 +359,9 @@ export default memo(withGlobal<OwnProps>(
archived: archivedStories,
},
},
activeSessions: {
byHash: sessions,
},
currentUserId,
archiveSettings,
} = global;
@ -376,6 +382,7 @@ export default memo(withGlobal<OwnProps>(
maxChatLists: selectCurrentLimit(global, 'chatlistJoined'),
archiveSettings,
isStoryRibbonShown,
sessions,
};
},
)(ChatFolders));

View File

@ -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<string, ApiSession>;
foldersDispatch: FolderEditDispatch;
onSettingsScreenSelect: (screen: SettingsScreens) => void;
onLeftColumnContentChange: (content: LeftColumnContent) => void;
@ -60,6 +64,7 @@ const ChatList: FC<OwnProps> = ({
isForumPanelOpen,
canDisplayArchive,
archiveSettings,
sessions,
foldersDispatch,
onSettingsScreenSelect,
onLeftColumnContentChange,
@ -73,13 +78,15 @@ const ChatList: FC<OwnProps> = ({
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(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<OwnProps> = ({
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 <Alt>+<Up/Down> to navigate between chats
useHotkeys(isActive && orderedIds?.length ? {
'Alt+ArrowUp': (e: KeyboardEvent) => {
@ -189,7 +208,7 @@ const ChatList: FC<OwnProps> = ({
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 (
<Chat
@ -217,10 +236,17 @@ const ChatList: FC<OwnProps> = ({
preloadBackwards={CHAT_LIST_SLICE}
withAbsolutePositioning
beforeChildren={renderedOverflowTrigger}
maxHeight={chatsHeight + archiveHeight}
maxHeight={chatsHeight + archiveHeight + unconfirmedSessionHeight}
onLoadMore={getMore}
onDragLeave={handleDragLeave}
>
{shouldShowUnconfirmedSessions && (
<UnconfirmedSession
key="unconfirmed"
sessions={sessions!}
onHeightChange={setUnconfirmedSessionHeight}
/>
)}
{shouldDisplayArchive && (
<Archive
key="archive"

View File

@ -0,0 +1,32 @@
/* stylelint-disable-next-line */
@value minimized from "./Archive.module.scss";
.root {
padding: 0.5rem;
text-align: center;
& + :global(.minimized) {
margin-top: 0 !important;
}
}
.title {
font-size: 1rem;
}
.info {
font-size: 0.875rem;
color: var(--color-text-secondary);
margin-bottom: 0.5rem;
}
.buttons {
display: flex;
justify-content: space-around;
column-gap: 1rem;
}
.button {
font-size: 0.875rem;
font-weight: 500;
}

View File

@ -0,0 +1,72 @@
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type { ApiSession } from '../../../api/types';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useResizeObserver from '../../../hooks/useResizeObserver';
import Button from '../../ui/Button';
import styles from './UnconfirmedSession.module.scss';
type OwnProps = {
sessions: Record<string, ApiSession>;
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<HTMLDivElement>(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 (
<div className={styles.root} ref={ref}>
<h2 className={styles.title}>{lang('UnconfirmedAuthTitle')}</h2>
<p className={styles.info}>
{lang('UnconfirmedAuthSingle', locationString)}
</p>
<div className={styles.buttons}>
<Button fluid isText size="smaller" className={styles.button} onClick={handleAccept}>
{lang('UnconfirmedAuthConfirm')}
</Button>
<Button fluid isText size="smaller" color="danger" onClick={handleReject} className={styles.button}>
{lang('UnconfirmedAuthDeny')}
</Button>
</div>
</div>
);
};
export default memo(UnconfirmedSession);

View File

@ -40,7 +40,6 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
}) => {
const {
loadProfilePhotos,
loadAuthorizations,
openPremiumModal,
openSupportChat,
openUrl,
@ -61,10 +60,6 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
onBack: onReset,
});
useEffect(() => {
loadAuthorizations();
}, []);
const handleOpenSupport = useLastCallback(() => {
openSupportChat();
closeSupportDialog();

View File

@ -68,7 +68,6 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
const {
loadPrivacySettings,
loadBlockedUsers,
loadAuthorizations,
loadContentSettings,
updateContentSettings,
loadGlobalPrivacySettings,
@ -80,7 +79,6 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
useEffect(() => {
loadBlockedUsers();
loadAuthorizations();
loadPrivacySettings();
loadContentSettings();
loadWebAuthorizations();

View File

@ -254,6 +254,7 @@ const Main: FC<OwnProps & StateProps> = ({
loadFeaturedEmojiStickers,
setIsElectronUpdateAvailable,
loadPremiumSetStickers,
loadAuthorizations,
} = getActions();
if (DEBUG && !DEBUG_isLogged) {
@ -327,6 +328,7 @@ const Main: FC<OwnProps & StateProps> = ({
loadTopReactions();
loadRecentReactions();
loadFeaturedEmojiStickers();
loadAuthorizations();
}
}, [isMasterTab, isSynced]);

View File

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

View File

@ -135,11 +135,14 @@ addActionHandler('terminateAllAuthorizations', async (global): Promise<void> =>
});
addActionHandler('changeSessionSettings', async (global, actions, payload): Promise<void> => {
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 }),
},
},
},

View File

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

View File

@ -1682,6 +1682,7 @@ export interface ActionPayloads {
hash: string;
areCallsEnabled?: boolean;
areSecretChatsEnabled?: boolean;
isConfirmed?: boolean;
};
changeSessionTtl: {
days: number;