feat(depot): update screens

This commit is contained in:
reya 2023-12-21 15:29:07 +07:00
parent a6ca2589ab
commit 4670778181
25 changed files with 787 additions and 211 deletions

View File

@ -34,7 +34,7 @@
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-toolbar": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.14.1",
"@tanstack/react-query": "^5.14.2",
"@tauri-apps/api": "2.0.0-alpha.11",
"@tauri-apps/cli": "2.0.0-alpha.17",
"@tauri-apps/plugin-autostart": "2.0.0-alpha.3",
@ -115,9 +115,9 @@
"prop-types": "^15.8.1",
"tailwind-merge": "^1.14.0",
"tailwind-scrollbar": "^3.0.5",
"tailwindcss": "^3.3.7",
"tailwindcss": "^3.4.0",
"typescript": "^5.3.3",
"vite": "4",
"vite": "^4.5.1",
"vite-tsconfig-paths": "^4.2.2"
}
}

View File

@ -54,8 +54,8 @@ dependencies:
specifier: ^1.0.7
version: 1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query':
specifier: ^5.14.1
version: 5.14.1(react@18.2.0)
specifier: ^5.14.2
version: 5.14.2(react@18.2.0)
'@tauri-apps/api':
specifier: 2.0.0-alpha.11
version: 2.0.0-alpha.11
@ -213,10 +213,10 @@ dependencies:
devDependencies:
'@tailwindcss/forms':
specifier: ^0.5.7
version: 0.5.7(tailwindcss@3.3.7)
version: 0.5.7(tailwindcss@3.4.0)
'@tailwindcss/typography':
specifier: ^0.5.10
version: 0.5.10(tailwindcss@3.3.7)
version: 0.5.10(tailwindcss@3.4.0)
'@trivago/prettier-plugin-sort-imports':
specifier: ^4.3.0
version: 4.3.0(prettier@3.1.1)
@ -291,15 +291,15 @@ devDependencies:
version: 1.14.0
tailwind-scrollbar:
specifier: ^3.0.5
version: 3.0.5(tailwindcss@3.3.7)
version: 3.0.5(tailwindcss@3.4.0)
tailwindcss:
specifier: ^3.3.7
version: 3.3.7
specifier: ^3.4.0
version: 3.4.0
typescript:
specifier: ^5.3.3
version: 5.3.3
vite:
specifier: '4'
specifier: ^4.5.1
version: 4.5.1(@types/node@20.10.5)
vite-tsconfig-paths:
specifier: ^4.2.2
@ -1946,16 +1946,16 @@ packages:
resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==}
dev: true
/@tailwindcss/forms@0.5.7(tailwindcss@3.3.7):
/@tailwindcss/forms@0.5.7(tailwindcss@3.4.0):
resolution: {integrity: sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==}
peerDependencies:
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
dependencies:
mini-svg-data-uri: 1.4.4
tailwindcss: 3.3.7
tailwindcss: 3.4.0
dev: true
/@tailwindcss/typography@0.5.10(tailwindcss@3.3.7):
/@tailwindcss/typography@0.5.10(tailwindcss@3.4.0):
resolution: {integrity: sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders'
@ -1964,19 +1964,19 @@ packages:
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
postcss-selector-parser: 6.0.10
tailwindcss: 3.3.7
tailwindcss: 3.4.0
dev: true
/@tanstack/query-core@5.14.1:
resolution: {integrity: sha512-TlZarySCVEiap4K7BCvrsYZnX7jBbEkR55YMrk8ELcRbuAx6ydL+qoxqUt8Fq8VMvQyGt52icn6T7eJL1Q35KQ==}
/@tanstack/query-core@5.14.2:
resolution: {integrity: sha512-QmoJvC72sSWs3hgGis8JdmlDvqLfYGWUK4UG6OR9Q6t28JMN9m2FDwKPqoSJ9YVocELCSjMt/FGjEiLfk8000Q==}
dev: false
/@tanstack/react-query@5.14.1(react@18.2.0):
resolution: {integrity: sha512-v7jhe/3jhChiR0XJbGHaG5WNPd/cURwzDGBCr4rzpUTeudPzxrtVRKsF1xJRLcJK3qH/0gIwTYHIPZ3gj+01Yw==}
/@tanstack/react-query@5.14.2(react@18.2.0):
resolution: {integrity: sha512-SbOzV7UBW8ED3tOnyn6kqNGscnOAfoxShYlbvaQo/5528mDZKpvrwoL/1du1/ukSC6RMAiKmx95SrYqlwPzWDw==}
peerDependencies:
react: ^18.0.0
dependencies:
'@tanstack/query-core': 5.14.1
'@tanstack/query-core': 5.14.2
react: 18.2.0
dev: false
@ -5608,17 +5608,17 @@ packages:
resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==}
dev: true
/tailwind-scrollbar@3.0.5(tailwindcss@3.3.7):
/tailwind-scrollbar@3.0.5(tailwindcss@3.4.0):
resolution: {integrity: sha512-0ZwxTivevqq9BY9fRP9zDjHl7Tu+J5giBGbln+0O1R/7nHtBUKnjQcA1aTIhK7Oyjp6Uc/Dj6/dn8Dq58k5Uww==}
engines: {node: '>=12.13.0'}
peerDependencies:
tailwindcss: 3.x
dependencies:
tailwindcss: 3.3.7
tailwindcss: 3.4.0
dev: true
/tailwindcss@3.3.7:
resolution: {integrity: sha512-pjgQxDZPvyS/nG3ZYkyCvsbONJl7GdOejfm24iMt2ElYQQw8Jc4p0m8RdMp7mznPD0kUhfzwV3zAwa80qI0zmQ==}
/tailwindcss@3.4.0:
resolution: {integrity: sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==}
engines: {node: '>=14.0.0'}
hasBin: true
dependencies:

