mirror of
https://github.com/luminous-devs/lume.git
synced 2024-10-01 09:21:07 +00:00
feat(depot): update screens
This commit is contained in:
parent
a6ca2589ab
commit
4670778181
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
48
src-tauri/src/commands.rs
Normal 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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
50
src/app.tsx
50
src/app.tsx
@ -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 };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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 {
|
||||
|
66
src/app/depot/components/contact.tsx
Normal file
66
src/app/depot/components/contact.tsx
Normal 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>
|
||||
);
|
||||
}
|
145
src/app/depot/components/members.tsx
Normal file
145
src/app/depot/components/members.tsx
Normal 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>
|
||||
);
|
||||
}
|
55
src/app/depot/components/profile.tsx
Normal file
55
src/app/depot/components/profile.tsx
Normal 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>
|
||||
);
|
||||
}
|
67
src/app/depot/components/relays.tsx
Normal file
67
src/app/depot/components/relays.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</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"
|
||||
>
|
||||
Sync
|
||||
</button>
|
||||
<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 className="flex items-center justify-between gap-6">
|
||||
<div>
|
||||
<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>
|
||||
</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"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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"
|
||||
>
|
||||
Config
|
||||
Open
|
||||
</button>
|
||||
</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-neutral-700 dark:text-neutral-300">
|
||||
Make your Depot visible in the Internet, everyone can connect into it.
|
||||
</p>
|
||||
</div>
|
||||
<ChevronDownIcon className="size-5 shrink-0" />
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<div className="flex w-full flex-col gap-4 p-5">
|
||||
<div>
|
||||
<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>
|
||||
<div>
|
||||
<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>
|
||||
<div>
|
||||
<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"
|
||||
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"
|
||||
>
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
]);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<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="m6 18 6-6m0 0 6-6m-6 6L6 6m6 6 6 6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
20
src/shared/icons/gossip.tsx
Normal file
20
src/shared/icons/gossip.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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
18
src/shared/icons/run.tsx
Normal 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>
|
||||
);
|
||||
}
|
18
src/shared/icons/userAdd.tsx
Normal file
18
src/shared/icons/userAdd.tsx
Normal 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>
|
||||
);
|
||||
}
|
18
src/shared/icons/userRemove.tsx
Normal file
18
src/shared/icons/userRemove.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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">
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
<div className="flex h-full min-h-0 w-full">
|
||||
<Navigation />
|
||||
<div className="h-full flex-1 px-1 pb-1">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -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)]">
|
||||
<Outlet />
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -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'}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user