146 lines
6.4 KiB
TypeScript
146 lines
6.4 KiB
TypeScript
import { useSignal } from '@preact/signals-react';
|
|
import * as Dialog from '@radix-ui/react-dialog';
|
|
import { resolveResource } from '@tauri-apps/api/path';
|
|
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
|
import { nip19 } from 'nostr-tools';
|
|
import { useEffect } from 'react';
|
|
import { parse, stringify } from 'smol-toml';
|
|
import { toast } from 'sonner';
|
|
import { CancelIcon, PlusIcon, UserAddIcon, UserRemoveIcon } from '@shared/icons';
|
|
import { User } from '@shared/user';
|
|
|
|
export function DepotMembers() {
|
|
const members = useSignal<Set<string>>(null);
|
|
const tmpMembers = useSignal<Array<string>>([]);
|
|
const newMember = useSignal('');
|
|
|
|
const addMember = async () => {
|
|
if (!newMember.value.startsWith('npub1'))
|
|
return toast.error('You need to enter a valid npub');
|
|
|
|
try {
|
|
const pubkey = nip19.decode(newMember.value).data as string;
|
|
tmpMembers.value.push(pubkey);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
};
|
|
|
|
const removeMember = (member: string) => {
|
|
tmpMembers.value = tmpMembers.value.filter((item) => item !== member);
|
|
};
|
|
|
|
const updateMembers = async () => {
|
|
members.value = new Set(tmpMembers.value);
|
|
|
|
const defaultConfig = await resolveResource('resources/config.toml');
|
|
const config = await readTextFile(defaultConfig);
|
|
const configContent = parse(config);
|
|
|
|
configContent.authorization['pubkey_whitelist'] = [...members.value];
|
|
|
|
const newConfig = stringify(configContent);
|
|
|
|
return await writeTextFile(defaultConfig, newConfig);
|
|
};
|
|
|
|
useEffect(() => {
|
|
async function loadConfig() {
|
|
const defaultConfig = await resolveResource('resources/config.toml');
|
|
const config = await readTextFile(defaultConfig);
|
|
const configContent = parse(config);
|
|
tmpMembers.value = Array.from(configContent.authorization['pubkey_whitelist']);
|
|
}
|
|
|
|
loadConfig();
|
|
}, []);
|
|
|
|
return (
|
|
<Dialog.Root>
|
|
<div className="flex items-center justify-between rounded-lg bg-neutral-50 p-5 dark:bg-neutral-950">
|
|
<div className="flex flex-col items-start">
|
|
<h3 className="text-lg font-semibold">Members</h3>
|
|
<p className="text-neutral-700 dark:text-neutral-300">
|
|
Only allowed users can publish event to your Depot
|
|
</p>
|
|
</div>
|
|
<div className="inline-flex items-center gap-2">
|
|
<div className="isolate flex -space-x-2">
|
|
{tmpMembers.value.slice(0, 5).map((item) => (
|
|
<User key={item} pubkey={item} variant="stacked" />
|
|
))}
|
|
{tmpMembers.value.length > 5 ? (
|
|
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-700">
|
|
<span className="text-xs font-medium">+{tmpMembers.value.length}</span>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
<Dialog.Trigger className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-lg bg-blue-500 px-3 text-white hover:bg-blue-600">
|
|
<UserAddIcon className="size-4" />
|
|
Manage
|
|
</Dialog.Trigger>
|
|
</div>
|
|
</div>
|
|
<Dialog.Portal>
|
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-black/20" />
|
|
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
|
<div className="relative h-min w-full max-w-xl overflow-hidden rounded-xl bg-white dark:bg-black">
|
|
<div className="inline-flex h-14 w-full shrink-0 items-center justify-between border-b border-neutral-100 px-5 dark:border-neutral-900">
|
|
<Dialog.Title className="text-center font-semibold">
|
|
Manage member
|
|
</Dialog.Title>
|
|
<div className="inline-flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={updateMembers}
|
|
className="inline-flex h-8 w-max items-center justify-center rounded-lg bg-blue-500 px-2.5 text-sm font-medium text-white hover:bg-blue-600"
|
|
>
|
|
Update
|
|
</button>
|
|
<Dialog.Close className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800">
|
|
<CancelIcon className="size-4" />
|
|
</Dialog.Close>
|
|
</div>
|
|
</div>
|
|
<div className="pb-3">
|
|
<div className="relative mb-2 mt-4 w-full px-5">
|
|
<input
|
|
type="text"
|
|
spellCheck={false}
|
|
value={newMember.value}
|
|
onChange={(e) => (newMember.value = e.target.value)}
|
|
placeholder="npub1..."
|
|
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 pl-3 pr-20 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={addMember}
|
|
className="absolute right-7 top-1/2 inline-flex h-7 w-max -translate-y-1/2 transform items-center justify-center gap-1 rounded-md bg-neutral-200 px-2.5 text-sm font-medium text-blue-500 hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-800"
|
|
>
|
|
<PlusIcon className="size-4" />
|
|
Add
|
|
</button>
|
|
</div>
|
|
{tmpMembers.value.map((member) => (
|
|
<div
|
|
key={member}
|
|
className="group flex items-center justify-between px-5 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-900"
|
|
>
|
|
<User pubkey={member} variant="simple" />
|
|
<button
|
|
type="button"
|
|
onClick={() => removeMember(member)}
|
|
className="hidden size-6 items-center justify-center rounded-md bg-neutral-200 group-hover:inline-flex hover:bg-red-200 dark:bg-neutral-800 dark:hover:bg-red-800 dark:hover:text-red-200"
|
|
>
|
|
<UserRemoveIcon className="size-4 text-red-500" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Dialog.Content>
|
|
</Dialog.Portal>
|
|
</Dialog.Root>
|
|
);
|
|
}
|