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

View File

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

View File

@ -63,32 +63,6 @@ export default function App() {
return { Component: RelayScreen }; 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', path: 'new',
element: <ComposerLayout />, 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, privkey: userPrivkey,
}); });
await ark.createEvent({
kind: NDKKind.RelayList,
tags: ark.relays.map((item) => ['r', item, '']),
});
setKeys({ npub: userNpub, nsec: userNsec }); setKeys({ npub: userNpub, nsec: userNsec });
setLoading(false); setLoading(false);
} else { } 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() { 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 ( return (
<div className="px-16 py-14"> <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="mx-auto w-full max-w-md"> <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 gap-10"> <div className="flex flex-col justify-center gap-4">
<div className="text-center"> <div className="size-16 rounded-xl bg-gradient-to-bl from-teal-300 to-teal-600 p-1">
<h1 className="mb-1 text-2xl font-semibold text-neutral-400 dark:text-neutral-600"> <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">
Your Depot is running <DepotIcon className="size-8 text-white" />
</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> </div>
</div> </div>
<div className="flex flex-col gap-6"> <h1 className="text-xl font-semibold">Depot is running</h1>
<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>
</div> </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 <button
type="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> </button>
</div> </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> <h3 className="text-lg font-semibold">Expose</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400"> <p className="text-neutral-700 dark:text-neutral-300">
Help other users can see your depot on Internet. You also can do it by Make your Depot visible in the Internet, everyone can connect into it.
yourself by using other service like ngrok or localtunnel.
</p> </p>
</div> </div>
<button <ChevronDownIcon className="size-5 shrink-0" />
type="button" </Collapsible.Trigger>
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" <Collapsible.Content>
> <div className="flex w-full flex-col gap-4 p-5">
Start
</button>
</div>
<div className="flex items-center justify-between gap-6">
<div> <div>
<h3 className="text-lg font-semibold">Relay Hint</h3> <p className="mb-1 font-medium">ngrok</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400"> <input
Instruct other Nostr client find your events in this depot. readOnly
</p> 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> </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> <div>
<h3 className="text-lg font-semibold">Invite</h3> <p className="mb-1 font-medium">Cloudflare Tunnel</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400"> <input
By default, only you can write event to Depot, but you can invite other readOnly
user to your Depot. value="cloudflared tunnel --url localhost:6090"
</p> 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>
<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> <div>
<h3 className="text-lg font-semibold">Customize</h3> <p className="mb-1 font-medium">Local Tunnel</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400"> <input
Depot also provide plenty config to customize your experiences. readOnly
</p> 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>
<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 <button
type="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> </button>
</div> </div>
</div> </div>
</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> </div>
); );
} }

View File

@ -51,7 +51,7 @@ export function DepotOnboardingScreen() {
}; };
return ( 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="flex flex-col items-center gap-8">
<div className="text-center"> <div className="text-center">
<h1 className="mb-1 text-3xl font-semibold text-neutral-400 dark:text-neutral-600"> <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 { useRef } from 'react';
import { VList, VListHandle } from 'virtua'; import { VList, VListHandle } from 'virtua';
import { useArk } from '@libs/ark'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon, PlusIcon } from '@shared/icons';
import { import {
ArticleWidget, ArticleWidget,
FileWidget, FileWidget,
@ -132,7 +132,7 @@ export function HomeScreen() {
}} }}
> >
{data.map((widget) => renderItem(widget))} {data.map((widget) => renderItem(widget))}
<ToggleWidgetList /> <div className="h-full w-[200px]" />
</VList> </VList>
</div> </div>
); );

View File

