diff --git a/src/global.css b/src/global.css
index b6b7a2c..6dac235 100644
--- a/src/global.css
+++ b/src/global.css
@@ -15,7 +15,7 @@ html {
}
a {
- @apply cursor-default no-underline !important;
+ @apply cursor-default !important;
}
button {
diff --git a/src/routes/$account/_layout/chats.$id.lazy.tsx b/src/routes/$account/_layout/chats.$id.lazy.tsx
index 46ba7a4..0f913f1 100644
--- a/src/routes/$account/_layout/chats.$id.lazy.tsx
+++ b/src/routes/$account/_layout/chats.$id.lazy.tsx
@@ -1,378 +1,407 @@
-import { commands } from '@/commands'
-import { cn, getReceivers, groupEventByDate, time, upload } from '@/commons'
-import { Spinner } from '@/components/spinner'
-import { User } from '@/components/user'
-import { CoopIcon } from '@/icons/coop'
-import { ArrowUp, Paperclip, X } from '@phosphor-icons/react'
-import * as ScrollArea from '@radix-ui/react-scroll-area'
-import { useQuery, useQueryClient } from '@tanstack/react-query'
-import { createLazyFileRoute } from '@tanstack/react-router'
-import { listen } from '@tauri-apps/api/event'
-import { message } from '@tauri-apps/plugin-dialog'
-import type { NostrEvent } from 'nostr-tools'
+import { commands } from "@/commands";
+import { cn, getReceivers, groupEventByDate, time, upload } from "@/commons";
+import { Spinner } from "@/components/spinner";
+import { User } from "@/components/user";
+import { CoopIcon } from "@/icons/coop";
+import { ArrowUp, Paperclip, X } from "@phosphor-icons/react";
+import * as ScrollArea from "@radix-ui/react-scroll-area";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { createLazyFileRoute } from "@tanstack/react-router";
+import { listen } from "@tauri-apps/api/event";
+import { message } from "@tauri-apps/plugin-dialog";
+import type { NostrEvent } from "nostr-tools";
import {
- type Dispatch,
- type SetStateAction,
- useCallback,
- useRef,
- useState,
- useTransition,
-} from 'react'
-import { useEffect } from 'react'
-import { Virtualizer, type VirtualizerHandle } from 'virtua'
+ type Dispatch,
+ type SetStateAction,
+ useCallback,
+ useRef,
+ useState,
+ useTransition,
+} from "react";
+import { useEffect } from "react";
+import { Virtualizer, type VirtualizerHandle } from "virtua";
type EventPayload = {
- event: string
- sender: string
-}
+ event: string;
+ sender: string;
+};
-export const Route = createLazyFileRoute('/$account/_layout/chats/$id')({
- component: Screen,
-})
+export const Route = createLazyFileRoute("/$account/_layout/chats/$id")({
+ component: Screen,
+});
function Screen() {
- return (
-
-
-
-
-
- )
+ return (
+
+
+
+
+
+ );
}
function Header() {
- const { account, id } = Route.useParams()
- const { platform } = Route.useRouteContext()
+ const { account, id } = Route.useParams();
+ const { platform } = Route.useRouteContext();
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
}
function List() {
- const { account, id } = Route.useParams()
- const { isLoading, isError, data } = useQuery({
- queryKey: ['chats', id],
- queryFn: async () => {
- const res = await commands.getChatMessages(id)
+ const { account, id } = Route.useParams();
+ const { isLoading, isError, data } = useQuery({
+ queryKey: ["chats", id],
+ queryFn: async () => {
+ const res = await commands.getChatMessages(id);
- if (res.status === 'ok') {
- const raw = res.data
- const events: NostrEvent[] = raw.map((item) => JSON.parse(item))
+ if (res.status === "ok") {
+ const raw = res.data;
+ const events: NostrEvent[] = raw.map((item) => JSON.parse(item));
- return events
- } else {
- throw new Error(res.error)
- }
- },
- select: (data) => {
- const groups = groupEventByDate(data)
- return Object.entries(groups).reverse()
- },
- refetchOnWindowFocus: false,
- })
+ return events;
+ } else {
+ throw new Error(res.error);
+ }
+ },
+ select: (data) => {
+ const groups = groupEventByDate(data);
+ return Object.entries(groups).reverse();
+ },
+ refetchOnWindowFocus: false,
+ });
- const queryClient = useQueryClient()
- const scrollRef = useRef(null)
- const ref = useRef(null)
- const shouldStickToBottom = useRef(true)
+ const queryClient = useQueryClient();
+ const scrollRef = useRef(null);
+ const ref = useRef(null);
+ const shouldStickToBottom = useRef(true);
- const renderItem = useCallback(
- (item: NostrEvent, idx: number) => {
- const self = account === item.pubkey
+ const renderItem = useCallback(
+ (item: NostrEvent, idx: number) => {
+ const self = account === item.pubkey;
- return (
-
-
-
-
- {time(item.created_at)}
-
-
-
- )
- },
- [data],
- )
+ return (
+
+
+
+
+ {time(item.created_at)}
+
+
+
+ );
+ },
+ [data],
+ );
- useEffect(() => {
- const unlisten = listen('event', async (data) => {
- const event: NostrEvent = JSON.parse(data.payload.event)
- const sender = data.payload.sender
- const receivers = getReceivers(event.tags)
- const group = [account, id]
+ useEffect(() => {
+ const unlisten = listen("event", async (data) => {
+ const event: NostrEvent = JSON.parse(data.payload.event);
+ const sender = data.payload.sender;
+ const receivers = getReceivers(event.tags);
+ const group = [account, id];
- if (!group.includes(sender)) return
- if (!group.some((item) => receivers.includes(item))) return
+ if (!group.includes(sender)) return;
+ if (!group.some((item) => receivers.includes(item))) return;
- await queryClient.setQueryData(
- ['chats', id],
- (prevEvents: NostrEvent[]) => {
- if (!prevEvents) return [event]
- return [event, ...prevEvents]
- },
- )
- })
+ await queryClient.setQueryData(
+ ["chats", id],
+ (prevEvents: NostrEvent[]) => {
+ if (!prevEvents) return [event];
+ return [event, ...prevEvents];
+ },
+ );
+ });
- return () => {
- unlisten.then((f) => f())
- }
- }, [account, id])
+ return () => {
+ unlisten.then((f) => f());
+ };
+ }, [account, id]);
- useEffect(() => {
- if (!data?.length) return
- if (!ref.current) return
- if (!shouldStickToBottom.current) return
+ useEffect(() => {
+ if (!data?.length) return;
+ if (!ref.current) return;
+ if (!shouldStickToBottom.current) return;
- ref.current.scrollToIndex(data.length - 1, {
- align: 'end',
- })
- }, [data])
+ ref.current.scrollToIndex(data.length - 1, {
+ align: "end",
+ });
+ }, [data]);
- return (
-
-
- {
- if (!ref.current) return
- shouldStickToBottom.current =
- offset - ref.current.scrollSize + ref.current.viewportSize >= -1.5
- }}
- >
- {isLoading ? (
- <>
-
-
- >
- ) : isError ? (
-
-
- Cannot load message. Please try again later.
-
-
- ) : !data.length ? (
-
-
-
- ) : (
- data.map((item) => (
-
-
- {item[0]}
-
-
- {item[1]
- .sort((a, b) => a.created_at - b.created_at)
- .map((item, idx) => renderItem(item, idx))}
-
-
- ))
- )}
-
-
-
-
-
-
-
- )
+ return (
+
+
+ {
+ if (!ref.current) return;
+ shouldStickToBottom.current =
+ offset - ref.current.scrollSize + ref.current.viewportSize >=
+ -1.5;
+ }}
+ >
+ {isLoading ? (
+ <>
+
+
+ >
+ ) : isError ? (
+
+
+ Cannot load message. Please try again later.
+
+
+ ) : !data.length ? (
+
+
+
+ ) : (
+ data.map((item) => (
+
+
+ {item[0]}
+
+
+ {item[1]
+ .sort((a, b) => a.created_at - b.created_at)
+ .map((item, idx) => renderItem(item, idx))}
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+function Message({ text }: { text: string }) {
+ const delimiter =
+ /((?:https?:\/\/)?(?:(?:[a-z0-9]?(?:[a-z0-9\-]{1,61}[a-z0-9])?\.[^\.|\s])+[a-z\.]*[a-z]+|(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3})(?::\d{1,5})*[a-z0-9.,_\/~#&=;%+?\-\\(\\)]*)/gi;
+
+ return (
+ <>
+ {text.split(delimiter).map((word) => {
+ const match = word.match(delimiter);
+ if (match) {
+ const url = match[0];
+ return (
+
+ {url}
+
+ );
+ }
+ return word;
+ })}
+ >
+ );
}
function Form() {
- const { id } = Route.useParams()
- const inboxRelays = Route.useLoaderData()
+ const { id } = Route.useParams();
+ const inboxRelays = Route.useLoaderData();
- const [newMessage, setNewMessage] = useState('')
- const [attaches, setAttaches] = useState([])
- const [isPending, startTransition] = useTransition()
+ const [newMessage, setNewMessage] = useState("");
+ const [attaches, setAttaches] = useState([]);
+ const [isPending, startTransition] = useTransition();
- const remove = (item: string) => {
- setAttaches((prev) => prev.filter((att) => att !== item))
- }
+ const remove = (item: string) => {
+ setAttaches((prev) => prev.filter((att) => att !== item));
+ };
- const submit = () => {
- startTransition(async () => {
- if (!newMessage.length) return
+ const submit = () => {
+ startTransition(async () => {
+ if (!newMessage.length) return;
- const content = `${newMessage}\r\n${attaches.join('\r\n')}`
- const res = await commands.sendMessage(id, content)
+ const content = `${newMessage}\r\n${attaches.join("\r\n")}`;
+ const res = await commands.sendMessage(id, content);
- if (res.status === 'error') {
- await message(res.error, {
- title: 'Send mesaage failed',
- kind: 'error',
- })
- return
- }
+ if (res.status === "error") {
+ await message(res.error, {
+ title: "Send mesaage failed",
+ kind: "error",
+ });
+ return;
+ }
- setNewMessage('')
- })
- }
+ setNewMessage("");
+ setAttaches([]);
+ });
+ };
- return (
-
- {!inboxRelays.length ? (
-
- This user doesn't have inbox relays. You cannot send messages to them.
-
- ) : (
-
- {attaches?.length ? (
-
- {attaches.map((item, index) => (
-
- ))}
-
- ) : null}
-
-
-
setNewMessage(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === 'Enter') submit()
- }}
- className="flex-1 h-9 rounded-full px-3.5 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:outline-none focus:border-blue-500 placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
- />
-
-
-
- )}
-
- )
+ return (
+
+ {!inboxRelays.length ? (
+
+ This user doesn't have inbox relays. You cannot send messages to them.
+
+ ) : (
+
+ {attaches?.length ? (
+
+ {attaches.map((item, index) => (
+
+ ))}
+
+ ) : null}
+
+
+
setNewMessage(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") submit();
+ }}
+ className="flex-1 h-9 rounded-full px-3.5 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:outline-none focus:border-blue-500 placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
+ />
+
+
+
+ )}
+
+ );
}
function AttachMedia({
- onUpload,
+ onUpload,
}: {
- onUpload: Dispatch>
+ onUpload: Dispatch>;
}) {
- const [isPending, startTransition] = useTransition()
+ const [isPending, startTransition] = useTransition();
- const attach = () => {
- startTransition(async () => {
- const file = await upload()
+ const attach = () => {
+ startTransition(async () => {
+ const file = await upload();
- if (file) {
- onUpload((prev) => [...prev, file])
- } else {
- return
- }
- })
- }
+ if (file) {
+ onUpload((prev) => [...prev, file]);
+ } else {
+ return;
+ }
+ });
+ };
- return (
-
- )
+ return (
+
+ );
}