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-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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
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"
|
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");
|
||||||
|
50
src/app.tsx
50
src/app.tsx
@ -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 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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 {
|
||||||
|
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() {
|
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>
|
||||||
<div>
|
<div className="mt-8 flex flex-col gap-4">
|
||||||
<h3 className="text-lg font-semibold">Backup</h3>
|
<div className="flex flex-col gap-1.5">
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
<div className="text-sm font-medium">Relay URL</div>
|
||||||
Sync all your data to Depot.
|
<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">
|
||||||
</p>
|
ws://localhost:6090
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-6">
|
</div>
|
||||||
<div>
|
<div className="flex flex-col gap-1.5">
|
||||||
<h3 className="text-lg font-semibold">Expose</h3>
|
<div className="text-sm font-medium">Database</div>
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
<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">
|
||||||
Help other users can see your depot on Internet. You also can do it by
|
<p className="shrink-0 pl-2 text-sm">nostr.db (SQLite)</p>
|
||||||
yourself by using other service like ngrok or localtunnel.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<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"
|
||||||
>
|
>
|
||||||
Start
|
Open
|
||||||
</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
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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"
|
||||||
|
@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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"
|
||||||
stroke="currentColor"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth={1.5}
|
width="24"
|
||||||
strokeLinecap="round"
|
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>
|
</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 './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
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 { 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">
|
||||||
<Outlet />
|
<Navigation />
|
||||||
<ScrollRestoration />
|
<div className="h-full flex-1 px-1 pb-1">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -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 />
|
<Outlet />
|
||||||
<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>
|
</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="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'}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user