106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
|
|
import { useCallback } from 'react';
|
||
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||
|
|
import { useTranslation } from 'react-i18next';
|
||
|
|
import { Link, useNavigate } from 'react-router';
|
||
|
|
|
||
|
|
import { logout } from '@/api/auth';
|
||
|
|
import { Avatar } from '@/components/ui/Avatar';
|
||
|
|
import { useCurrentUser } from '@/lib/hooks';
|
||
|
|
import { useApiErrorMessage } from '@/lib/useApiErrorMessage';
|
||
|
|
import { useGatewayStore } from '@/stores/gateway';
|
||
|
|
import { useSessionStore } from '@/stores/session';
|
||
|
|
import { userStatuses, type UserStatus } from '@/api/types';
|
||
|
|
|
||
|
|
/** Панель пользователя внизу: аватар, имя, статус, настройки и выход. */
|
||
|
|
export function UserPanel() {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
const navigate = useNavigate();
|
||
|
|
const queryClient = useQueryClient();
|
||
|
|
const currentUser = useCurrentUser();
|
||
|
|
const describeError = useApiErrorMessage();
|
||
|
|
const sessionUser = useSessionStore((state) => state.user);
|
||
|
|
const disconnect = useGatewayStore((state) => state.disconnect);
|
||
|
|
|
||
|
|
const name = sessionUser?.display_name ?? currentUser.data?.display_name ?? '';
|
||
|
|
const username = sessionUser?.username ?? currentUser.data?.username ?? '';
|
||
|
|
const status: UserStatus = sessionUser?.status ?? currentUser.data?.status ?? 'online';
|
||
|
|
const customStatus = sessionUser?.custom_status ?? '';
|
||
|
|
const emoji = sessionUser?.custom_status_emoji ?? '';
|
||
|
|
const seed = currentUser.data?.id ?? sessionUser?.id ?? username;
|
||
|
|
const avatarFileId = sessionUser?.avatar_file_id ?? currentUser.data?.avatar_file_id;
|
||
|
|
|
||
|
|
const signOut = useMutation({
|
||
|
|
mutationFn: () => logout(),
|
||
|
|
onSuccess: () => {
|
||
|
|
// Сессия в cookie уже погашена — сбрасываем локальное состояние.
|
||
|
|
disconnect();
|
||
|
|
useSessionStore.getState().reset();
|
||
|
|
queryClient.clear();
|
||
|
|
void navigate('/login', { replace: true });
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const statusLabel = userStatuses.includes(status)
|
||
|
|
? t(`user.status.${status}`)
|
||
|
|
: t('user.status.offline');
|
||
|
|
|
||
|
|
const onCopyId = useCallback(() => {
|
||
|
|
if (currentUser.data === undefined) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
void navigator.clipboard?.writeText(currentUser.data.id);
|
||
|
|
}, [currentUser.data]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<footer
|
||
|
|
aria-label={t('user.panelLabel')}
|
||
|
|
className="flex items-center gap-2 border-t border-border/40 bg-surface-2/80 px-2 py-2"
|
||
|
|
>
|
||
|
|
<Avatar name={name === '' ? username : name} seed={seed} fileId={avatarFileId} size="md" />
|
||
|
|
<div className="min-w-0 flex-1">
|
||
|
|
<p className="truncate text-sm font-medium" title={name}>
|
||
|
|
{name === '' ? username : name}
|
||
|
|
</p>
|
||
|
|
<p className="truncate text-xs text-fg-muted" title={statusLabel}>
|
||
|
|
<span aria-hidden="true" className="mr-1">
|
||
|
|
{emoji}
|
||
|
|
</span>
|
||
|
|
{customStatus === '' ? statusLabel : `${statusLabel} · ${customStatus}`}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
aria-label={t('user.copyId')}
|
||
|
|
title={t('user.copyId')}
|
||
|
|
className="rounded-[var(--radius-sm)] px-2 py-1 text-fg-muted hover:bg-surface-3 hover:text-fg"
|
||
|
|
onClick={onCopyId}
|
||
|
|
>
|
||
|
|
#
|
||
|
|
</button>
|
||
|
|
<Link
|
||
|
|
to="/settings"
|
||
|
|
aria-label={t('user.settings')}
|
||
|
|
title={t('user.settings')}
|
||
|
|
className="rounded-[var(--radius-sm)] px-2 py-1 text-fg-muted hover:bg-surface-3 hover:text-fg"
|
||
|
|
>
|
||
|
|
⚙
|
||
|
|
</Link>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
aria-label={t('user.logout')}
|
||
|
|
title={t('user.logout')}
|
||
|
|
disabled={signOut.isPending}
|
||
|
|
className="rounded-[var(--radius-sm)] px-2 py-1 text-fg-muted hover:bg-surface-3 hover:text-fg disabled:opacity-50"
|
||
|
|
onClick={() => signOut.mutate()}
|
||
|
|
>
|
||
|
|
⏻
|
||
|
|
</button>
|
||
|
|
{signOut.isError ? (
|
||
|
|
<span role="alert" className="max-w-40 truncate text-xs text-danger">
|
||
|
|
{describeError(signOut.error)}
|
||
|
|
</span>
|
||
|
|
) : null}
|
||
|
|
</footer>
|
||
|
|
);
|
||
|
|
}
|