mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-01 08:41:08 +00:00
refactor: identity dropdown -> settings
This commit is contained in:
parent
333b5a4ed4
commit
0cab27cae8
7 changed files with 84 additions and 76 deletions
|
@ -3,13 +3,14 @@ import { type AccountKeyDB, openAccountKeyDB } from './accountKeyDB';
|
||||||
import { BlahIdentity, type BlahIdentityFile, type BlahProfile } from '@blah-im/core/identity';
|
import { BlahIdentity, type BlahIdentityFile, type BlahProfile } from '@blah-im/core/identity';
|
||||||
import { type IdentityFileDB, openIdentityFileDB } from '$lib/identityFiles/identityFileDB';
|
import { type IdentityFileDB, openIdentityFileDB } from '$lib/identityFiles/identityFileDB';
|
||||||
import { BlahKeyPair } from '@blah-im/core/crypto';
|
import { BlahKeyPair } from '@blah-im/core/crypto';
|
||||||
|
import { persisted } from 'svelte-persisted-store';
|
||||||
|
|
||||||
export type Account = BlahIdentityFile & {
|
export type Account = BlahIdentityFile & {
|
||||||
holdingKeyPrivate: boolean;
|
holdingKeyPrivate: boolean;
|
||||||
holdingPrivateOfActKey?: string;
|
holdingPrivateOfActKey?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AccountStore implements Readable<Account[]> {
|
class AccountStore implements Readable<Account[]> {
|
||||||
private keyDB: AccountKeyDB;
|
private keyDB: AccountKeyDB;
|
||||||
private identityFileDB: IdentityFileDB;
|
private identityFileDB: IdentityFileDB;
|
||||||
private internalStore = writable<Account[]>([]);
|
private internalStore = writable<Account[]>([]);
|
||||||
|
@ -83,3 +84,16 @@ export class AccountStore implements Readable<Account[]> {
|
||||||
await this.saveIdentityFile(identity);
|
await this.saveIdentityFile(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let accountStore: AccountStore | undefined;
|
||||||
|
|
||||||
|
export async function openAccountStore(): Promise<AccountStore> {
|
||||||
|
if (!accountStore) {
|
||||||
|
accountStore = await AccountStore.open();
|
||||||
|
}
|
||||||
|
return accountStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { AccountStore };
|
||||||
|
|
||||||
|
export const currentAccountStore = persisted<string | null>('weblah-current-account-id-key', null);
|
||||||
|
|
21
src/lib/components/ProfilePicture.svelte
Normal file
21
src/lib/components/ProfilePicture.svelte
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Account } from '$lib/accounts/accountStore';
|
||||||
|
import { AvatarBeam } from 'svelte-boring-avatars';
|
||||||
|
|
||||||
|
export let account: Account | undefined;
|
||||||
|
export let size: number = 32;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if account}
|
||||||
|
{#key account.id_key}
|
||||||
|
<AvatarBeam {size} name={account.id_key} />
|
||||||
|
{/key}
|
||||||
|
<span class="sr-only">{account.profile.signee.payload.name}</span>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="box-border size-[--weblah-profile-pic-size] rounded-full border-2 border-dashed border-ss-primary"
|
||||||
|
style:--weblah-profile-pic-size={`${size - 2}px`}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<span class="sr-only">Account Unavailable</span>
|
||||||
|
{/if}
|
|
@ -2,6 +2,7 @@
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { onNavigate } from '$app/navigation';
|
import { onNavigate } from '$app/navigation';
|
||||||
import ChatList from './ChatList.svelte';
|
import ChatList from './ChatList.svelte';
|
||||||
|
import SettingsList from './settings/SettingsList.svelte';
|
||||||
|
|
||||||
onNavigate((navigation) => {
|
onNavigate((navigation) => {
|
||||||
if (!document.startViewTransition) return;
|
if (!document.startViewTransition) return;
|
||||||
|
@ -14,10 +15,9 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$: isSettings = $page.route.id?.startsWith('/settings');
|
||||||
$: mainVisible =
|
$: mainVisible =
|
||||||
!!$page.params.chatId ||
|
!!$page.params.chatId || (isSettings && !$page.route.id?.startsWith('/settings/_mobile_empty'));
|
||||||
($page.route.id?.startsWith('/settings') &&
|
|
||||||
!$page.route.id?.startsWith('/settings/_mobile_empty'));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -28,6 +28,9 @@
|
||||||
class="relative h-[100dvh] min-h-0 overflow-hidden border-ss-primary bg-sb-primary shadow-lg [view-transition-name:chat-list] after:pointer-events-none after:absolute after:inset-0 after:size-full after:bg-transparent group-data-[weblah-main-visible]:after:bg-black/30 sm:w-1/3 sm:border-e sm:after:hidden lg:w-1/4"
|
class="relative h-[100dvh] min-h-0 overflow-hidden border-ss-primary bg-sb-primary shadow-lg [view-transition-name:chat-list] after:pointer-events-none after:absolute after:inset-0 after:size-full after:bg-transparent group-data-[weblah-main-visible]:after:bg-black/30 sm:w-1/3 sm:border-e sm:after:hidden lg:w-1/4"
|
||||||
>
|
>
|
||||||
<ChatList />
|
<ChatList />
|
||||||
|
{#if isSettings}
|
||||||
|
<SettingsList />
|
||||||
|
{/if}
|
||||||
</aside>
|
</aside>
|
||||||
{#if mainVisible}
|
{#if mainVisible}
|
||||||
<main
|
<main
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import InputFrame from '$lib/components/InputFrame.svelte';
|
import InputFrame from '$lib/components/InputFrame.svelte';
|
||||||
import { Icon, MagnifyingGlass, PencilSquare, XCircle } from 'svelte-hero-icons';
|
import { Icon, MagnifyingGlass, PencilSquare, XCircle } from 'svelte-hero-icons';
|
||||||
import IdentityMenu from './IdentityMenu.svelte';
|
|
||||||
import { tw } from '$lib/tw';
|
import { tw } from '$lib/tw';
|
||||||
|
import CurrentAccountPicture from './CurrentAccountPicture.svelte';
|
||||||
|
|
||||||
export let searchQuery: string = '';
|
export let searchQuery: string = '';
|
||||||
export let isSearchFocused: boolean;
|
export let isSearchFocused: boolean;
|
||||||
|
@ -18,7 +18,15 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="flex items-center justify-stretch gap-2 border-b border-ss-secondary p-2 shadow-sm">
|
<header class="flex items-center justify-stretch gap-2 border-b border-ss-secondary p-2 shadow-sm">
|
||||||
<IdentityMenu class={tw('transition-opacity duration-200', isSearchFocused && 'opacity-0')} />
|
<a
|
||||||
|
class={tw(
|
||||||
|
'transition-[opacity,transform] duration-200',
|
||||||
|
isSearchFocused && '-translate-x-full opacity-0'
|
||||||
|
)}
|
||||||
|
href="/settings"
|
||||||
|
>
|
||||||
|
<CurrentAccountPicture />
|
||||||
|
</a>
|
||||||
<InputFrame
|
<InputFrame
|
||||||
class={tw('z-10 h-8 flex-1 transition-all duration-200', isSearchFocused && '-mx-10')}
|
class={tw('z-10 h-8 flex-1 transition-all duration-200', isSearchFocused && '-mx-10')}
|
||||||
>
|
>
|
||||||
|
@ -46,8 +54,8 @@
|
||||||
<button
|
<button
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class={tw(
|
class={tw(
|
||||||
'-mx-2 -my-1.5 flex size-8 cursor-text items-center justify-center text-slate-300 opacity-0 transition-opacity duration-200 dark:text-slate-500',
|
'-mx-2 -my-1.5 flex size-8 cursor-text items-center justify-center text-slate-300 opacity-0 transition-[opacity,transform] duration-200 dark:text-slate-500',
|
||||||
isSearchFocused && 'cursor-default opacity-100 '
|
isSearchFocused && 'translate-x-full cursor-default opacity-100'
|
||||||
)}
|
)}
|
||||||
on:click={onTapClear}
|
on:click={onTapClear}
|
||||||
>
|
>
|
||||||
|
|
30
src/routes/(app)/CurrentAccountPicture.svelte
Normal file
30
src/routes/(app)/CurrentAccountPicture.svelte
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
currentAccountStore,
|
||||||
|
openAccountStore,
|
||||||
|
type Account,
|
||||||
|
type AccountStore
|
||||||
|
} from '$lib/accounts/accountStore';
|
||||||
|
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
|
||||||
|
|
||||||
|
export let size: number = 32;
|
||||||
|
|
||||||
|
let accountStore: AccountStore;
|
||||||
|
|
||||||
|
async function getAccount(idKeyId: string | null): Promise<Account | undefined> {
|
||||||
|
if (!accountStore) {
|
||||||
|
accountStore = await openAccountStore();
|
||||||
|
}
|
||||||
|
if (!idKeyId) return;
|
||||||
|
let currentAccount = $accountStore.find((account) => account.id_key === idKeyId);
|
||||||
|
if (!currentAccount && $accountStore.length > 0) {
|
||||||
|
currentAccount = $accountStore[0];
|
||||||
|
$currentAccountStore = currentAccount.id_key;
|
||||||
|
}
|
||||||
|
return currentAccount;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await getAccount($currentAccountStore) then currentAccount}
|
||||||
|
<ProfilePicture account={currentAccount} {size} />
|
||||||
|
{/await}
|
|
@ -1,68 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import * as DropdownMenu from '$lib/components/DropdownMenu';
|
|
||||||
import { AvatarBeam } from 'svelte-boring-avatars';
|
|
||||||
import { keyStore, currentKeyIndex, currentKeyPair } from '$lib/keystore';
|
|
||||||
import { BlahKeyPair } from '@blah-im/core/crypto';
|
|
||||||
|
|
||||||
let className: string = '';
|
|
||||||
export { className as class };
|
|
||||||
|
|
||||||
let currentKeyId: string | undefined;
|
|
||||||
let currentKeyName: string | null;
|
|
||||||
$: {
|
|
||||||
currentKeyId = $currentKeyPair?.id;
|
|
||||||
currentKeyName = currentKeyId
|
|
||||||
? currentKeyId.slice(0, 4) + '...' + currentKeyId.slice(-4)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createKeyPair() {
|
|
||||||
const newKeyPair = await BlahKeyPair.generate();
|
|
||||||
const encoded = await newKeyPair.encode();
|
|
||||||
$keyStore = [...$keyStore, encoded];
|
|
||||||
$currentKeyIndex = $keyStore.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCurrentKeyIndex(idx: string | undefined | null) {
|
|
||||||
$currentKeyIndex = parseInt(idx ?? '0', 10);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenu.Root closeOnItemClick={false}>
|
|
||||||
<DropdownMenu.Trigger class={className}>
|
|
||||||
{#if currentKeyId}
|
|
||||||
{#key currentKeyId}
|
|
||||||
<AvatarBeam size={32} name={currentKeyId} />
|
|
||||||
{/key}
|
|
||||||
<span class="sr-only">Using identity {currentKeyName}</span>
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
class="box-border size-[30px] rounded-full border-2 border-dashed border-ss-primary"
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
<span class="sr-only">Using no identity</span>
|
|
||||||
{/if}
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
<DropdownMenu.Content class="origin-top-left">
|
|
||||||
{#if $keyStore.length > 0}
|
|
||||||
<DropdownMenu.RadioGroup
|
|
||||||
value={$currentKeyIndex.toString()}
|
|
||||||
onValueChange={setCurrentKeyIndex}
|
|
||||||
>
|
|
||||||
{#each $keyStore as { id }, idx}
|
|
||||||
{@const name = id.slice(0, 4) + '...' + id.slice(-4)}
|
|
||||||
<DropdownMenu.RadioItem value={idx.toString()}>
|
|
||||||
<div class="flex items-center gap-2 py-0.5">
|
|
||||||
<AvatarBeam size={24} name={id} />
|
|
||||||
<span>{name}</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.RadioItem>
|
|
||||||
{/each}
|
|
||||||
</DropdownMenu.RadioGroup>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item>Settings...</DropdownMenu.Item>
|
|
||||||
{:else}
|
|
||||||
<DropdownMenu.Item on:click={createKeyPair}>Create new identity</DropdownMenu.Item>
|
|
||||||
{/if}
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
0
src/routes/(app)/settings/SettingsList.svelte
Normal file
0
src/routes/(app)/settings/SettingsList.svelte
Normal file
Loading…
Add table
Reference in a new issue