feat: improve query message

This commit is contained in:
reya
2024-07-25 16:43:26 +07:00
parent 005cbeab72
commit 89a6883dbe
21 changed files with 313 additions and 280 deletions

View File

@@ -1,51 +1,51 @@
{
"name": "coop",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@tanstack/react-query": "^5.51.11",
"@tanstack/react-router": "^1.45.8",
"@tauri-apps/api": ">=2.0.0-beta.0",
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.5",
"@tauri-apps/plugin-dialog": "2.0.0-beta.7",
"@tauri-apps/plugin-os": "2.0.0-beta.7",
"@tauri-apps/plugin-shell": ">=2.0.0-beta.0",
"dayjs": "^1.11.12",
"minidenticons": "^4.2.1",
"nostr-tools": "^2.7.1",
"react": "19.0.0-rc-d025ddd3-20240722",
"react-dom": "19.0.0-rc-d025ddd3-20240722",
"virtua": "^0.33.3"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@tanstack/router-plugin": "^1.45.8",
"@tauri-apps/cli": ">=2.0.0-beta.0",
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625",
"clsx": "^2.1.1",
"postcss": "^8.4.39",
"tailwind-merge": "^2.4.0",
"tailwindcss": "^3.4.6",
"typescript": "^5.2.2",
"vite": "^5.3.1",
"vite-tsconfig-paths": "^4.3.2"
},
"overrides": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc"
}
"name": "coop",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@tanstack/react-query": "^5.51.11",
"@tanstack/react-router": "^1.45.8",
"@tauri-apps/api": ">=2.0.0-beta.0",
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.5",
"@tauri-apps/plugin-dialog": "2.0.0-beta.7",
"@tauri-apps/plugin-os": "2.0.0-beta.7",
"@tauri-apps/plugin-shell": ">=2.0.0-beta.0",
"dayjs": "^1.11.12",
"minidenticons": "^4.2.1",
"nostr-tools": "^2.7.1",
"react": "19.0.0-rc-d025ddd3-20240722",
"react-dom": "19.0.0-rc-d025ddd3-20240722",
"virtua": "^0.33.3"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@tanstack/router-plugin": "^1.45.8",
"@tauri-apps/cli": ">=2.0.0-beta.0",
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625",
"clsx": "^2.1.1",
"postcss": "^8.4.39",
"tailwind-merge": "^2.4.0",
"tailwindcss": "^3.4.6",
"typescript": "^5.2.2",
"vite": "^5.3.1",
"vite-tsconfig-paths": "^4.3.2"
},
"overrides": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc"
}
}

View File

