TelegramPWA/src/components/left/settings/SettingsActiveSessions.tsx
2026-06-01 01:16:15 +02:00

294 lines
8.4 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import {
memo, useCallback, useMemo, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiSession } from '../../../api/types';
import type { GlobalState } from '../../../global/types';
import { formatPastTimeShort } from '../../../util/dates/oldDateFormat';
import getSessionIcon from './helpers/getSessionIcon';
import useFlag from '../../../hooks/useFlag';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useOldLang from '../../../hooks/useOldLang';
import Island, { IslandTitle } from '../../gili/layout/Island';
import ConfirmDialog from '../../ui/ConfirmDialog';
import ListItem from '../../ui/ListItem';
import RadioGroup from '../../ui/RadioGroup';
import SettingsActiveSession from './SettingsActiveSession';
import './SettingsActiveSessions.scss';
type OwnProps = {
isActive?: boolean;
onReset: () => void;
};
type StateProps = GlobalState['activeSessions'];
const SettingsActiveSessions: FC<OwnProps & StateProps> = ({
isActive,
onReset,
byHash,
orderedHashes,
ttlDays,
}) => {
const {
terminateAuthorization,
terminateAllAuthorizations,
changeSessionTtl,
} = getActions();
const oldLang = useOldLang();
const lang = useLang();
const [isConfirmTerminateAllDialogOpen, openConfirmTerminateAllDialog, closeConfirmTerminateAllDialog] = useFlag();
const [openedSessionHash, setOpenedSessionHash] = useState<string | undefined>();
const [isModalOpen, openModal, closeModal] = useFlag();
const autoTerminateValue = useMemo(() => {
// https://github.com/DrKLO/Telegram/blob/96dce2c9aabc33b87db61d830aa087b6b03fe397/TMessagesProj/src/main/java/org/telegram/ui/SessionsActivity.java#L195
if (ttlDays === undefined) {
return undefined;
}
if (ttlDays <= 7) {
return '7';
}
if (ttlDays <= 30) {
return '30';
}
if (ttlDays <= 93) {
return '90';
}
if (ttlDays <= 183) {
return '183';
}
if (ttlDays > 183) {
return '365';
}
return undefined;
}, [ttlDays]);
const AUTO_TERMINATE_OPTIONS = useMemo(() => {
const options = [{
label: lang('Weeks', { count: 1 }, { pluralValue: 1 }),
value: '7',
}, {
label: lang('Months', { count: 1 }, { pluralValue: 1 }),
value: '30',
}, {
label: lang('Months', { count: 3 }, { pluralValue: 3 }),
value: '90',
}, {
label: lang('Months', { count: 6 }, { pluralValue: 6 }),
value: '183',
}];
if (ttlDays && ttlDays >= 365) {
options.push({
label: lang('Years', { count: 1 }, { pluralValue: 1 }),
value: '365',
});
}
return options;
}, [lang, ttlDays]);
const handleTerminateSessionClick = useCallback((hash: string) => {
terminateAuthorization({ hash });
}, [terminateAuthorization]);
const handleTerminateAllSessions = useCallback(() => {
closeConfirmTerminateAllDialog();
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(() => {
const currentSessionHash = orderedHashes.find((hash) => byHash[hash].isCurrent);
return currentSessionHash ? byHash[currentSessionHash] : undefined;
}, [byHash, orderedHashes]);
const otherSessionHashes = useMemo(() => {
return orderedHashes.filter((hash) => !byHash[hash].isCurrent);
}, [byHash, orderedHashes]);
const hasOtherSessions = Boolean(otherSessionHashes.length);
useHistoryBack({
isActive,
onBack: onReset,
});
function renderCurrentSession(session: ApiSession) {
return (
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('AuthSessionsCurrentSession')}
</IslandTitle>
<Island>
<ListItem narrow inactive icon={`device-${getSessionIcon(session)}`} iconClassName="icon-device">
<div className="multiline-item full-size" dir="auto">
<span className="title" dir="auto">{session.deviceModel}</span>
<span className="subtitle black tight">
{session.appName}
{' '}
{session.appVersion}
,
{' '}
{session.platform}
{' '}
{session.systemVersion}
</span>
<span className="subtitle">
{session.ip}
{' '}
-
{' '}
{getLocation(session)}
</span>
</div>
</ListItem>
{hasOtherSessions && (
<ListItem
className="destructive mb-0 no-icon"
icon="stop"
ripple
narrow
onClick={openConfirmTerminateAllDialog}
>
{lang('TerminateAllSessions')}
</ListItem>
)}
</Island>
</>
);
}
function renderOtherSessions(sessionHashes: string[]) {
return (
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('OtherSessions')}
</IslandTitle>
<Island>
{sessionHashes.map(renderSession)}
</Island>
</>
);
}
function renderAutoTerminate() {
return (
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('TerminateOldSessionHeader')}
</IslandTitle>
<Island>
<p className="settings-item-description-larger">{lang('IfInactiveFor')}</p>
<RadioGroup
name="session_ttl"
options={AUTO_TERMINATE_OPTIONS}
selected={autoTerminateValue}
onChange={handleChangeSessionTtl}
/>
</Island>
</>
);
}
function renderSession(sessionHash: string) {
const session = byHash[sessionHash];
return (
<ListItem
key={session.hash}
ripple
narrow
contextActions={[{
title: lang('SessionTerminate'),
icon: 'stop',
destructive: true,
handler: () => {
handleTerminateSessionClick(session.hash);
},
}]}
icon={`device-${getSessionIcon(session)}`}
iconClassName="icon-device"
onClick={() => { handleOpenSessionModal(session.hash); }}
>
<div className="multiline-item full-size" dir="auto">
<span className="title title-with-date">
{session.deviceModel}
<span className="date">{formatPastTimeShort(oldLang, session.dateActive * 1000)}</span>
</span>
<span className="subtitle black tight">
{session.appName}
{' '}
{session.appVersion}
,
{' '}
{session.platform}
{' '}
{session.systemVersion}
</span>
<span className="subtitle">
{session.ip}
{' '}
{getLocation(session)}
</span>
</div>
</ListItem>
);
}
return (
<div className="settings-content custom-scroll SettingsActiveSessions">
{currentSession && renderCurrentSession(currentSession)}
{hasOtherSessions && renderOtherSessions(otherSessionHashes)}
{renderAutoTerminate()}
{hasOtherSessions && (
<ConfirmDialog
isOpen={isConfirmTerminateAllDialogOpen}
onClose={closeConfirmTerminateAllDialog}
text={lang('AreYouSureSessions')}
confirmLabel={lang('TerminateAllSessions')}
confirmHandler={handleTerminateAllSessions}
confirmIsDestructive
areButtonsInColumn
/>
)}
<SettingsActiveSession isOpen={isModalOpen} hash={openedSessionHash} onClose={handleCloseSessionModal} />
</div>
);
};
function getLocation(session: ApiSession) {
return [session.region, session.country].filter(Boolean).join(', ');
}
export default memo(withGlobal<OwnProps>(
(global): Complete<StateProps> => global.activeSessions as Complete<StateProps>,
)(SettingsActiveSessions));