From d7bcf7a7690e8beaf72a97a1fa47b31833166c08 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 26 Apr 2022 17:08:47 +0200 Subject: [PATCH] Settings: Active Sessions 2.0 (#1843) --- src/api/gramjs/apiBuilders/misc.ts | 1 + src/api/gramjs/methods/account.ts | 26 ++++ src/api/gramjs/methods/index.ts | 2 +- src/api/gramjs/methods/settings.ts | 5 +- src/api/types/misc.ts | 1 + .../SettingsActiveSession.module.scss | 52 +++++++ .../left/settings/SettingsActiveSession.tsx | 111 ++++++++++++++ .../left/settings/SettingsActiveSessions.tsx | 141 ++++++++++++++---- src/components/left/settings/SettingsMain.tsx | 2 +- src/global/actions/api/accounts.ts | 110 +++++++++++++- src/global/actions/api/settings.ts | 42 ------ src/global/cache.ts | 7 + src/global/initialState.ts | 5 +- src/global/types.ts | 16 +- src/lib/gramjs/tl/apiTl.js | 2 + src/lib/gramjs/tl/static/api.json | 2 + 16 files changed, 444 insertions(+), 81 deletions(-) create mode 100644 src/components/left/settings/SettingsActiveSession.module.scss create mode 100644 src/components/left/settings/SettingsActiveSession.tsx diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index 3a7655f92..802858c73 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -36,6 +36,7 @@ export function buildApiSession(session: GramJs.Authorization): ApiSession { isOfficialApp: Boolean(session.officialApp), isPasswordPending: Boolean(session.passwordPending), hash: String(session.hash), + areCallsEnabled: !session.callRequestsDisabled, ...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 f9bae3b46..1922ff872 100644 --- a/src/api/gramjs/methods/account.ts +++ b/src/api/gramjs/methods/account.ts @@ -1,3 +1,4 @@ +import BigInt from 'big-integer'; import { ApiChat, ApiPhoto, ApiReportReason, ApiUser, } from '../../types'; @@ -41,3 +42,28 @@ export async function reportProfilePhoto({ return result; } + +export async function changeSessionSettings({ + hash, areCallsEnabled, +}: { + hash: string; areCallsEnabled: boolean; +}) { + const result = await invokeRequest(new GramJs.account.ChangeAuthorizationSettings({ + hash: BigInt(hash), + callRequestsDisabled: !areCallsEnabled, + })); + + return result; +} + +export async function changeSessionTtl({ + days, +}: { + days: number; +}) { + const result = await invokeRequest(new GramJs.account.SetAuthorizationTTL({ + authorizationTtlDays: days, + })); + + return result; +} diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 16a32667a..212e22e07 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -3,7 +3,7 @@ export { } from './client'; export { - reportPeer, reportProfilePhoto, + reportPeer, reportProfilePhoto, changeSessionSettings, changeSessionTtl, } from './account'; export { diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index c1af376df..1af6262f8 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -160,7 +160,10 @@ export async function fetchAuthorizations() { return undefined; } - return result.authorizations.map(buildApiSession); + return { + authorizations: buildCollectionByKey(result.authorizations.map(buildApiSession), 'hash'), + ttlDays: result.authorizationTtlDays, + }; } export function terminateAuthorization(hash: string) { diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 96965e9e8..20f9125a1 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -61,6 +61,7 @@ export interface ApiSession { ip: string; country: string; region: string; + areCallsEnabled: boolean; } export interface ApiSessionData { diff --git a/src/components/left/settings/SettingsActiveSession.module.scss b/src/components/left/settings/SettingsActiveSession.module.scss new file mode 100644 index 000000000..9f7ef7b5b --- /dev/null +++ b/src/components/left/settings/SettingsActiveSession.module.scss @@ -0,0 +1,52 @@ +$icons: "android", "apple", "brave", "chrome", "edge", "firefox", "linux", "opera", "safari", "samsung", "ubuntu", "unknown", "vivaldi", "windows", "xbox"; + +@mixin device-icon($icon-name) { + .iconDevice__#{$icon-name} { + background-image: url("../../../assets/devices/#{$icon-name}.svg"); + } +} + +.SettingsActiveSession { + :global(.modal-dialog) { + max-width: 28rem; + } +} + +.iconDevice { + width: 5rem; + height: 5rem; + background-repeat: no-repeat; + background-size: 5rem; + margin: 0 auto 1rem; +} + +@each $icon in $icons { + @include device-icon($icon); +} + +.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; +} + +.actionHeader { + margin-top: 1px; +} + +.actionName { + margin-right: auto; +} diff --git a/src/components/left/settings/SettingsActiveSession.tsx b/src/components/left/settings/SettingsActiveSession.tsx new file mode 100644 index 000000000..a838d3ac5 --- /dev/null +++ b/src/components/left/settings/SettingsActiveSession.tsx @@ -0,0 +1,111 @@ +import React, { FC, memo, useCallback } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import { ApiSession } from '../../../api/types'; + +import { formatDateTimeToString } from '../../../util/dateFormat'; +import useLang from '../../../hooks/useLang'; +import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; +import getSessionIcon from './helpers/getSessionIcon'; +import buildClassName from '../../../util/buildClassName'; + +import ListItem from '../../ui/ListItem'; +import Modal from '../../ui/Modal'; +import Switcher from '../../ui/Switcher'; +import Button from '../../ui/Button'; + +import styles from './SettingsActiveSession.module.scss'; + +type OwnProps = { + isOpen: boolean; + hash?: string; + onClose: () => void; +}; + +type StateProps = { + session?: ApiSession; +}; + +const SettingsActiveSession: FC = ({ + isOpen, session, onClose, +}) => { + const { changeSessionSettings, terminateAuthorization } = getActions(); + const lang = useLang(); + + const renderingSession = useCurrentOrPrev(session, true); + + const handleCallsStateChange = useCallback(() => { + changeSessionSettings({ + hash: session!.hash, + areCallsEnabled: !session?.areCallsEnabled, + }); + }, [changeSessionSettings, session]); + + const handleTerminateSessionClick = useCallback(() => { + terminateAuthorization({ hash: session!.hash }); + onClose(); + }, [onClose, session, terminateAuthorization]); + + if (!renderingSession) { + return undefined; + } + + return ( + +
+

{renderingSession?.deviceModel}

+
+ {formatDateTimeToString(renderingSession.dateActive * 1000, lang.code)} +
+ +
+
{lang('SessionPreview.App')}
+
+ {renderingSession?.appName} {renderingSession?.appVersion}, + {renderingSession?.platform} {renderingSession?.systemVersion} +
+ +
{lang('SessionPreview.Ip')}
+
{renderingSession?.ip}
+ +
{lang('SessionPreview.Location')}
+
{renderingSession && getLocation(renderingSession)}
+
+ +

{lang('SessionPreview.IpDesc')}

+ +

{lang('SessionPreview.AcceptHeader')}

+ + + {lang('SessionPreview.Accept.Calls')} + + + + + + ); +}; + +function getLocation(session: ApiSession) { + return [session.region, session.country].filter(Boolean).join(', '); +} + +export default memo(withGlobal((global, { hash }) => { + return { + session: hash ? global.activeSessions.byHash[hash] : undefined, + }; +})(SettingsActiveSession)); diff --git a/src/components/left/settings/SettingsActiveSessions.tsx b/src/components/left/settings/SettingsActiveSessions.tsx index a633f6c61..03174e7f0 100644 --- a/src/components/left/settings/SettingsActiveSessions.tsx +++ b/src/components/left/settings/SettingsActiveSessions.tsx @@ -1,5 +1,6 @@ +/* eslint-disable react/jsx-no-bind */ import React, { - FC, memo, useCallback, useMemo, + FC, memo, useCallback, useMemo, useState, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; @@ -14,8 +15,10 @@ import getSessionIcon from './helpers/getSessionIcon'; import ListItem from '../../ui/ListItem'; import ConfirmDialog from '../../ui/ConfirmDialog'; +import SettingsActiveSession from './SettingsActiveSession'; import './SettingsActiveSessions.scss'; +import RadioGroup from '../../ui/RadioGroup'; type OwnProps = { isActive?: boolean; @@ -24,21 +27,63 @@ type OwnProps = { }; type StateProps = { - activeSessions: ApiSession[]; + byHash: Record; + orderedHashes: string[]; + ttlDays?: number; }; const SettingsActiveSessions: FC = ({ isActive, onScreenSelect, onReset, - activeSessions, + byHash, + orderedHashes, + ttlDays, }) => { const { terminateAuthorization, terminateAllAuthorizations, + changeSessionTtl, } = getActions(); + const lang = useLang(); const [isConfirmTerminateAllDialogOpen, openConfirmTerminateAllDialog, closeConfirmTerminateAllDialog] = useFlag(); + const [openedSessionHash, setOpenedSessionHash] = useState(); + const [isModalOpen, openModal, closeModal] = useFlag(); + + const autoTerminateValue = useMemo(() => { + if (ttlDays === undefined) { + return undefined; + } + if (ttlDays <= 7) { + return '7'; + } + if (ttlDays <= 30) { + return '30'; + } + if (ttlDays <= 90) { + return '90'; + } + if (ttlDays <= 180) { + return '180'; + } + + return undefined; + }, [ttlDays]); + + const AUTO_TERMINATE_OPTIONS = useMemo(() => [{ + label: lang('Weeks', 1, 'i'), + value: '7', + }, { + label: lang('Months', 1, 'i'), + value: '30', + }, { + label: lang('Months', 3, 'i'), + value: '90', + }, { + label: lang('Months', 6, 'i'), + value: '180', + }], [lang]); const handleTerminateSessionClick = useCallback((hash: string) => { terminateAuthorization({ hash }); @@ -49,15 +94,30 @@ const SettingsActiveSessions: FC = ({ terminateAllAuthorizations(); }, [closeConfirmTerminateAllDialog, terminateAllAuthorizations]); + const handleOpenSessionModal = useCallback((hash: string) => { + setOpenedSessionHash(hash); + openModal(); + }, [openModal]); + + const handleCloseSessionModal = useCallback(() => { + setOpenedSessionHash(undefined); + closeModal(); + }, [closeModal]); + + const handleChangeSessionTtl = useCallback((value: string) => { + changeSessionTtl({ days: Number(value) }); + }, [changeSessionTtl]); + const currentSession = useMemo(() => { - return activeSessions.find((session) => session.isCurrent); - }, [activeSessions]); + const currentSessionHash = orderedHashes.find((hash) => byHash[hash].isCurrent); - const otherSessions = useMemo(() => { - return activeSessions.filter((session) => !session.isCurrent); - }, [activeSessions]); + return currentSessionHash ? byHash[currentSessionHash] : undefined; + }, [byHash, orderedHashes]); - const lang = useLang(); + const otherSessionHashes = useMemo(() => { + return orderedHashes.filter((hash) => !byHash[hash].isCurrent); + }, [byHash, orderedHashes]); + const hasOtherSessions = Boolean(otherSessionHashes.length); useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.ActiveSessions); @@ -78,32 +138,54 @@ const SettingsActiveSessions: FC = ({
- - {lang('TerminateAllSessions')} - + {hasOtherSessions && ( + + {lang('TerminateAllSessions')} + + )} ); } - function renderOtherSessions(sessions: ApiSession[]) { + function renderOtherSessions(sessionHashes: string[]) { return (

{lang('OtherSessions')}

- {sessions.map(renderSession)} + {sessionHashes.map(renderSession)}
); } - function renderSession(session: ApiSession) { + function renderAutoTerminate() { + return ( +
+

+ {lang('TerminateOldSessionHeader')} +

+ +

{lang('IfInactiveFor')}

+ +
+ ); + } + + function renderSession(sessionHash: string) { + const session = byHash[sessionHash]; + return ( = ({ }, }]} icon={`device-${getSessionIcon(session)} icon-device`} + onClick={() => { handleOpenSessionModal(session.hash); }} >
{formatPastTimeShort(lang, session.dateActive * 1000)} @@ -133,17 +216,19 @@ const SettingsActiveSessions: FC = ({ return (
{currentSession && renderCurrentSession(currentSession)} - {otherSessions && renderOtherSessions(otherSessions)} - {otherSessions && ( + {hasOtherSessions && renderOtherSessions(otherSessionHashes)} + {renderAutoTerminate()} + {hasOtherSessions && ( )} +
); }; @@ -153,9 +238,5 @@ function getLocation(session: ApiSession) { } export default memo(withGlobal( - (global): StateProps => { - return { - activeSessions: global.activeSessions, - }; - }, + (global): StateProps => global.activeSessions, )(SettingsActiveSessions)); diff --git a/src/components/left/settings/SettingsMain.tsx b/src/components/left/settings/SettingsMain.tsx index f56d64b65..00274146c 100644 --- a/src/components/left/settings/SettingsMain.tsx +++ b/src/components/left/settings/SettingsMain.tsx @@ -127,7 +127,7 @@ export default memo(withGlobal( const { currentUserId, lastSyncTime } = global; return { - sessionCount: global.activeSessions.length, + sessionCount: global.activeSessions.orderedHashes.length, currentUser: currentUserId ? selectUser(global, currentUserId) : undefined, lastSyncTime, }; diff --git a/src/global/actions/api/accounts.ts b/src/global/actions/api/accounts.ts index f2380aa0e..f3aff86e4 100644 --- a/src/global/actions/api/accounts.ts +++ b/src/global/actions/api/accounts.ts @@ -1,4 +1,4 @@ -import { addActionHandler } from '../../index'; +import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { selectChat } from '../../selectors'; import { callApi } from '../../../api/gramjs'; import { getTranslation } from '../../../util/langProvider'; @@ -60,3 +60,111 @@ addActionHandler('reportProfilePhoto', async (global, actions, payload) => { : 'An error occurred while submitting your report. Please, try again later.', }); }); + +addActionHandler('loadAuthorizations', async () => { + const result = await callApi('fetchAuthorizations'); + if (!result) { + return; + } + + setGlobal({ + ...getGlobal(), + activeSessions: { + byHash: result.authorizations, + orderedHashes: Object.keys(result.authorizations), + ttlDays: result.ttlDays, + }, + }); +}); + +addActionHandler('terminateAuthorization', async (global, actions, payload) => { + const { hash } = payload!; + + const result = await callApi('terminateAuthorization', hash); + if (!result) { + return; + } + + global = getGlobal(); + + const { [hash]: removedSessions, ...newSessions } = global.activeSessions.byHash; + + setGlobal({ + ...global, + activeSessions: { + byHash: newSessions, + orderedHashes: global.activeSessions.orderedHashes.filter((el) => el !== hash), + }, + }); +}); + +addActionHandler('terminateAllAuthorizations', async (global) => { + const result = await callApi('terminateAllAuthorizations'); + if (!result) { + return; + } + + global = getGlobal(); + const currentSessionHash = global.activeSessions.orderedHashes + .find((hash) => global.activeSessions.byHash[hash].isCurrent); + if (!currentSessionHash) { + return; + } + const currentSession = global.activeSessions.byHash[currentSessionHash]; + + setGlobal({ + ...global, + activeSessions: { + byHash: { + [currentSessionHash]: currentSession, + }, + orderedHashes: [currentSessionHash], + }, + }); +}); + +addActionHandler('changeSessionSettings', async (global, actions, payload) => { + const { hash, areCallsEnabled } = payload; + const result = await callApi('changeSessionSettings', { + hash, + areCallsEnabled, + }); + + if (!result) { + return; + } + + global = getGlobal(); + setGlobal({ + ...global, + activeSessions: { + ...global.activeSessions, + byHash: { + ...global.activeSessions.byHash, + [hash]: { + ...global.activeSessions.byHash[hash], + areCallsEnabled, + }, + }, + }, + }); +}); + +addActionHandler('changeSessionTtl', async (global, actions, payload) => { + const { days } = payload; + + const result = await callApi('changeSessionTtl', { days }); + + if (!result) { + return; + } + + global = getGlobal(); + setGlobal({ + ...global, + activeSessions: { + ...global.activeSessions, + ttlDays: days, + }, + }); +}); diff --git a/src/global/actions/api/settings.ts b/src/global/actions/api/settings.ts index c0fa105c8..14dfa5b2e 100644 --- a/src/global/actions/api/settings.ts +++ b/src/global/actions/api/settings.ts @@ -241,48 +241,6 @@ addActionHandler('unblockContact', async (global, actions, payload) => { setGlobal(removeBlockedContact(getGlobal(), contactId)); }); -addActionHandler('loadAuthorizations', async () => { - const result = await callApi('fetchAuthorizations'); - if (!result) { - return; - } - - setGlobal({ - ...getGlobal(), - activeSessions: result, - }); -}); - -addActionHandler('terminateAuthorization', async (global, actions, payload) => { - const { hash } = payload!; - - const result = await callApi('terminateAuthorization', hash); - if (!result) { - return; - } - - global = getGlobal(); - - setGlobal({ - ...global, - activeSessions: global.activeSessions.filter((session) => session.hash !== hash), - }); -}); - -addActionHandler('terminateAllAuthorizations', async (global) => { - const result = await callApi('terminateAllAuthorizations'); - if (!result) { - return; - } - - global = getGlobal(); - - setGlobal({ - ...global, - activeSessions: global.activeSessions.filter((session) => session.isCurrent), - }); -}); - addActionHandler('loadNotificationExceptions', async (global) => { const { serverTimeOffset } = global; diff --git a/src/global/cache.ts b/src/global/cache.ts index d7a098022..a9504868b 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -235,6 +235,13 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) { if (!cached.trustedBotIds) { cached.trustedBotIds = []; } + + if (cached.activeSessions?.byHash === undefined) { + cached.activeSessions = { + byHash: {}, + orderedHashes: [], + }; + } } function updateCache() { diff --git a/src/global/initialState.ts b/src/global/initialState.ts index a365ef43d..82d560f52 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -138,7 +138,10 @@ export const INITIAL_STATE: GlobalState = { dialogs: [], - activeSessions: [], + activeSessions: { + byHash: {}, + orderedHashes: [], + }, settings: { byKey: { diff --git a/src/global/types.ts b/src/global/types.ts index 6d06f4842..7f358199a 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -482,8 +482,11 @@ export type GlobalState = { notifications: ApiNotification[]; dialogs: (ApiError | ApiInviteInfo)[]; - // TODO Move to settings - activeSessions: ApiSession[]; + activeSessions: { + byHash: Record; + orderedHashes: string[]; + ttlDays?: number; + }; settings: { byKey: ISettings; @@ -598,6 +601,13 @@ export interface ActionPayloads { description: string; photo?: ApiPhoto; }; + changeSessionSettings: { + hash: string; + areCallsEnabled: boolean; + }; + changeSessionTtl: { + days: number; + }; // Chats openChat: { @@ -700,7 +710,6 @@ export interface ActionPayloads { }; // Bots - clickBotInlineButton: { messageId: number; button: ApiKeyboardButton; @@ -778,7 +787,6 @@ export interface ActionPayloads { }; // Misc - openPollModal: { isQuiz?: boolean; }; diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index bcd4d4f65..78507975d 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1032,6 +1032,8 @@ account.uploadWallPaper#dd853661 file:InputFile mime_type:string settings:WallPa account.setContentSettings#b574b16b flags:# sensitive_enabled:flags.0?true = Bool; account.getContentSettings#8b9b4dae = account.ContentSettings; account.reportProfilePhoto#fa8cc6f5 peer:InputPeer photo_id:InputPhoto reason:ReportReason message:string = Bool; +account.setAuthorizationTTL#bf899aa0 authorization_ttl_days:int = Bool; +account.changeAuthorizationSettings#40f48462 flags:# hash:long encrypted_requests_disabled:flags.0?Bool call_requests_disabled:flags.1?Bool = Bool; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; contacts.getContacts#5dd69e12 hash:long = contacts.Contacts; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 522ace7ca..8e6fcb263 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -45,6 +45,8 @@ "account.getContentSettings", "account.reportPeer", "account.reportProfilePhoto", + "account.changeAuthorizationSettings", + "account.setAuthorizationTTL", "users.getUsers", "users.getFullUser", "contacts.getContacts",