From d03865eeaa2f96d7275ffdfbd0cb8242e2f23d29 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 18 Sep 2024 15:32:36 +0700 Subject: [PATCH] feat: add highlight for link in message --- src/global.css | 2 +- .../$account/_layout/chats.$id.lazy.tsx | 697 +++++++++--------- 2 files changed, 364 insertions(+), 335 deletions(-) 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 ( -
-
-
- - - - - - - - - - -
-
-
-
- - - - -
Connected
-
-
-
- ) + return ( +
+
+
+ + + + + + + + + + +
+
+
+
+ + + + +
Connected
+
+
+
+ ); } 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 ( -
-
-
- {item.content} -
-
-
- - {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 ( + + ); }