@@ -149,78 +149,17 @@ pub async fn login(
state: State<'_, Nostr>,
handle: tauri::AppHandle,
) -> Result<String, String> {
let app = handle.app_handle().clone();
let client = &state.client;
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
let hex = public_key.to_hex();
let keyring = Entry::new(&id, "nostr_secret").expect("Unexpected.");
let password = match keyring.get_password() {
Ok(pw) => pw,
Err(_) => return Err("Cancelled".into()),
};
match bunker {
Some(uri) => {
let app_keys =
Keys::parse(password).expect("Secret Key is modified, please check again.");
match NostrConnectURI::parse(uri) {
Ok(bunker_uri) => {
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(30), None)
.await
{
Ok(signer) => client.set_signer(Some(signer.into())).await,
Err(err) => return Err(err.to_string()),
}
}
Err(err) => return Err(err.to_string()),
}
}
None => {
let keys = Keys::parse(password).expect("Secret Key is modified, please check again.");
let signer = NostrSigner::Keys(keys);
// Update signer
client.set_signer(Some(signer)).await;
}
}
let hex = public_key.to_hex();
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
if let Ok(events) = client.get_events_of(vec![inbox], None).await {
if let Some(event) = events.into_iter().next() {
for tag in &event.tags {
if let Some(TagStandard::Relay(url)) = tag.as_standardized() {
let url = url.to_string();
if client.add_relay(&url).await.is_ok() {
println!("Adding relay {} ...", url);
if client.connect_relay(&url).await.is_ok() {
println!("Connecting relay {} ...", url);
}
}
}
}
}
}
tauri::async_runtime::spawn(async move {
let window = handle.get_webview_window("main").expect("Window is terminated.");
let window = app.get_webview_window("main").expect("Window is terminated.");
let state = window.state::<Nostr>();
let client = &state.client;
let old = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let new = Filter::new().kind(Kind::GiftWrap).pubkey(public_key).limit(0);
if client.reconcile(old, NegentropyOptions::default()).await.is_ok() {
println!("Sync done.")
};
if client.subscribe(vec![new], None).await.is_ok() {
println!("Waiting for new message...")
};
client
.handle_notifications(|notification| async {
if let RelayPoolNotification::Message { message, relay_url } = notification {
@@ -265,5 +204,64 @@ pub async fn login(
.await
});
let password = match keyring.get_password() {
Ok(pw) => pw,
Err(_) => return Err("Cancelled".into()),
};
match bunker {
Some(uri) => {
let app_keys =
Keys::parse(password).expect("Secret Key is modified, please check again.");
match NostrConnectURI::parse(uri) {
Ok(bunker_uri) => {
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(30), None)
.await
{
Ok(signer) => client.set_signer(Some(signer.into())).await,
Err(err) => return Err(err.to_string()),
}
}
Err(err) => return Err(err.to_string()),
}
}
None => {
let keys = Keys::parse(password).expect("Secret Key is modified, please check again.");
let signer = NostrSigner::Keys(keys);
// Update signer
client.set_signer(Some(signer)).await;
}
}
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
let old = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let new = Filter::new().kind(Kind::GiftWrap).pubkey(public_key).limit(0);
let mut relays = Vec::new();
if let Ok(events) = client.get_events_of(vec![inbox], None).await {
if let Some(event) = events.into_iter().next() {
for tag in &event.tags {
if let Some(TagStandard::Relay(relay)) = tag.as_standardized() {
let url = relay.to_string();
if client.add_relay(&url).await.is_ok() {
relays.push(url)
}
}
}
}
}
if client.reconcile_with(relays.clone(), old, NegentropyOptions::default()).await.is_ok() {
handle.emit("synchronized", ()).unwrap();
println!("synchronized");
};
if client.subscribe_to(relays, vec![new], None).await.is_ok() {
println!("Waiting for new message...")
};
Ok(hex)
}

View File

@@ -16,7 +16,7 @@ pub async fn get_chats(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
match client.database().query(vec![filter], Order::Desc).await {
match client.get_events_of(vec![filter], None).await {
Ok(events) => {
let rumors = stream::iter(events)
.filter_map(|ev| async move {
@@ -51,14 +51,13 @@ pub async fn get_chat_messages(
state: State<'_, Nostr>,
) -> Result<Vec<String>, String> {
let client = &state.client;
let database = client.database();
let signer = client.signer().await.map_err(|e| e.to_string())?;
let receiver_pk = signer.public_key().await.map_err(|e| e.to_string())?;
let sender_pk = PublicKey::parse(sender).map_err(|e| e.to_string())?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkeys(vec![receiver_pk, sender_pk]);
match database.query(vec![filter], Order::Desc).await {
match client.get_events_of(vec![filter], None).await {
Ok(events) => {
let rumors = stream::iter(events)
.filter_map(|ev| async move {
@@ -84,14 +83,18 @@ pub async fn get_chat_messages(
#[tauri::command]
#[specta::specta]
pub async fn subscribe_to(id: String, state: State<'_, Nostr>) -> Result<(), String> {
pub async fn subscribe_to(
id: String,
relays: Vec<String>,
state: State<'_, Nostr>,
) -> Result<(), String> {
let client = &state.client;
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key).limit(0);
let subscription_id = SubscriptionId::new(&id[..6]);
if client.subscribe_with_id(subscription_id, vec![filter], None).await.is_ok() {
if client.subscribe_with_id_to(relays, subscription_id, vec![filter], None).await.is_ok() {
println!("Watching ... {}", id)
};
@@ -126,8 +129,6 @@ pub async fn get_inboxes(id: String, state: State<'_, Nostr>) -> Result<Vec<Stri
if let Some(TagStandard::Relay(url)) = tag.as_standardized() {
let relay = url.to_string();
let _ = client.add_relay(&relay).await;
let _ = client.connect_relay(&relay).await;
relays.push(relay);
}
}
@@ -139,18 +140,6 @@ pub async fn get_inboxes(id: String, state: State<'_, Nostr>) -> Result<Vec<Stri
}
}
#[tauri::command]
#[specta::specta]
pub async fn drop_inbox(relays: Vec<String>, state: State<'_, Nostr>) -> Result<(), ()> {
let client = &state.client;
for relay in relays.iter() {
let _ = client.disconnect_relay(relay).await;
}
Ok(())
}
#[tauri::command]
#[specta::specta]
pub async fn send_message(

View File

@@ -35,7 +35,6 @@ fn main() {
send_message,
subscribe_to,
unsubscribe,
drop_inbox
]);
#[cfg(debug_assertions)]
@@ -71,9 +70,10 @@ fn main() {
// Config
let opts = Options::new()
.automatic_authentication(true)
.autoconnect(true)
.automatic_authentication(false)
.timeout(Duration::from_secs(5))
.send_timeout(Some(Duration::from_secs(5)))
.send_timeout(Some(Duration::from_secs(50)))
.connection_timeout(Some(Duration::from_secs(20)));
// Setup nostr client
@@ -83,11 +83,8 @@ fn main() {
};
// Add bootstrap relay
let _ = client.add_relay("wss://relay.damus.io/").await;
let _ = client.add_relay("wss://relay.nostr.net/").await;
// Connect
client.connect().await;
let _ =
client.add_relays(["wss://relay.damus.io/", "wss://relay.nostr.net/"]).await;
// Create global state
app.handle().manage(Nostr { client, contact_list: Mutex::new(vec![]) })

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -79,9 +79,9 @@ try {
else return { status: "error", error: e as any };
}
},
async subscribeTo(id: string) : Promise<Result<null, string>> {
async subscribeTo(id: string, relays: string[]) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("subscribe_to", { id }) };
return { status: "ok", data: await TAURI_INVOKE("subscribe_to", { id, relays }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
@@ -94,14 +94,6 @@ try {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async dropInbox(relays: string[]) : Promise<Result<null, null>> {
try {
return { status: "ok", data: await TAURI_INVOKE("drop_inbox", { relays }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
}
}

View File

@@ -1,8 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { type ClassValue, clsx } from "clsx";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import updateLocale from "dayjs/plugin/updateLocale";
import { twMerge } from "tailwind-merge";
import { commands } from "./commands";
dayjs.extend(relativeTime);
dayjs.extend(updateLocale);
@@ -72,3 +74,20 @@ export function getReceivers(tags: string[][]) {
const p = tags.map((tag) => tag[0] === "p" && tag[1]);
return p;
}
export const useRelays = (id: string) =>
useQuery({
queryKey: ["relays", id],
queryFn: async () => {
const res = await commands.getInboxes(id);
if (res.status === "ok") {
return res.data;
} else {
throw new Error(res.error);
}
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
});

View File

@@ -1,14 +1,11 @@
import { cn } from "@/commons";
import type { ReactNode } from "react";
export function Spinner({
children,
className,
}: {
children?: ReactNode;
className?: string;
}) {
const spinner = (
return (
<span className={cn("block relative opacity-65 size-4", className)}>
<span className="spinner-leaf" />
<span className="spinner-leaf" />
@@ -20,28 +17,4 @@ export function Spinner({
<span className="spinner-leaf" />
</span>
);
if (children === undefined) return spinner;
return (
<div className="relative flex items-center justify-center">
<span>
{/**
* `display: contents` removes the content from the accessibility tree in some browsers,
* so we force remove it with `aria-hidden`
*/}
<span
aria-hidden
style={{ display: "contents", visibility: "hidden" }}
// biome-ignore lint/correctness/noConstantCondition: Workaround to use `inert` until https://github.com/facebook/react/pull/24730 is merged.
{...{ inert: true ? "" : undefined }}
>
{children}
</span>
<div className="absolute flex items-center justify-center">
<span>{spinner}</span>
</div>
</span>
</div>
);
}

View File

@@ -21,13 +21,15 @@ export function UserAvatar({ className }: { className?: string }) {
className,
)}
>
<Avatar.Image
src={`//wsrv.nl/?url=${user.profile?.picture}&w=200&h=200`}
alt={user.pubkey}
loading="lazy"
decoding="async"
className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
/>
{user?.profile?.picture ? (
<Avatar.Image
src={`//wsrv.nl/?url=${user.profile?.picture}&w=200&h=200`}
alt={user.pubkey}
loading="lazy"
decoding="async"
className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
/>
) : null}
<Avatar.Fallback>
<img
src={fallback}

19
src/icons/coop.tsx Normal file
View File

@@ -0,0 +1,19 @@
import { SVGProps } from "react";
export function CoopIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M0 12C0 5.373 5.373 0 12 0c6.628 0 12 5.373 12 12 0 6.628-5.372 12-12 12H1.426A1.426 1.426 0 010 22.574V12z"
fill="currentColor"
/>
</svg>
);
}

View File

@@ -22,6 +22,7 @@ const NewLazyImport = createFileRoute('/new')()
const ImportKeyLazyImport = createFileRoute('/import-key')()
const CreateAccountLazyImport = createFileRoute('/create-account')()
const AccountChatsLazyImport = createFileRoute('/$account/chats')()
const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')()
const AccountChatsIdLazyImport = createFileRoute('/$account/chats/$id')()
// Create/Update Routes
@@ -60,6 +61,13 @@ const AccountChatsLazyRoute = AccountChatsLazyImport.update({
import('./routes/$account.chats.lazy').then((d) => d.Route),
)
const AccountChatsNewLazyRoute = AccountChatsNewLazyImport.update({
path: '/new',
getParentRoute: () => AccountChatsLazyRoute,
} as any).lazy(() =>
import('./routes/$account.chats.new.lazy').then((d) => d.Route),
)
const AccountChatsIdLazyRoute = AccountChatsIdLazyImport.update({
path: '/$id',
getParentRoute: () => AccountChatsLazyRoute,
@@ -120,6 +128,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AccountChatsIdLazyImport
parentRoute: typeof AccountChatsLazyImport
}
'/$account/chats/new': {
id: '/$account/chats/new'
path: '/new'
fullPath: '/$account/chats/new'
preLoaderRoute: typeof AccountChatsNewLazyImport
parentRoute: typeof AccountChatsLazyImport
}
}
}
@@ -133,6 +148,7 @@ export const routeTree = rootRoute.addChildren({
NostrConnectLazyRoute,
AccountChatsLazyRoute: AccountChatsLazyRoute.addChildren({
AccountChatsIdLazyRoute,
AccountChatsNewLazyRoute,
}),
})
@@ -170,12 +186,17 @@ export const routeTree = rootRoute.addChildren({
"/$account/chats": {
"filePath": "$account.chats.lazy.tsx",
"children": [
"/$account/chats/$id"
"/$account/chats/$id",
"/$account/chats/new"
]
},
"/$account/chats/$id": {
"filePath": "$account.chats.$id.lazy.tsx",
"parent": "/$account/chats"
},
"/$account/chats/new": {
"filePath": "$account.chats.new.lazy.tsx",
"parent": "/$account/chats"
}
}
}

View File

@@ -1,5 +1,5 @@
import { commands } from "@/commands";
import { cn, getReceivers, time } from "@/commons";
import { cn, getReceivers, time, useRelays } from "@/commons";
import { Spinner } from "@/components/spinner";
import { ArrowUp, CloudArrowUp, Paperclip } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area";
@@ -23,14 +23,17 @@ export const Route = createLazyFileRoute("/$account/chats/$id")({
function Screen() {
const { id } = Route.useParams();
const { isLoading, data: relays } = useRelays(id);
useEffect(() => {
commands.subscribeTo(id).then(() => console.log("sub: ", id));
if (!isLoading && relays?.length)
commands.subscribeTo(id, relays).then(() => console.log("sub: ", id));
return () => {
commands.unsubscribe(id).then(() => console.log("unsub: ", id));
if (!isLoading && relays?.length)
commands.unsubscribe(id).then(() => console.log("unsub: ", id));
};
}, []);
}, [isLoading, relays]);
return (
<div className="size-full flex flex-col">
@@ -43,6 +46,7 @@ function Screen() {
function List() {
const { account, id } = Route.useParams();
const { isLoading: rl, isError: rE } = useRelays(id);
const { isLoading, isError, data } = useQuery({
queryKey: ["chats", id],
queryFn: async () => {
@@ -59,18 +63,20 @@ function List() {
throw new Error(res.error);
}
},
enabled: !rl && !rE,
refetchOnWindowFocus: false,
});
const queryClient = useQueryClient();
const ref = useRef<HTMLDivElement>(null);
const renderItem = useCallback(
(item: NostrEvent) => {
(item: NostrEvent, idx: number) => {
const self = account === item.pubkey;
return (
<div
key={item.id}
key={idx + item.id}
className="flex items-center justify-between gap-3 my-1.5 px-3 border-l-2 border-transparent hover:border-blue-400"
>
<div
@@ -139,12 +145,21 @@ function List() {
className="relative h-full py-2 [&>div]:!flex [&>div]:flex-col [&>div]:justify-end [&>div]:min-h-full"
>
<Virtualizer scrollRef={ref} shift>
{isLoading ? (
<p>Loading...</p>
) : isError || !data ? (
<p>Error</p>
{isLoading || !data ? (
<div className="w-full h-56 flex items-center justify-center">
<div className="flex items-center gap-1.5">
<Spinner />
Loading message...
</div>
</div>
) : isError ? (
<div className="w-full h-56 flex items-center justify-center">
<div className="flex items-center gap-1.5">
Cannot load message. Please try again later.
</div>
</div>
) : (
data.map((item) => renderItem(item))
data.map((item, idx) => renderItem(item, idx))
)}
</Virtualizer>
</ScrollArea.Viewport>
@@ -161,25 +176,7 @@ function List() {
function Form() {
const { id } = Route.useParams();
const {
isLoading,
isError,
data: relays,
} = useQuery({
queryKey: ["inboxes", id],
queryFn: async () => {
const res = await commands.getInboxes(id);
if (res.status === "ok") {
return res.data;
} else {
throw new Error(res.error);
}
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
});
const { isLoading, isError, data: relays } = useRelays(id);
const [newMessage, setNewMessage] = useState("");
const [isPending, startTransition] = useTransition();

View File

@@ -80,27 +80,40 @@ function ChatList() {
throw new Error(res.error);
}
},
refetchOnWindowFocus: false,
});
const queryClient = useQueryClient();
useEffect(() => {
const unlisten = listen("synchronized", async () => {
await queryClient.refetchQueries({ queryKey: ["chats"] });
});
return () => {
unlisten.then((f) => f());
};
}, []);
useEffect(() => {
const unlisten = listen<Payload>("event", async (data) => {
const event: NostrEvent = JSON.parse(data.payload.event);
const chats: NostrEvent[] = await queryClient.getQueryData(["chats"]);
const exist = chats.find((ev) => ev.pubkey === event.pubkey);
if (!exist) {
await queryClient.setQueryData(
["chats"],
(prevEvents: NostrEvent[]) => {
if (!prevEvents) return prevEvents;
if (event.pubkey === account) return;
if (chats) {
const exist = chats.find((ev) => ev.pubkey === event.pubkey);
return [event, ...prevEvents];
// queryClient.invalidateQueries(['chats', id]);
},
);
if (!exist) {
await queryClient.setQueryData(
["chats"],
(prevEvents: NostrEvent[]) => {
if (!prevEvents) return prevEvents;
if (event.pubkey === account) return;
return [event, ...prevEvents];
},
);
}
}
});

View File

@@ -0,0 +1,14 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import { CoopIcon } from "@/icons/coop";
export const Route = createLazyFileRoute("/$account/chats/new")({
component: Screen,
});
function Screen() {
return (
<div className="size-full flex items-center justify-center">
<CoopIcon className="size-10 text-neutral-200 dark:text-neutral-800" />
</div>
);
}

View File

@@ -82,7 +82,7 @@ function Screen() {
type="button"
onClick={() => submit()}
disabled={isPending}
className="inline-flex items-center justify-center w-full h-10 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
className="inline-flex items-center justify-center w-full h-9 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{isPending ? <Spinner /> : "Continue"}
</button>

View File

@@ -90,7 +90,7 @@ function Screen() {
type="button"
onClick={() => submit()}
disabled={isPending}
className="inline-flex items-center justify-center w-full h-10 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
className="inline-flex items-center justify-center w-full h-9 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{isPending ? <Spinner /> : "Continue"}
</button>

View File

@@ -49,7 +49,7 @@ function Screen() {
if (res.status === "ok") {
navigate({
to: "/$account/chats",
to: "/$account/chats/new",
params: { account: res.data },
replace: true,
});

View File

@@ -16,13 +16,13 @@ function Screen() {
<div className="flex flex-col gap-3">
<Link
to="/create-account"
className="w-full h-10 bg-blue-500 hover:bg-blue-600 text-white rounded-lg inline-flex items-center justify-center shadow"
className="w-full h-9 bg-blue-500 hover:bg-blue-600 text-white rounded-lg inline-flex items-center justify-center shadow"
>
Create a new identity
</Link>
<Link
to="/nostr-connect"
className="w-full h-10 bg-white hover:bg-neutral-100 dark:hover:bg-neutral-950 dark:bg-neutral-900 rounded-lg inline-flex items-center justify-center"
className="w-full h-9 bg-white hover:bg-neutral-100 dark:hover:bg-neutral-950 dark:bg-neutral-900 rounded-lg inline-flex items-center justify-center"
>
Login with Nostr Connect
</Link>

View File

@@ -76,7 +76,7 @@ function Screen() {
type="button"
onClick={() => submit()}
disabled={isPending}
className="inline-flex items-center justify-center w-full h-10 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
className="inline-flex items-center justify-center w-full h-9 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{isPending ? <Spinner /> : "Continue"}
</button>

View File

@@ -1,40 +1,38 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
"baseUrl": "./",
"paths": {
"@/*": [
"./src/*"
]
},
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"strictNullChecks": false
},
"include": [
"src"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
"baseUrl": "./",
"paths": {
"@/*": [
"./src/*"
]
},
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"strictNullChecks": false
},
"include": [
"src"
],
"references": [
{
"path": "./tsconfig.node.json"
}
],
}

View File

@@ -1,10 +1,12 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts"
]
}