Files
beavergram/frontend/src/lib/components/settings/MyAccount.svelte
T

300 lines
7.8 KiB
Svelte

<script lang="ts">
import { Dialog } from "bits-ui";
import { ripple } from "#lib/actions/ripple.js";
import { ApiError } from "#lib/api/client.js";
import AddAccountDialog from "#lib/components/settings/AddAccountDialog.svelte";
import SettingsItem from "#lib/components/settings/SettingsItem.svelte";
import Avatar from "#lib/components/ui/Avatar.svelte";
import Button from "#lib/components/ui/Button.svelte";
import Icon from "#lib/components/ui/Icon.svelte";
import { accountName } from "#lib/format/peer.js";
import { accounts } from "#lib/stores/accounts.svelte.js";
import { toasts } from "#lib/stores/toasts.svelte.js";
const current = $derived(accounts.selected);
const others = $derived(
accounts.list.filter(
(account) => account.account_id !== accounts.selectedId
)
);
const DEVICE_LIMIT = 32;
const DEFAULT_DEVICE = "Beavergram";
let adding = $state(false);
let confirming = $state(false);
let renaming = $state(false);
let deviceName = $state("");
let busy = $state(false);
const deviceLabel = $derived(current?.device_model ?? DEFAULT_DEVICE);
function openRename() {
deviceName = deviceLabel;
renaming = true;
}
async function rename(event: SubmitEvent) {
event.preventDefault();
const value = deviceName.trim();
if (!(current && value) || busy) {
return;
}
busy = true;
try {
await accounts.renameDevice(current.account_id, value);
renaming = false;
toasts.success("Имя устройства обновлено");
} catch (error) {
toasts.error(
error instanceof ApiError ? error.detail : "Не удалось переименовать"
);
} finally {
busy = false;
}
}
async function logout() {
if (!current || busy) {
return;
}
busy = true;
try {
await accounts.logout(current.account_id);
confirming = false;
toasts.success("Сессия завершена");
} catch {
toasts.error("Не удалось выйти из аккаунта");
} finally {
busy = false;
}
}
</script>
<div class="my-account">
{#if current}
<div class="profile">
<Avatar
colorKey={current.account_id}
name={accountName(current)}
size={5}
/>
<div class="name">{accountName(current)}</div>
{#if current.phone}
<div class="phone">+{current.phone}</div>
{/if}
{#if !current.is_active}
<div class="inactive">Сессия завершена, доступен только архив</div>
{/if}
</div>
{/if}
{#if others.length > 0}
<div class="switch">
{#each others as account (account.account_id)}
<button
class="switch-row"
onclick={() => accounts.select(account.account_id)}
type="button"
use:ripple
>
<Avatar
colorKey={account.account_id}
name={accountName(account)}
size={2.25}
/>
<span>{accountName(account)}</span>
{#if !account.is_active}
<span class="tag">архив</span>
{/if}
<Icon class="chevron" name="arrow-right" size="1rem" />
</button>
{/each}
</div>
{/if}
<div class="actions">
{#if current?.is_active}
<SettingsItem
icon="active-sessions"
label="Имя устройства"
onclick={openRename}
value={deviceLabel}
/>
{/if}
<SettingsItem
icon="add-user"
label="Добавить аккаунт"
onclick={() => {
adding = true;
}}
/>
{#if current?.is_active}
<SettingsItem
icon="logout"
label="Выйти из аккаунта"
onclick={() => {
confirming = true;
}}
/>
{/if}
</div>
</div>
<AddAccountDialog bind:open={adding} />
<Dialog.Root bind:open={renaming}>
<Dialog.Portal>
<Dialog.Overlay class="dialog-overlay" />
<Dialog.Content class="dialog-content">
<header class="dialog-head">
<Dialog.Title class="dialog-title">Имя устройства</Dialog.Title>
<Dialog.Close aria-label="Закрыть" class="dialog-close">
<Icon name="close" size="1.25rem" />
</Dialog.Close>
</header>
<form onsubmit={rename}>
<div class="dialog-body">
<p class="confirm-text">
Так сессия называется в списке устройств Telegram. Чтобы имя
применилось, соединение переподключится — сбор сообщений прервётся
на пару секунд.
</p>
<div class="input-group">
<input
class="form-control"
id="device-name"
maxlength={DEVICE_LIMIT}
placeholder={DEFAULT_DEVICE}
type="text"
bind:value={deviceName}
>
<label for="device-name">Название</label>
</div>
</div>
<div class="dialog-actions">
<Button
disabled={busy || deviceName.trim().length === 0}
loading={busy}
pill
type="submit"
>
Сохранить
</Button>
</div>
</form>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<Dialog.Root bind:open={confirming}>
<Dialog.Portal>
<Dialog.Overlay class="dialog-overlay" />
<Dialog.Content class="dialog-content">
<header class="dialog-head">
<Dialog.Title class="dialog-title">Выйти из аккаунта?</Dialog.Title>
<Dialog.Close aria-label="Закрыть" class="dialog-close">
<Icon name="close" size="1.25rem" />
</Dialog.Close>
</header>
<div class="dialog-body">
<p class="confirm-text">
Сессия {current ? accountName(current) : ""} будет завершена в
Telegram, новые сообщения перестанут собираться. Уже собранный архив
останется доступным.
</p>
</div>
<div class="dialog-actions">
<Button
onclick={() => {
confirming = false;
}}
pill
variant="text"
>
Отмена
</Button>
<Button loading={busy} onclick={logout} pill variant="danger">
Выйти
</Button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<style lang="scss">
.profile {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1.5rem 1rem;
}
.name {
font-size: 1.25rem;
font-weight: var(--font-weight-medium);
color: var(--color-text);
}
.phone {
font-size: 0.9375rem;
color: var(--color-text-secondary);
}
.inactive {
font-size: 0.8125rem;
color: var(--color-error);
}
.switch-row {
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
min-height: 3.5rem;
padding: 0 1.25rem;
border: 0;
font-size: 1rem;
text-align: start;
color: var(--color-text);
background-color: transparent;
transition: background-color 0.15s ease;
&:hover {
background-color: var(--color-chat-hover);
}
span {
flex: 1;
}
}
.tag {
flex: 0 0 auto !important;
font-size: 0.75rem;
color: var(--color-text-secondary);
}
.actions {
padding: 0.5rem 0;
border-top: 1px solid var(--color-borders);
}
.confirm-text {
margin: 0;
color: var(--color-text-secondary);
}
:global(.switch-row .chevron) {
flex-shrink: 0;
color: var(--color-icon-secondary);
}
</style>