feat: improve ui
This commit is contained in:
@@ -25,6 +25,7 @@
|
|||||||
"nostr-tools": "^2.7.1",
|
"nostr-tools": "^2.7.1",
|
||||||
"react": "19.0.0-rc-d025ddd3-20240722",
|
"react": "19.0.0-rc-d025ddd3-20240722",
|
||||||
"react-dom": "19.0.0-rc-d025ddd3-20240722",
|
"react-dom": "19.0.0-rc-d025ddd3-20240722",
|
||||||
|
"unique-names-generator": "^4.7.1",
|
||||||
"virtua": "^0.33.3"
|
"virtua": "^0.33.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -53,6 +53,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: 19.0.0-rc-d025ddd3-20240722
|
specifier: 19.0.0-rc-d025ddd3-20240722
|
||||||
version: 19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722)
|
version: 19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722)
|
||||||
|
unique-names-generator:
|
||||||
|
specifier: ^4.7.1
|
||||||
|
version: 4.7.1
|
||||||
virtua:
|
virtua:
|
||||||
specifier: ^0.33.3
|
specifier: ^0.33.3
|
||||||
version: 0.33.3(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)
|
version: 0.33.3(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)
|
||||||
@@ -1462,6 +1465,10 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
unique-names-generator@4.7.1:
|
||||||
|
resolution: {integrity: sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
unplugin@1.11.0:
|
unplugin@1.11.0:
|
||||||
resolution: {integrity: sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==}
|
resolution: {integrity: sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -2824,6 +2831,8 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.5.4: {}
|
typescript@5.5.4: {}
|
||||||
|
|
||||||
|
unique-names-generator@4.7.1: {}
|
||||||
|
|
||||||
unplugin@1.11.0:
|
unplugin@1.11.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.12.1
|
acorn: 8.12.1
|
||||||
|
|||||||
11
src-tauri/Cargo.lock
generated
11
src-tauri/Cargo.lock
generated
@@ -929,6 +929,7 @@ dependencies = [
|
|||||||
"tauri-plugin-devtools",
|
"tauri-plugin-devtools",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-os",
|
"tauri-plugin-os",
|
||||||
|
"tauri-plugin-prevent-default",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tauri-specta",
|
"tauri-specta",
|
||||||
]
|
]
|
||||||
@@ -5092,6 +5093,16 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-prevent-default"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38be0ac8fcc5fa03422409fc506015b01dc29cc8a3a72572c68d41e6f10f4491"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"tauri",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-shell"
|
name = "tauri-plugin-shell"
|
||||||
version = "2.0.0-beta.9"
|
version = "2.0.0-beta.9"
|
||||||
|
|||||||
@@ -12,16 +12,18 @@ tauri-build = { version = "2.0.0-beta", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
|
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
|
||||||
"sqlite",
|
"sqlite",
|
||||||
] }
|
] }
|
||||||
tauri = { version = "2.0.0-beta", features = [
|
tauri = { version = "2.0.0-beta", features = [
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"macos-private-api",
|
"macos-private-api",
|
||||||
"protocol-asset",
|
"protocol-asset",
|
||||||
] }
|
] }
|
||||||
tauri-specta = { git = "https://github.com/reyamir/tauri-specta", branch = "feat/tauri-v2", features = [
|
tauri-specta = { git = "https://github.com/reyamir/tauri-specta", branch = "feat/tauri-v2", features = [
|
||||||
"typescript",
|
"typescript",
|
||||||
] }
|
] }
|
||||||
|
tauri-plugin-devtools = "2.0.0-beta"
|
||||||
|
tauri-plugin-prevent-default = "0.1"
|
||||||
tauri-plugin-os = "2.0.0-beta"
|
tauri-plugin-os = "2.0.0-beta"
|
||||||
tauri-plugin-clipboard-manager = "2.0.0-beta"
|
tauri-plugin-clipboard-manager = "2.0.0-beta"
|
||||||
tauri-plugin-dialog = "2.0.0-beta"
|
tauri-plugin-dialog = "2.0.0-beta"
|
||||||
@@ -30,15 +32,14 @@ tauri-plugin-decorum = "0.1.5"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
keyring = { version = "3", features = [
|
keyring = { version = "3", features = [
|
||||||
"apple-native",
|
"apple-native",
|
||||||
"windows-native",
|
"windows-native",
|
||||||
"linux-native",
|
"linux-native",
|
||||||
] }
|
] }
|
||||||
keyring-search = "1.2.0"
|
keyring-search = "1.2.0"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
specta = "^2.0.0-rc.12"
|
specta = "^2.0.0-rc.12"
|
||||||
tauri-plugin-devtools = "2.0.0-beta"
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
border = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }
|
border = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }
|
||||||
|
|||||||
@@ -155,55 +155,6 @@ pub async fn login(
|
|||||||
let hex = public_key.to_hex();
|
let hex = public_key.to_hex();
|
||||||
let keyring = Entry::new(&id, "nostr_secret").expect("Unexpected.");
|
let keyring = Entry::new(&id, "nostr_secret").expect("Unexpected.");
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
let window = app.get_webview_window("main").expect("Window is terminated.");
|
|
||||||
let state = window.state::<Nostr>();
|
|
||||||
let client = &state.client;
|
|
||||||
|
|
||||||
client
|
|
||||||
.handle_notifications(|notification| async {
|
|
||||||
if let RelayPoolNotification::Message { message, relay_url } = notification {
|
|
||||||
if let RelayMessage::Event { event, .. } = message {
|
|
||||||
if event.kind == Kind::GiftWrap {
|
|
||||||
if let Ok(UnwrappedGift { rumor, sender }) =
|
|
||||||
client.unwrap_gift_wrap(&event).await
|
|
||||||
{
|
|
||||||
window
|
|
||||||
.emit(
|
|
||||||
"event",
|
|
||||||
Payload { event: rumor.as_json(), sender: sender.to_hex() },
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let RelayMessage::Auth { challenge } = message {
|
|
||||||
match client.auth(challenge, relay_url.clone()).await {
|
|
||||||
Ok(..) => {
|
|
||||||
println!("Authenticated to {} relay.", relay_url);
|
|
||||||
|
|
||||||
if let Ok(relay) = client.relay(relay_url).await {
|
|
||||||
let opts = RelaySendOptions::new().skip_send_confirmation(true);
|
|
||||||
if let Err(e) = relay.resubscribe(opts).await {
|
|
||||||
println!(
|
|
||||||
"Impossible to resubscribe to '{}': {e}",
|
|
||||||
relay.url()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Can't authenticate to '{relay_url}' relay: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("relay message: {}", message.as_json());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
});
|
|
||||||
|
|
||||||
let password = match keyring.get_password() {
|
let password = match keyring.get_password() {
|
||||||
Ok(pw) => pw,
|
Ok(pw) => pw,
|
||||||
Err(_) => return Err("Cancelled".into()),
|
Err(_) => return Err("Cancelled".into()),
|
||||||
@@ -256,12 +207,41 @@ pub async fn login(
|
|||||||
|
|
||||||
if client.reconcile_with(relays.clone(), old, NegentropyOptions::default()).await.is_ok() {
|
if client.reconcile_with(relays.clone(), old, NegentropyOptions::default()).await.is_ok() {
|
||||||
handle.emit("synchronized", ()).unwrap();
|
handle.emit("synchronized", ()).unwrap();
|
||||||
println!("synchronized");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if client.subscribe_to(relays, vec![new], None).await.is_ok() {
|
if client.subscribe_to(relays, vec![new], None).await.is_ok() {
|
||||||
println!("Waiting for new message...")
|
println!("Waiting for new message...")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let window = app.get_webview_window("main").expect("Window is terminated.");
|
||||||
|
let state = window.state::<Nostr>();
|
||||||
|
let client = &state.client;
|
||||||
|
|
||||||
|
// Workaround for https://github.com/rust-nostr/nostr/issues/509
|
||||||
|
// TODO: remove
|
||||||
|
let _ = client.get_events_of(vec![Filter::new().kind(Kind::TextNote).limit(0)], None).await;
|
||||||
|
|
||||||
|
client
|
||||||
|
.handle_notifications(|notification| async {
|
||||||
|
if let RelayPoolNotification::Event { event, .. } = notification {
|
||||||
|
if event.kind == Kind::GiftWrap {
|
||||||
|
if let Ok(UnwrappedGift { rumor, sender }) =
|
||||||
|
client.unwrap_gift_wrap(&event).await
|
||||||
|
{
|
||||||
|
window
|
||||||
|
.emit(
|
||||||
|
"event",
|
||||||
|
Payload { event: rumor.as_json(), sender: sender.to_hex() },
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
|
||||||
Ok(hex)
|
Ok(hex)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ pub async fn get_chats(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
|||||||
|
|
||||||
let uniqs = rumors
|
let uniqs = rumors
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.sorted_by_key(|ev| Reverse(ev.created_at))
|
||||||
.filter(|ev| ev.pubkey != public_key)
|
.filter(|ev| ev.pubkey != public_key)
|
||||||
.unique_by(|ev| ev.pubkey)
|
.unique_by(|ev| ev.pubkey)
|
||||||
.sorted_by_key(|ev| Reverse(ev.created_at))
|
|
||||||
.map(|ev| ev.as_json())
|
.map(|ev| ev.as_json())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
|||||||
@@ -76,9 +76,8 @@ fn main() {
|
|||||||
// Config
|
// Config
|
||||||
let opts = Options::new()
|
let opts = Options::new()
|
||||||
.autoconnect(true)
|
.autoconnect(true)
|
||||||
.automatic_authentication(false)
|
|
||||||
.timeout(Duration::from_secs(5))
|
.timeout(Duration::from_secs(5))
|
||||||
.send_timeout(Some(Duration::from_secs(50)))
|
.send_timeout(Some(Duration::from_secs(5)))
|
||||||
.connection_timeout(Some(Duration::from_secs(20)));
|
.connection_timeout(Some(Duration::from_secs(20)));
|
||||||
|
|
||||||
// Setup nostr client
|
// Setup nostr client
|
||||||
@@ -98,6 +97,7 @@ fn main() {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.enable_macos_default_menu(false)
|
.enable_macos_default_menu(false)
|
||||||
|
.plugin(tauri_plugin_prevent_default::init())
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
|||||||
@@ -17,26 +17,29 @@ export function UserAvatar({ className }: { className?: string }) {
|
|||||||
return (
|
return (
|
||||||
<Avatar.Root
|
<Avatar.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 block overflow-hidden bg-neutral-200 dark:bg-neutral-800",
|
"shrink-0 block overflow-hidden bg-black/10 dark:bg-white/10",
|
||||||
|
user.isLoading ? "animate-pulse" : "",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{user?.profile?.picture ? (
|
{!user.isLoading ? (
|
||||||
<Avatar.Image
|
<>
|
||||||
src={`//wsrv.nl/?url=${user.profile?.picture}&w=200&h=200`}
|
<Avatar.Image
|
||||||
alt={user.pubkey}
|
src={`//wsrv.nl/?url=${user.profile?.picture}&w=200&h=200`}
|
||||||
loading="lazy"
|
alt={user.pubkey}
|
||||||
decoding="async"
|
loading="lazy"
|
||||||
className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
decoding="async"
|
||||||
/>
|
className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
||||||
|
/>
|
||||||
|
<Avatar.Fallback>
|
||||||
|
<img
|
||||||
|
src={fallback}
|
||||||
|
alt={user.pubkey}
|
||||||
|
className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
||||||
|
/>
|
||||||
|
</Avatar.Fallback>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<Avatar.Fallback>
|
|
||||||
<img
|
|
||||||
src={fallback}
|
|
||||||
alt={user.pubkey}
|
|
||||||
className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
|
||||||
/>
|
|
||||||
</Avatar.Fallback>
|
|
||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
import { cn } from "@/commons";
|
import { cn } from "@/commons";
|
||||||
import { useUserContext } from "./provider";
|
import { useUserContext } from "./provider";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { uniqueNamesGenerator, names } from "unique-names-generator";
|
||||||
|
|
||||||
export function UserName({ className }: { className?: string }) {
|
export function UserName({ className }: { className?: string }) {
|
||||||
const user = useUserContext();
|
const user = useUserContext();
|
||||||
|
const name = useMemo(
|
||||||
|
() => uniqueNamesGenerator({ dictionaries: [names] }),
|
||||||
|
[user.pubkey],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (user.isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="size-4 w-20 bg-black/10 dark:bg-white/10 animate-pulse" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("max-w-[12rem] truncate", className)}>
|
<div className={cn("max-w-[12rem] truncate", className)}>
|
||||||
{user.profile?.display_name || user.profile?.name || "Anon"}
|
{user.profile?.display_name || user.profile?.name || name}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,9 @@ function Screen() {
|
|||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="shrink-0 w-[280px] h-full flex flex-col justify-between border-r border-black/5 dark:border-white/5"
|
className="shrink-0 w-[280px] h-full flex flex-col justify-between border-r border-black/5 dark:border-white/5"
|
||||||
>
|
>
|
||||||
<div data-tauri-drag-region className="flex-1">
|
<Header />
|
||||||
<Header />
|
<ChatList />
|
||||||
<ChatList />
|
<CurrentUser />
|
||||||
</div>
|
|
||||||
<div className="h-12 shrink-0 flex items-center px-2.5 border-t border-black/5 dark:border-white/5">
|
|
||||||
<CurrentUser />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0 min-h-0 bg-white dark:bg-neutral-900 overflow-auto">
|
<div className="flex-1 min-w-0 min-h-0 bg-white dark:bg-neutral-900 overflow-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
@@ -44,7 +40,7 @@ function Header() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="h-12 px-3.5 flex items-center justify-end"
|
className="shrink-0 h-12 px-3.5 flex items-center justify-end"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
@@ -130,9 +126,19 @@ function ChatList() {
|
|||||||
>
|
>
|
||||||
<ScrollArea.Viewport className="relative h-full px-1.5">
|
<ScrollArea.Viewport className="relative h-full px-1.5">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<p>Loading...</p>
|
<div>
|
||||||
|
{Array.from(Array(5)).map((index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center rounded-lg p-2 mb-1 gap-2"
|
||||||
|
>
|
||||||
|
<div className="size-9 rounded-full animate-pulse bg-black/10 dark:bg-white/10" />
|
||||||
|
<div className="size-4 w-20 rounded animate-pulse bg-black/10 dark:bg-white/10" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
) : isError ? (
|
) : isError ? (
|
||||||
<p>Error</p>
|
<div>Error</div>
|
||||||
) : (
|
) : (
|
||||||
data.map((item) => (
|
data.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -148,7 +154,7 @@ function ChatList() {
|
|||||||
isActive ? "bg-black/5 dark:bg-white/5" : "",
|
isActive ? "bg-black/5 dark:bg-white/5" : "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<User.Avatar className="shrink-0 size-9 rounded-full object-cover" />
|
<User.Avatar className="size-9 rounded-full" />
|
||||||
<div className="flex-1 inline-flex items-center justify-between text-sm">
|
<div className="flex-1 inline-flex items-center justify-between text-sm">
|
||||||
<div className="inline-flex leading-tight">
|
<div className="inline-flex leading-tight">
|
||||||
<User.Name className="max-w-[8rem] truncate font-semibold" />
|
<User.Name className="max-w-[8rem] truncate font-semibold" />
|
||||||
@@ -182,11 +188,13 @@ function CurrentUser() {
|
|||||||
const { account } = Route.useParams();
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<User.Provider pubkey={account}>
|
<div className="shrink-0 h-12 flex items-center px-2.5 border-t border-black/5 dark:border-white/5">
|
||||||
<User.Root className="inline-flex items-center gap-2">
|
<User.Provider pubkey={account}>
|
||||||
<User.Avatar className="size-8 rounded-full object-cover" />
|
<User.Root className="inline-flex items-center gap-2">
|
||||||
<User.Name className="text-sm font-medium leading-tight" />
|
<User.Avatar className="size-8 rounded-full" />
|
||||||
</User.Root>
|
<User.Name className="text-sm font-medium leading-tight" />
|
||||||
</User.Provider>
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user