feat: improve ui

This commit is contained in:
reya
2024-07-26 08:25:58 +07:00
parent b6d3f1f24a
commit ccc5d85fc2
10 changed files with 121 additions and 96 deletions

View File

@@ -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
View File

@@ -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
View File

@@ -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"

View File

@@ -22,6 +22,8 @@ tauri = { version = "2.0.0-beta", features = [
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"
@@ -38,7 +40,6 @@ 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" }

View File

@@ -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)
} }

View File

@@ -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<_>>();

View File

@@ -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())

View File

@@ -17,11 +17,13 @@ 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 <Avatar.Image
src={`//wsrv.nl/?url=${user.profile?.picture}&w=200&h=200`} src={`//wsrv.nl/?url=${user.profile?.picture}&w=200&h=200`}
alt={user.pubkey} alt={user.pubkey}
@@ -29,7 +31,6 @@ export function UserAvatar({ className }: { className?: string }) {
decoding="async" decoding="async"
className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]" className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
/> />
) : null}
<Avatar.Fallback> <Avatar.Fallback>
<img <img
src={fallback} src={fallback}
@@ -37,6 +38,8 @@ export function UserAvatar({ className }: { className?: string }) {
className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]" className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
/> />
</Avatar.Fallback> </Avatar.Fallback>
</>
) : null}
</Avatar.Root> </Avatar.Root>
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -25,14 +25,10 @@ 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 />
</div>
<div className="h-12 shrink-0 flex items-center px-2.5 border-t border-black/5 dark:border-white/5">
<CurrentUser /> <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 />
</div> </div>
@@ -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 (
<div className="shrink-0 h-12 flex items-center px-2.5 border-t border-black/5 dark:border-white/5">
<User.Provider pubkey={account}> <User.Provider pubkey={account}>
<User.Root className="inline-flex items-center gap-2"> <User.Root className="inline-flex items-center gap-2">
<User.Avatar className="size-8 rounded-full object-cover" /> <User.Avatar className="size-8 rounded-full" />
<User.Name className="text-sm font-medium leading-tight" /> <User.Name className="text-sm font-medium leading-tight" />
</User.Root> </User.Root>
</User.Provider> </User.Provider>
</div>
); );
} }