48
src-tauri/src/commands.rs Normal file
View File

@ -0,0 +1,48 @@
use std::process::Command;
#[tauri::command]
pub async fn show_in_folder(path: String) {
#[cfg(target_os = "windows")]
{
Command::new("explorer")
.args(["/select,", &path]) // The comma after select is not a typo
.spawn()
.unwrap();
}
#[cfg(target_os = "linux")]
{
use std::fs::metadata;
use std::path::PathBuf;
if path.contains(",") {
// see https://gitlab.freedesktop.org/dbus/dbus/-/issues/76
let new_path = match metadata(&path).unwrap().is_dir() {
true => path,
false => {
let mut path2 = PathBuf::from(path);
path2.pop();
path2.into_os_string().into_string().unwrap()
}
};
Command::new("xdg-open").arg(&new_path).spawn().unwrap();
} else {
Command::new("dbus-send")
.args([
"--session",
"--dest=org.freedesktop.FileManager1",
"--type=method_call",
"/org/freedesktop/FileManager1",
"org.freedesktop.FileManager1.ShowItems",
format!("array:string:file://{path}").as_str(),
"string:\"\"",
])
.spawn()
.unwrap();
}
}
#[cfg(target_os = "macos")]
{
Command::new("open").args(["-R", &path]).spawn().unwrap();
}
}

View File

@ -3,6 +3,8 @@
windows_subsystem = "windows"
)]
mod commands;
use keyring::Entry;
use std::time::Duration;
use tauri_plugin_autostart::MacosLauncher;
@ -161,7 +163,8 @@ fn main() {
opengraph,
secure_save,
secure_load,
secure_remove
secure_remove,
commands::show_in_folder,
])
.run(ctx)
.expect("error while running tauri application");

View File

@ -63,32 +63,6 @@ export default function App() {
return { Component: RelayScreen };
},
},
{
path: 'depot',
children: [
{
index: true,
loader: () => {
const depot = ark.checkDepot();
if (!depot) return redirect('/depot/onboarding/');
return null;
},
async lazy() {
const { DepotScreen } = await import('@app/depot');
return { Component: DepotScreen };
},
},
{
path: 'onboarding',
async lazy() {
const { DepotOnboardingScreen } = await import(
'@app/depot/onboarding'
);
return { Component: DepotOnboardingScreen };
},
},
],
},
{
path: 'new',
element: <ComposerLayout />,
@ -188,6 +162,30 @@ export default function App() {
},
],
},
{
path: 'depot',
children: [
{
index: true,
loader: () => {
const depot = ark.checkDepot();
if (!depot) return redirect('/depot/onboarding/');
return null;
},
async lazy() {
const { DepotScreen } = await import('@app/depot');
return { Component: DepotScreen };
},
},
{
path: 'onboarding',
async lazy() {
const { DepotOnboardingScreen } = await import('@app/depot/onboarding');
return { Component: DepotOnboardingScreen };
},
},
],
},
],
},
{

View File

@ -71,11 +71,6 @@ export function CreateAccountScreen() {
privkey: userPrivkey,
});
await ark.createEvent({
kind: NDKKind.RelayList,
tags: ark.relays.map((item) => ['r', item, '']),
});
setKeys({ npub: userNpub, nsec: userNsec });
setLoading(false);
} else {

View File

@ -0,0 +1,66 @@
import { NDKKind } from '@nostr-dev-kit/ndk';
import { useSignal } from '@preact/signals-react';
import { toast } from 'sonner';
import { useArk } from '@libs/ark';
import { LoaderIcon, RunIcon } from '@shared/icons';
import { User } from '@shared/user';
export function DepotContactCard() {
const ark = useArk();
const status = useSignal(false);
const backupContact = async () => {
try {
status.value = true;
const event = await ark.getEventByFilter({
filter: { authors: [ark.account.pubkey], kinds: [NDKKind.Contacts] },
});
// broadcast to depot
const publish = await event.publish();
if (publish) {
status.value = false;
toast.success('Backup contact list successfully.');
}
} catch (e) {
status.value = false;
toast.error(e);
}
};
return (
<div className="flex h-56 w-full flex-col gap-2 overflow-hidden rounded-xl bg-neutral-100 p-2 dark:bg-neutral-900">
<div className="flex flex-1 items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
<div className="isolate flex -space-x-2">
{ark.account.contacts
?.slice(0, 8)
.map((item) => <User key={item} pubkey={item} variant="ministacked" />)}
{ark.account.contacts?.length > 8 ? (
<div className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-neutral-300 text-neutral-900 ring-1 ring-white dark:bg-neutral-700 dark:text-neutral-100 dark:ring-black">
<span className="text-[8px] font-medium">
+{ark.account.contacts?.length - 8}
</span>
</div>
) : null}
</div>
</div>
<div className="inline-flex shrink-0 items-center justify-between">
<div className="text-sm font-medium">Contacts</div>
<button
type="button"
onClick={backupContact}
className="inline-flex h-8 w-max items-center justify-center gap-2 rounded-md bg-blue-500 pl-2 pr-3 font-medium text-white shadow shadow-blue-500/50 hover:bg-blue-600"
>
{status.value ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
<RunIcon className="size-4" />
)}
Backup
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,145 @@
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>
);
}