@ -1,5 +1,7 @@
import * as Avatar from '@radix-ui/react-avatar'; import * as Avatar from '@radix-ui/react-avatar';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { minidenticon } from 'minidenticons'; import { minidenticon } from 'minidenticons';
import { nip19 } from 'nostr-tools';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useArk } from '@libs/ark'; import { useArk } from '@libs/ark';
import { EditIcon, LoaderIcon } from '@shared/icons'; import { EditIcon, LoaderIcon } from '@shared/icons';
@ -8,12 +10,16 @@ import { useProfile } from '@utils/hooks/useProfile';
export function ProfileCard() { export function ProfileCard() {
const ark = useArk(); const ark = useArk();
const { isLoading, user } = useProfile(ark.account.pubkey);
const svgURI = const svgURI =
'data:image/svg+xml;utf8,' + 'data:image/svg+xml;utf8,' +
encodeURIComponent(minidenticon(ark.account.pubkey, 90, 50)); 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 ( 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"> <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 ? ( {isLoading ? (
@ -22,7 +28,14 @@ export function ProfileCard() {
</div> </div>
) : ( ) : (
<div className="flex h-full w-full flex-col justify-between p-4"> <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 <Link
to="/settings/edit-profile" 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" 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'; } from 'nostr-fetch';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { NDKCacheAdapterTauri } from '@libs/cache'; import { NDKCacheAdapterTauri } from '@libs/cache';
import { delay } from '@utils/delay';
import { import {
type Account, type Account,
type NDKCacheUser, type NDKCacheUser,
@ -52,6 +53,7 @@ export class Ark {
media: boolean; media: boolean;
hashtag: boolean; hashtag: boolean;
depot: boolean; depot: boolean;
tunnelUrl: string;
}; };
constructor({ storage, platform }: { storage: Database; platform: Platform }) { constructor({ storage, platform }: { storage: Database; platform: Platform }) {
@ -64,6 +66,7 @@ export class Ark {
media: true, media: true,
hashtag: true, hashtag: true,
depot: false, depot: false,
tunnelUrl: '',
}; };
} }
@ -77,29 +80,11 @@ export class Ark {
public async connectDepot() { public async connectDepot() {
if (!this.#depot) return; if (!this.#depot) return;
return this.ndk.addExplicitRelay(
// connect new NDKRelay(normalizeRelayUrl('ws://localhost:6090')),
this.ndk.addExplicitRelay(new NDKRelay('ws://localhost:6090'), undefined, true); 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,
});
} }
public checkDepot() { public checkDepot() {
@ -146,7 +131,10 @@ export class Ark {
const localSigner = new NDKPrivateKeySigner(localSignerPrivkey); const localSigner = new NDKPrivateKeySigner(localSignerPrivkey);
const bunker = new NDK({ 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); await bunker.connect(3000);
@ -183,6 +171,7 @@ export class Ark {
if (item.key === 'hashtag') this.settings.hashtag = !!parseInt(item.value); if (item.key === 'hashtag') this.settings.hashtag = !!parseInt(item.value);
if (item.key === 'autoupdate') this.settings.autoupdate = !!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 === 'depot') this.settings.depot = !!parseInt(item.value);
if (item.key === 'tunnel_url') this.settings.tunnelUrl = item.value;
} }
const explicitRelayUrls = normalizeRelayUrlSet([ const explicitRelayUrls = normalizeRelayUrlSet([
@ -191,11 +180,23 @@ export class Ark {
'wss://nostr.mutinywallet.com', '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 // #TODO: user should config outbox relays
const outboxRelayUrls = normalizeRelayUrlSet(['wss://purplepag.es']); const outboxRelayUrls = normalizeRelayUrlSet(['wss://purplepag.es']);
// #TODO: user should config blacklist relays // #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 cacheAdapter = new NDKCacheAdapterTauri(this.#storage);
const ndk = new NDK({ const ndk = new NDK({
@ -215,7 +216,7 @@ export class Ark {
if (signer) ndk.signer = signer; if (signer) ndk.signer = signer;
// connect // connect
await ndk.connect(5000); await ndk.connect(3000);
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk)); const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
// update account's metadata // update account's metadata
@ -373,25 +374,17 @@ export class Ark {
} }
public async createSetting(key: string, value: string | undefined) { public async createSetting(key: string, value: string | undefined) {
if (value) { const currentSetting = await this.checkSettingValue(key);
if (!currentSetting) {
return await this.#storage.execute( return await this.#storage.execute(
'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);', 'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);',
[key, value] [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;', [ return await this.#storage.execute('UPDATE settings SET value = $1 WHERE key = $2;', [
+!currentValue, value,
key, key,
]); ]);
} }

View File

@ -8,7 +8,6 @@ import { PropsWithChildren, createContext, useContext, useEffect, useState } fro
import { Ark } from '@libs/ark'; import { Ark } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { QUOTES } from '@utils/constants'; import { QUOTES } from '@utils/constants';
import { delay } from '@utils/delay';
const ArkContext = createContext<Ark>(undefined); 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); setArk(_ark);
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@ -1,14 +1,18 @@
import { SVGProps } from 'react'; export function CancelIcon(props: JSX.IntrinsicElements['svg']) {
export function CancelIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return ( return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg
<path {...props}
d="M4.75 4.75L19.25 19.25M19.25 4.75L4.75 19.25" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor" stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round" strokeLinecap="round"
/> strokeLinejoin="round"
strokeWidth="2"
>
<path d="m6 18 6-6m0 0 6-6m-6 6L6 6m6 6 6 6" />
</svg> </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 './announcement';
export * from './depot'; export * from './depot';
export * from './search'; 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 { 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 { twMerge } from 'tailwind-merge';
import { Navigation } from '@shared/navigation';
import { WindowTitleBar } from '@shared/titlebar'; import { WindowTitleBar } from '@shared/titlebar';
export function AppLayout({ platform }: { platform: Platform }) { export function AppLayout({ platform }: { platform: Platform }) {
@ -8,7 +9,7 @@ export function AppLayout({ platform }: { platform: Platform }) {
<div <div
className={twMerge( className={twMerge(
'flex h-screen w-screen flex-col', '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' ? ( {platform !== 'macos' ? (
@ -16,9 +17,11 @@ export function AppLayout({ platform }: { platform: Platform }) {
) : ( ) : (
<div data-tauri-drag-region className="h-9 shrink-0" /> <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 /> <Outlet />
<ScrollRestoration /> </div>
</div> </div>
</div> </div>
); );

View File

@ -1,13 +1,9 @@
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { Navigation } from '@shared/navigation';
export function HomeLayout() { export function HomeLayout() {
return ( return (
<div className="flex h-full w-full"> <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)]">
<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)]">
<Outlet /> <Outlet />
</div> </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="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="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"> <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>
</div> </div>
<User <User
pubkey={data.pubkey} pubkey={data?.pubkey}
time={data.created_at} time={data?.created_at}
variant="childnote" variant="childnote"
subtext={isRoot ? 'posted' : 'replied'} subtext={isRoot ? 'posted' : 'replied'}
/> />