refactor: account management system

Replace store-based approach with manager-based implementation and update
related components to use the new system. Change ProfilePicture props
from `account` to `identity`.
This commit is contained in:
Shibo Lyu 2025-04-14 00:49:59 +08:00
parent 055f7240df
commit b467ec1491
8 changed files with 91 additions and 88 deletions

View file

@ -6,7 +6,6 @@ import {
} from '@blah-im/core/identity'; } from '@blah-im/core/identity';
import { type IdentityDB, openIdentityDB } from './identityFileDB'; import { type IdentityDB, openIdentityDB } from './identityFileDB';
import { BlahKeyPair } from '@blah-im/core/crypto'; import { BlahKeyPair } from '@blah-im/core/crypto';
import { persisted } from 'svelte-persisted-store';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
export type Account = BlahIdentityDescription & { export type Account = BlahIdentityDescription & {
@ -30,24 +29,25 @@ class AccountManager {
constructor() { constructor() {
if (browser) { if (browser) {
this.currentAccountId = localStorage.getItem(localStorageCurrentAccountIdKey); this.currentAccountId = localStorage.getItem(localStorageCurrentAccountIdKey);
console.log('currentAccountId', this.currentAccountId);
$effect.root(() => {
$effect(() =>
this.currentAccountId
? localStorage.setItem(localStorageCurrentAccountIdKey, this.currentAccountId)
: localStorage.removeItem(localStorageCurrentAccountIdKey)
);
});
(async () => {
this.inProgress = true;
const [keyDB, identityDB] = await Promise.all([openAccountKeyDB(), openIdentityDB()]);
this.keyDB = keyDB;
this.identityDB = identityDB;
await this.loadAccounts();
this.inProgress = false;
})();
} }
$effect.root(() => {
$effect(() =>
this.currentAccountId
? localStorage.setItem(localStorageCurrentAccountIdKey, this.currentAccountId)
: localStorage.removeItem(localStorageCurrentAccountIdKey)
);
});
(async () => {
this.inProgress = true;
const [keyDB, identityDB] = await Promise.all([openAccountKeyDB(), openIdentityDB()]);
this.keyDB = keyDB;
this.identityDB = identityDB;
await this.loadAccounts();
this.inProgress = false;
})();
} }
async loadAccounts() { async loadAccounts() {

View file

@ -2,7 +2,6 @@ import { persisted } from 'svelte-persisted-store';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { BlahChatServerConnection } from './blah/connection/chatServer'; import { BlahChatServerConnection } from './blah/connection/chatServer';
import { BlahKeyPair, type EncodedBlahKeyPair } from '@blah-im/core/crypto'; import { BlahKeyPair, type EncodedBlahKeyPair } from '@blah-im/core/crypto';
import { currentKeyPair } from './keystore';
import { ChatListManager } from './chatList'; import { ChatListManager } from './chatList';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { GlobalSearchManager } from './globalSearch'; import { GlobalSearchManager } from './globalSearch';
@ -18,7 +17,7 @@ class ChatServerConnectionPool {
constructor() { constructor() {
if (browser) { if (browser) {
chatServers.subscribe(this.onChatServersChange.bind(this)); chatServers.subscribe(this.onChatServersChange.bind(this));
currentKeyPair.subscribe(this.onKeyPairChange.bind(this)); // currentKeyPair.subscribe(this.onKeyPairChange.bind(this));
} }
} }

View file

@ -1,14 +1,15 @@
<script lang="ts"> <script lang="ts">
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
import type { HTMLAttributes } from 'svelte/elements';
interface Props { interface Props extends HTMLAttributes<HTMLDivElement> {
class?: string; class?: string;
children?: import('svelte').Snippet; children?: import('svelte').Snippet;
} }
let { children, class: classNames }: Props = $props(); let { children, class: classNames, ...rest }: Props = $props();
</script> </script>
<div class={tw('px-4 py-3', classNames)}> <div class={tw('px-4 py-3', classNames)} {...rest}>
{@render children?.()} {@render children?.()}
</div> </div>

View file

@ -3,7 +3,7 @@
import { formatMessageDate, formatFullMessageDate, formatUnreadCount } from '$lib/formatters'; import { formatMessageDate, formatFullMessageDate, formatUnreadCount } from '$lib/formatters';
import type { Chat } from '$lib/types'; import type { Chat } from '$lib/types';
import { currentKeyPair } from '$lib/keystore'; import accountManager from '$lib/accounts/manager.svelte';
import { toPlainText } from '@blah-im/core/richText'; import { toPlainText } from '@blah-im/core/richText';
import { page } from '$app/state'; import { page } from '$app/state';
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
@ -56,7 +56,7 @@
{#if chat.lastMessage} {#if chat.lastMessage}
{#if chat.id !== chat.lastMessage.sender.id} {#if chat.id !== chat.lastMessage.sender.id}
<span class="text-sf-primary"> <span class="text-sf-primary">
{chat.lastMessage.sender.id === $currentKeyPair.id {chat.lastMessage.sender.id === accountManager.currentAccountId
? 'You' ? 'You'
: chat.lastMessage.sender.name}: : chat.lastMessage.sender.name}:
</span> </span>

View file

@ -24,7 +24,7 @@
{#if accountStore && $accountStore} {#if accountStore && $accountStore}
{@const currentAccount = $accountStore.find((account) => account.id_key === $currentAccountStore)} {@const currentAccount = $accountStore.find((account) => account.id_key === $currentAccountStore)}
<ProfilePicture account={currentAccount} {size} /> <ProfilePicture identity={currentAccount} {size} />
{:else} {:else}
<ProfilePicture account={undefined} {size} /> <ProfilePicture identity={undefined} {size} />
{/if} {/if}

View file

@ -0,0 +1,14 @@
<script lang="ts">
import LoadingIndicator from '$lib/components/LoadingIndicator.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import { Button } from 'bits-ui';
import accountsManager from '$lib/accounts/manager.svelte';
const currentAccount = $derived(accountsManager.currentAccount);
</script>
{#if currentAccount}
<PageHeader>
<h3 class="flex-1">{currentAccount.profile.signee.payload.name}</h3>
</PageHeader>
{/if}

View file

@ -2,76 +2,66 @@
import { GroupedListItem, GroupedListSection } from '$lib/components/GroupedList'; import { GroupedListItem, GroupedListSection } from '$lib/components/GroupedList';
import { ArrowRightEndOnRectangle, Plus, UserPlus } from 'svelte-hero-icons'; import { ArrowRightEndOnRectangle, Plus, UserPlus } from 'svelte-hero-icons';
import SettingsListItem from './SettingsListItem.svelte'; import SettingsListItem from './SettingsListItem.svelte';
import { import manager, { type Account } from '$lib/accounts/manager.svelte';
openAccountStore,
currentAccountStore,
type AccountStore,
type Account
} from '$lib/accounts/accountStore';
import { onMount } from 'svelte';
import ProfilePicture from '$lib/components/ProfilePicture.svelte'; import ProfilePicture from '$lib/components/ProfilePicture.svelte';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { blur } from 'svelte/transition'; import { blur } from 'svelte/transition';
import GroupedListContent from '$lib/components/GroupedList/GroupedListContent.svelte';
let accountStore: AccountStore | undefined = $state(); const currentAccount = $derived(manager.currentAccount);
const remainingAccounts = $derived(
onMount(() => { manager.accounts
openAccountStore().then((store) => { .filter((acc) => acc.id_key !== manager.currentAccountId)
accountStore = store; .toSorted((a, b) =>
}); a.profile.signee.payload.name.localeCompare(b.profile.signee.payload.name)
}); )
);
function switchToAccount(account: Account) { function switchToAccount(account: Account) {
$currentAccountStore = account.id_key; manager.currentAccountId = account.id_key;
} }
</script> </script>
{#if accountStore && $accountStore} {#if currentAccount}
{@const currentAccount = $accountStore.find((acc) => acc.id_key === $currentAccountStore)} {#key currentAccount.id_key}
{@const remainingAccounts = $accountStore <div class="mt-6 p-4 text-center" in:blur>
.filter((acc) => acc.id_key !== $currentAccountStore) <div class="inline-block">
.toSorted((a, b) => a.profile.signee.payload.name.localeCompare(b.profile.signee.payload.name))} <ProfilePicture identity={currentAccount} size={68} />
{#if currentAccount}
{#key currentAccount.id_key}
<div class="mt-6 p-4 text-center" in:blur>
<div class="inline-block">
<ProfilePicture account={currentAccount} size={68} />
</div>
<p>
<span class="text-sf-primary text-xl font-semibold">
{currentAccount.profile.signee.payload.name}
</span>
</p>
<p>
<code class="text-sf-secondary text-sm">
{currentAccount.id_key.slice(0, 4) + '..' + currentAccount.id_key.slice(-4)}
</code>
</p>
</div> </div>
{/key} <p>
{/if} <span class="text-sf-primary text-xl font-semibold">
{currentAccount.profile.signee.payload.name}
{#if remainingAccounts.length > 0} </span>
<GroupedListSection> </p>
{#each remainingAccounts as account (account.id_key)} <p>
<div animate:flip={{ duration: 250 }} transition:blur> <code class="text-sf-secondary text-sm">
<GroupedListItem onclick={() => switchToAccount(account)}> {currentAccount.id_key.slice(0, 4) + '..' + currentAccount.id_key.slice(-4)}
<div class="-mx-0.5"><ProfilePicture {account} size={24} /></div> </code>
{account.profile.signee.payload.name} </p>
</GroupedListItem> </div>
</div> {/key}
{/each}
</GroupedListSection>
{/if}
{/if} {/if}
<GroupedListSection> {#if remainingAccounts.length > 0}
{#if ($accountStore?.length ?? 0) > 0} <GroupedListSection>
{#each remainingAccounts as account (account.id_key)}
<GroupedListContent
class="flex gap-2"
role="button"
tabindex={0}
onclick={() => switchToAccount(account)}
>
<div class="-mx-0.5"><ProfilePicture identity={account} size={24} /></div>
{account.profile.signee.payload.name}
</GroupedListContent>
{/each}
<SettingsListItem icon={Plus} route="/account/add">Add Account</SettingsListItem> <SettingsListItem icon={Plus} route="/account/add">Add Account</SettingsListItem>
{:else} </GroupedListSection>
{:else}
<GroupedListSection>
<SettingsListItem icon={ArrowRightEndOnRectangle} route="/account/add"> <SettingsListItem icon={ArrowRightEndOnRectangle} route="/account/add">
Sign in Sign in
</SettingsListItem> </SettingsListItem>
<SettingsListItem icon={UserPlus} route="/account/new">Create Account</SettingsListItem> <SettingsListItem icon={UserPlus} route="/account/new">Create Account</SettingsListItem>
{/if} </GroupedListSection>
</GroupedListSection> {/if}

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { currentAccountStore, openAccountStore } from '$lib/accounts/accountStore'; import accountsManager from '$lib/accounts/manager.svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import { import {
GroupedListContainer, GroupedListContainer,
@ -47,9 +47,8 @@
isBusy = true; isBusy = true;
try { try {
const accountStore = await openAccountStore(); const idKeyId = await accountsManager.createAccount(profile, password);
const idKeyId = await accountStore.createAccount(profile, password); accountsManager.currentAccountId = idKeyId;
$currentAccountStore = idKeyId;
goto('/settings'); goto('/settings');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -70,7 +69,7 @@
<GroupedListContainer> <GroupedListContainer>
<GroupedListSection> <GroupedListSection>
<GroupedListContent class="flex items-center"> <GroupedListContent class="flex items-center">
<ProfilePicture size={64} account={undefined} /> <ProfilePicture size={64} identity={undefined} />
<input <input
type="text" type="text"
bind:value={name} bind:value={name}