View File

@ -0,0 +1,55 @@
import { NDKKind } from '@nostr-dev-kit/ndk';
import { useSignal } from '@preact/signals-react';
import { toast } from 'sonner';
import { useArk } from '@libs/ark';
import { LoaderIcon, RunIcon } from '@shared/icons';
import { User } from '@shared/user';
export function DepotProfileCard() {
const ark = useArk();
const status = useSignal(false);
const backupProfile = async () => {
try {
status.value = true;
const event = await ark.getEventByFilter({
filter: { authors: [ark.account.pubkey], kinds: [NDKKind.Metadata] },
});
// broadcast to depot
const publish = await event.publish();
if (publish) {
status.value = false;
toast.success('Backup profile successfully.');
}
} catch (e) {
status.value = false;
toast.error(JSON.stringify(e));
}
};
return (
<div className="flex h-56 w-full flex-col gap-2 overflow-hidden rounded-xl bg-neutral-100 p-2 dark:bg-neutral-900">
<div className="flex flex-1 items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
<User pubkey={ark.account.pubkey} variant="simple" />
</div>
<div className="inline-flex shrink-0 items-center justify-between">
<div className="text-sm font-medium">Profile</div>
<button
type="button"
onClick={backupProfile}
className="inline-flex h-8 w-max items-center justify-center gap-2 rounded-md bg-blue-500 pl-2 pr-3 font-medium text-white shadow shadow-blue-500/50 hover:bg-blue-600"
>
{status.value ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
<RunIcon className="size-4" />
)}
Backup
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,67 @@
import { NDKKind } from '@nostr-dev-kit/ndk';
import { useSignal } from '@preact/signals-react';
import { useEffect } from 'react';
import { toast } from 'sonner';
import { useArk } from '@libs/ark';
import { LoaderIcon, RunIcon } from '@shared/icons';
export function DepotRelaysCard() {
const ark = useArk();
const status = useSignal(false);
const relaySize = useSignal(0);
const backupRelays = async () => {
try {
status.value = true;
const event = await ark.getEventByFilter({
filter: { authors: [ark.account.pubkey], kinds: [NDKKind.RelayList] },
});
// broadcast to depot
const publish = await event.publish();
if (publish) {
status.value = false;
toast.success('Backup profile successfully.');
}
} catch (e) {
status.value = false;
toast.error(JSON.stringify(e));
}
};
useEffect(() => {
async function loadRelays() {
const event = await ark.getEventByFilter({
filter: { authors: [ark.account.pubkey], kinds: [NDKKind.RelayList] },
});
if (event) relaySize.value = event.tags.length;
}
loadRelays();
}, []);
return (
<div className="flex h-56 w-full flex-col gap-2 overflow-hidden rounded-xl bg-neutral-100 p-2 dark:bg-neutral-900">
<div className="flex flex-1 items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
<p className="text-lg font-semibold">{relaySize} relays</p>
</div>
<div className="inline-flex shrink-0 items-center justify-between">
<div className="text-sm font-medium">Relay List</div>
<button
type="button"
onClick={backupRelays}
className="inline-flex h-8 w-max items-center justify-center gap-2 rounded-md bg-blue-500 pl-2 pr-3 font-medium text-white shadow shadow-blue-500/50 hover:bg-blue-600"
>
{status.value ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
<RunIcon className="size-4" />
)}
Backup
</button>
</div>
</div>
);
}

View File

@ -1,96 +1,215 @@
import { NDKKind } from '@nostr-dev-kit/ndk';
import { useSignal } from '@preact/signals-react';
import * as Collapsible from '@radix-ui/react-collapsible';
import { appConfigDir } from '@tauri-apps/api/path';
import { invoke } from '@tauri-apps/api/primitives';
import { useEffect } from 'react';
import { toast } from 'sonner';
import { DepotContactCard } from '@app/depot/components/contact';
import { DepotMembers } from '@app/depot/components/members';
import { DepotProfileCard } from '@app/depot/components/profile';
import { DepotRelaysCard } from '@app/depot/components/relays';
import { useArk } from '@libs/ark';
import { ChevronDownIcon, DepotIcon, GossipIcon } from '@shared/icons';
export function DepotScreen() {
const ark = useArk();
const dataPath = useSignal('');
const tunnelUrl = useSignal('');
const openFolder = async () => {
await invoke('show_in_folder', {
path: dataPath.value + '/nostr.db',
});
};
const updateRelayList = async () => {
try {
if (tunnelUrl.value.length < 1) return toast.info('Please enter a valid relay url');
if (!tunnelUrl.value.startsWith('ws'))
return toast.info('Please enter a valid relay url');
const relayUrl = new URL(tunnelUrl.value.replace(/\s/g, ''));
if (!/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/.test(relayUrl.host)) return;
const relayEvent = await ark.getEventByFilter({
filter: { authors: [ark.account.pubkey], kinds: [NDKKind.RelayList] },
});
let publish: { id: string; seens: string[] };
if (!relayEvent) {
publish = await ark.createEvent({
kind: NDKKind.RelayList,
tags: [['r', tunnelUrl.value, '']],
});
}
const newTags = relayEvent.tags ?? [];
newTags.push(['r', tunnelUrl.value, '']);
publish = await ark.createEvent({
kind: NDKKind.RelayList,
tags: newTags,
});
if (publish) {
await ark.createSetting('tunnel_url', tunnelUrl.value);
toast.success('Update relay list successfully.');
tunnelUrl.value = '';
}
} catch (e) {
console.error(e);
toast.error('Error');
}
};
useEffect(() => {
async function loadConfig() {
const appDir = await appConfigDir();
dataPath.value = appDir;
}
loadConfig();
}, []);
return (
<div className="px-16 py-14">
<div className="mx-auto w-full max-w-md">
<div className="flex flex-col gap-10">
<div className="text-center">
<h1 className="mb-1 text-2xl font-semibold text-neutral-400 dark:text-neutral-600">
Your Depot is running
</h1>
<div className="flex items-center justify-center gap-2.5">
<span className="relative flex h-3 w-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-teal-400 opacity-75"></span>
<span className="relative inline-flex h-3 w-3 rounded-full bg-teal-500"></span>
</span>
<p className="font-medium">ws://localhost:6090</p>
<div className="flex h-full w-full rounded-xl shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
<div className="h-full w-72 shrink-0 rounded-l-xl bg-white/50 px-8 pt-8 backdrop-blur-xl dark:bg-black/50">
<div className="flex flex-col justify-center gap-4">
<div className="size-16 rounded-xl bg-gradient-to-bl from-teal-300 to-teal-600 p-1">
<div className="relative inline-flex h-full w-full items-center justify-center overflow-hidden rounded-lg bg-gradient-to-bl from-teal-400 to-teal-700 shadow-sm shadow-white/20">
<DepotIcon className="size-8 text-white" />
</div>
</div>
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between gap-6">
<div>
<h3 className="text-lg font-semibold">Backup</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Sync all your data to Depot.
</p>
<h1 className="text-xl font-semibold">Depot is running</h1>
</div>
<div className="mt-8 flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<div className="text-sm font-medium">Relay URL</div>
<div className="inline-flex h-10 w-full select-text items-center rounded-lg bg-black/10 px-3 text-sm backdrop-blur-xl dark:bg-white/10">
ws://localhost:6090
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="text-sm font-medium">Database</div>
<div className="inline-flex h-10 w-full items-center gap-2 truncate rounded-lg bg-black/10 p-1 backdrop-blur-xl dark:bg-white/10">
<p className="shrink-0 pl-2 text-sm">nostr.db (SQLite)</p>
<button
type="button"
className="inline-flex h-9 w-20 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
onClick={openFolder}
className="inline-flex h-full w-full items-center justify-center rounded-md bg-white text-sm font-medium shadow hover:bg-blue-500 hover:text-white dark:bg-black"
>
Sync
Open
</button>
</div>
<div className="flex items-center justify-between gap-6">
<div>
</div>
</div>
</div>
<div className="flex-1 overflow-y-auto rounded-r-xl bg-white pb-20 dark:bg-black">
<div className="mb-5 flex h-12 items-center border-b border-neutral-100 px-5 dark:border-neutral-900">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Actions
</h3>
</div>
<div className="flex flex-col gap-5 px-5">
<Collapsible.Root
defaultOpen
className="flex flex-col overflow-hidden rounded-xl border border-transparent bg-neutral-50 data-[state=open]:border-blue-500 dark:bg-neutral-950"
>
<Collapsible.Trigger className="flex h-20 items-center justify-between px-5 hover:bg-neutral-100 dark:hover:bg-neutral-900">
<div className="flex flex-col items-start">
<h3 className="text-lg font-semibold">Expose</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Help other users can see your depot on Internet. You also can do it by
yourself by using other service like ngrok or localtunnel.
<p className="text-neutral-700 dark:text-neutral-300">
Make your Depot visible in the Internet, everyone can connect into it.
</p>
</div>
<button
type="button"
className="inline-flex h-9 w-20 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Start
</button>
</div>
<div className="flex items-center justify-between gap-6">
<ChevronDownIcon className="size-5 shrink-0" />
</Collapsible.Trigger>
<Collapsible.Content>
<div className="flex w-full flex-col gap-4 p-5">
<div>
<h3 className="text-lg font-semibold">Relay Hint</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Instruct other Nostr client find your events in this depot.
</p>
<p className="mb-1 font-medium">ngrok</p>
<input
readOnly
value="ngrok http --domain=<your_domain> 6090"
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 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"
/>
</div>
<button
type="button"
className="inline-flex h-9 w-20 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Add
</button>
</div>
<div className="flex items-center justify-between gap-6">
<div>
<h3 className="text-lg font-semibold">Invite</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
By default, only you can write event to Depot, but you can invite other
user to your Depot.
</p>
<p className="mb-1 font-medium">Cloudflare Tunnel</p>
<input
readOnly
value="cloudflared tunnel --url localhost:6090"
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 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"
/>
</div>
<button
type="button"
className="inline-flex h-9 w-20 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Invite
</button>
</div>
<div className="flex items-center justify-between gap-6">
<div>
<h3 className="text-lg font-semibold">Customize</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Depot also provide plenty config to customize your experiences.
</p>
<p className="mb-1 font-medium">Local Tunnel</p>
<input
readOnly
value="lt --port 6090"
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 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"
/>
</div>
<div className="mt-4 border-t border-neutral-100 pt-4 dark:border-neutral-900">
<div className="inline-flex items-center gap-2">
<GossipIcon className="size-5 text-blue-500" />
<h3 className="mb-1 font-semibold">
Support Gossip Model (Recommended)
</h3>
</div>
<div className="w-full max-w-xl">
<p className=" text-balance">
By adding to Relay List, other Nostr Client which support Gossip
Model will automatically connect to your Depot and improve the
discoverability.
</p>
<div className="mt-2 inline-flex w-full items-center gap-2">
<input
type="text"
value={tunnelUrl.value}
onChange={(e) => (tunnelUrl.value = e.target.value)}
spellCheck={false}
placeholder="wss://"
className="h-10 flex-1 rounded-lg border-transparent bg-neutral-100 px-3 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"
className="inline-flex h-9 w-20 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
onClick={updateRelayList}
className="inline-flex h-10 w-max shrink-0 items-center justify-center rounded-lg bg-blue-500 px-4 font-medium text-white hover:bg-blue-600"
>
Config
Update
</button>
</div>
</div>
</div>
</div>
</Collapsible.Content>
</Collapsible.Root>
<Collapsible.Root className="flex flex-col overflow-hidden rounded-xl border border-transparent bg-neutral-50 data-[state=open]:border-blue-500 dark:bg-neutral-950">
<Collapsible.Trigger className="flex h-20 items-center justify-between px-5 hover:bg-neutral-100 dark:hover:bg-neutral-900">
<div className="flex flex-col items-start">
<h3 className="text-lg font-semibold">Backup (Recommended)</h3>
<p className="text-neutral-700 dark:text-neutral-300">
Backup all your data to Depot, it always live on your machine.
</p>
</div>
<ChevronDownIcon className="size-5 shrink-0" />
</Collapsible.Trigger>
<Collapsible.Content>
<div className="grid grid-cols-3 gap-4 px-5 py-5">
<DepotProfileCard />
<DepotContactCard />
<DepotRelaysCard />
</div>
</Collapsible.Content>
</Collapsible.Root>
<DepotMembers />
</div>
</div>
</div>
);
}

View File

@ -51,7 +51,7 @@ export function DepotOnboardingScreen() {
};
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-10">
<div className="flex h-full w-full flex-col items-center justify-center gap-10 rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
<div className="flex flex-col items-center gap-8">
<div className="text-center">
<h1 className="mb-1 text-3xl font-semibold text-neutral-400 dark:text-neutral-600">

View File

@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { useRef } from 'react';
import { VList, VListHandle } from 'virtua';
import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons';
import { LoaderIcon, PlusIcon } from '@shared/icons';
import {
ArticleWidget,
FileWidget,
@ -132,7 +132,7 @@ export function HomeScreen() {
}}
>
{data.map((widget) => renderItem(widget))}
<ToggleWidgetList />
<div className="h-full w-[200px]" />
</VList>
</div>
);

View File

@ -1,5 +1,7 @@
import * as Avatar from '@radix-ui/react-avatar';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { minidenticon } from 'minidenticons';
import { nip19 } from 'nostr-tools';
import { Link } from 'react-router-dom';
import { useArk } from '@libs/ark';
import { EditIcon, LoaderIcon } from '@shared/icons';
@ -8,12 +10,16 @@ import { useProfile } from '@utils/hooks/useProfile';
export function ProfileCard() {
const ark = useArk();
const { isLoading, user } = useProfile(ark.account.pubkey);
const svgURI =
'data:image/svg+xml;utf8,' +
encodeURIComponent(minidenticon(ark.account.pubkey, 90, 50));
const { isLoading, user } = useProfile(ark.account.pubkey);
const copyNpub = async () => {
return await writeText(nip19.npubEncode(ark.account.pubkey));
};
return (
<div className="mb-4 h-56 w-full rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
{isLoading ? (
@ -22,7 +28,14 @@ export function ProfileCard() {
</div>
) : (
<div className="flex h-full w-full flex-col justify-between p-4">
<div className="flex h-10 w-full justify-end">
<div className="flex h-10 w-full justify-end gap-3">
<button
type="button"
onClick={copyNpub}
className="inline-flex h-8 w-28 transform items-center justify-center gap-1.5 rounded-full bg-neutral-200 text-sm font-medium hover:bg-neutral-400 active:translate-y-1 dark:bg-neutral-800 dark:hover:bg-neutral-600"
>
Copy NPUB
</button>
<Link
to="/settings/edit-profile"
className="inline-flex h-8 w-20 items-center justify-center gap-1.5 rounded-full bg-neutral-200 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-600"

View File

@ -27,6 +27,7 @@ import {
} from 'nostr-fetch';
import { nip19 } from 'nostr-tools';
import { NDKCacheAdapterTauri } from '@libs/cache';
import { delay } from '@utils/delay';
import {
type Account,
type NDKCacheUser,
@ -52,6 +53,7 @@ export class Ark {
media: boolean;
hashtag: boolean;
depot: boolean;
tunnelUrl: string;
};
constructor({ storage, platform }: { storage: Database; platform: Platform }) {
@ -64,6 +66,7 @@ export class Ark {
media: true,
hashtag: true,
depot: false,
tunnelUrl: '',
};
}
@ -77,29 +80,11 @@ export class Ark {
public async connectDepot() {
if (!this.#depot) return;
// connect
this.ndk.addExplicitRelay(new NDKRelay('ws://localhost:6090'), undefined, true);
const relayEvent = await this.ndk.fetchEvent({
kinds: [NDKKind.RelayList],
authors: [this.account.pubkey],
});
if (!relayEvent) {
// create new relay list
return await this.createEvent({
kind: NDKKind.RelayList,
tags: [['r', 'ws://localhost:6090', '']],
});
}
// update old relay list
relayEvent.tags.push(['r', 'ws://localhost:6090', '']);
return await this.createEvent({
kind: NDKKind.RelayList,
tags: relayEvent.tags,
});
return this.ndk.addExplicitRelay(
new NDKRelay(normalizeRelayUrl('ws://localhost:6090')),
undefined,
true
);
}
public checkDepot() {
@ -146,7 +131,10 @@ export class Ark {
const localSigner = new NDKPrivateKeySigner(localSignerPrivkey);
const bunker = new NDK({
explicitRelayUrls: ['wss://relay.nsecbunker.com', 'wss://nostr.vulpem.com'],
explicitRelayUrls: normalizeRelayUrlSet([
'wss://relay.nsecbunker.com/',
'wss://nostr.vulpem.com/',
]),
});
await bunker.connect(3000);
@ -183,6 +171,7 @@ export class Ark {
if (item.key === 'hashtag') this.settings.hashtag = !!parseInt(item.value);
if (item.key === 'autoupdate') this.settings.autoupdate = !!parseInt(item.value);
if (item.key === 'depot') this.settings.depot = !!parseInt(item.value);
if (item.key === 'tunnel_url') this.settings.tunnelUrl = item.value;
}
const explicitRelayUrls = normalizeRelayUrlSet([
@ -191,11 +180,23 @@ export class Ark {
'wss://nostr.mutinywallet.com',
]);
if (this.settings.depot) {
await this.launchDepot();
await delay(2000);
explicitRelayUrls.push(normalizeRelayUrl('ws://localhost:6090'));
}
// #TODO: user should config outbox relays
const outboxRelayUrls = normalizeRelayUrlSet(['wss://purplepag.es']);
// #TODO: user should config blacklist relays
const blacklistRelayUrls = normalizeRelayUrlSet(['wss://brb.io']);
// No need to connect depot tunnel url
const blacklistRelayUrls = this.settings.tunnelUrl.length
? [this.settings.tunnelUrl, this.settings.tunnelUrl + '/']
: [];
console.log(blacklistRelayUrls);
const cacheAdapter = new NDKCacheAdapterTauri(this.#storage);
const ndk = new NDK({
@ -215,7 +216,7 @@ export class Ark {
if (signer) ndk.signer = signer;
// connect
await ndk.connect(5000);
await ndk.connect(3000);
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
// update account's metadata
@ -373,25 +374,17 @@ export class Ark {
}
public async createSetting(key: string, value: string | undefined) {
if (value) {
const currentSetting = await this.checkSettingValue(key);
if (!currentSetting) {
return await this.#storage.execute(
'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);',
[key, value]
);
}
const currentSetting = await this.checkSettingValue(key);
if (!currentSetting)
return await this.#storage.execute(
'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);',
[key, value]
);
const currentValue = !!parseInt(currentSetting);
return await this.#storage.execute('UPDATE settings SET value = $1 WHERE key = $2;', [
+!currentValue,
value,
key,
]);
}

View File

@ -8,7 +8,6 @@ import { PropsWithChildren, createContext, useContext, useEffect, useState } fro
import { Ark } from '@libs/ark';
import { LoaderIcon } from '@shared/icons';
import { QUOTES } from '@utils/constants';
import { delay } from '@utils/delay';
const ArkContext = createContext<Ark>(undefined);
@ -36,12 +35,6 @@ const ArkProvider = ({ children }: PropsWithChildren<object>) => {
}
}
// start depot
if (_ark.settings.depot) {
await _ark.launchDepot();
await delay(2000);
}
setArk(_ark);
} catch (e) {
console.error(e);

View File

@ -1,14 +1,18 @@
import { SVGProps } from 'react';
export function CancelIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
export function CancelIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M4.75 4.75L19.25 19.25M19.25 4.75L4.75 19.25"
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
/>
strokeLinejoin="round"
strokeWidth="2"
>
<path d="m6 18 6-6m0 0 6-6m-6 6L6 6m6 6 6 6" />
</svg>
);
}

View File

@ -0,0 +1,20 @@
export function GossipIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M8.577 5.764c.46-.893.69-1.34.999-1.485a1 1 0 0 1 .848 0c.31.145.539.592.999 1.485l1.191 2.315c.08.154.12.232.171.3a1 1 0 0 0 .157.165c.065.055.14.098.291.186l2.386 1.387c.782.454 1.173.681 1.305.977a1 1 0 0 1 0 .812c-.132.296-.523.523-1.305.977l-2.386 1.387c-.15.088-.226.131-.291.186-.059.049-.111.104-.157.165-.051.068-.091.146-.17.3l-1.192 2.315c-.46.893-.69 1.34-.999 1.485a1 1 0 0 1-.848 0c-.309-.145-.539-.592-.999-1.485l-1.191-2.315c-.08-.154-.12-.232-.171-.3a1.003 1.003 0 0 0-.157-.165 2.099 2.099 0 0 0-.291-.186l-2.386-1.387c-.782-.454-1.173-.681-1.305-.977a1 1 0 0 1 0-.812c.132-.296.523-.523 1.305-.977L6.767 8.73c.15-.088.226-.131.291-.186a1 1 0 0 0 .157-.165c.051-.068.091-.146.17-.3l1.192-2.315Z" />
<path d="M17.46 19.406c-.254-.317-.381-.476-.429-.659a.993.993 0 0 1 0-.494c.048-.183.175-.342.429-.66l.815-1.019c.254-.317.38-.475.527-.535a.52.52 0 0 1 .396 0c.146.06.273.218.527.535l.816 1.02c.253.317.38.476.428.659a.993.993 0 0 1 0 .494c-.048.183-.175.342-.428.66l-.816 1.019c-.254.317-.38.475-.527.535a.52.52 0 0 1-.396 0c-.146-.06-.273-.218-.527-.535l-.815-1.02Z" />
<path d="M18.23 4.362c-.127-.126-.19-.19-.214-.263a.32.32 0 0 1 0-.198c.024-.073.087-.137.214-.263l.408-.408c.126-.127.19-.19.263-.214a.32.32 0 0 1 .198 0c.073.023.137.087.264.214l.407.408c.127.126.19.19.214.263a.319.319 0 0 1 0 .198c-.023.073-.087.137-.214.263l-.407.408c-.127.127-.19.19-.264.214a.32.32 0 0 1-.198 0c-.073-.023-.137-.087-.263-.214l-.408-.408Z" />
</svg>
);
}

View File

@ -87,3 +87,7 @@ export * from './system';
export * from './announcement';
export * from './depot';
export * from './search';
export * from './run';
export * from './gossip';
export * from './userAdd';
export * from './userRemove';

18
src/shared/icons/run.tsx Normal file
View File

@ -0,0 +1,18 @@
export function RunIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M7 11.808c0-2.643 0-3.964.552-4.702a2.768 2.768 0 0 1 2.02-1.102c.918-.065 2.03.649 4.253 2.078l.298.192c1.93 1.24 2.894 1.86 3.227 2.649a2.768 2.768 0 0 1 0 2.155c-.333.788-1.298 1.408-3.227 2.648l-.298.192c-2.223 1.429-3.335 2.143-4.254 2.078a2.769 2.769 0 0 1-2.019-1.102C7 16.156 7 14.834 7 12.192v-.384Z" />
</svg>
);
}

View File

@ -0,0 +1,18 @@
export function UserAddIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M11 15H7a4 4 0 0 0-4 4 2 2 0 0 0 2 2h10m3-3v-3m0 0v-3m0 3h-3m3 0h3m-6-8a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z" />
</svg>
);
}

View File

@ -0,0 +1,18 @@
export function UserRemoveIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M11 15H7a4 4 0 0 0-4 4 2 2 0 0 0 2 2h10m0-6h6m-6-8a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z" />
</svg>
);
}

View File

@ -1,6 +1,7 @@
import { type Platform } from '@tauri-apps/plugin-os';
import { Outlet, ScrollRestoration } from 'react-router-dom';
import { Outlet } from 'react-router-dom';
import { twMerge } from 'tailwind-merge';
import { Navigation } from '@shared/navigation';
import { WindowTitleBar } from '@shared/titlebar';
export function AppLayout({ platform }: { platform: Platform }) {
@ -8,7 +9,7 @@ export function AppLayout({ platform }: { platform: Platform }) {
<div
className={twMerge(
'flex h-screen w-screen flex-col',
platform !== 'macos' ? 'bg-neutral-50 dark:bg-neutral-950' : ''
platform !== 'macos' ? 'bg-blue-50 dark:bg-blue-950' : ''
)}
>
{platform !== 'macos' ? (
@ -16,9 +17,11 @@ export function AppLayout({ platform }: { platform: Platform }) {
) : (
<div data-tauri-drag-region className="h-9 shrink-0" />
)}
<div className="h-full w-full">
<div className="flex h-full min-h-0 w-full">
<Navigation />
<div className="h-full flex-1 px-1 pb-1">
<Outlet />
<ScrollRestoration />
</div>
</div>
</div>
);

View File

@ -1,13 +1,9 @@
import { Outlet } from 'react-router-dom';
import { Navigation } from '@shared/navigation';
export function HomeLayout() {
return (
<div className="flex h-full w-full">
<Navigation />
<div className="min-h-0 flex-1 rounded-tl-lg bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
<div className="h-full w-full rounded-xl overflow-hidden bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
<Outlet />
</div>
</div>
);
}

View File

@ -24,12 +24,12 @@ export function ChildNote({ id, isRoot }: { id: string; isRoot?: boolean }) {
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
<div className="absolute right-0 top-[18px] h-3 w-3 -translate-y-1/2 translate-x-1/2 rotate-45 transform bg-neutral-200 dark:bg-neutral-800"></div>
<div className="break-p mt-6 line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
{data.content}
{data?.content}
</div>
</div>
<User
pubkey={data.pubkey}
time={data.created_at}
pubkey={data?.pubkey}
time={data?.created_at}
variant="childnote"
subtext={isRoot ? 'posted' : 'replied'}
/>