From e4446962acff8072603691669282b28b6e2933a0 Mon Sep 17 00:00:00 2001 From: Kieran Date: Tue, 6 May 2025 12:34:42 +0100 Subject: [PATCH] feat: improve messages: 1. WoT filter 2. React to read status --- packages/app/config/iris.json | 4 +- packages/app/src/Pages/Messages/DM.tsx | 6 +-- .../app/src/Pages/Messages/MessagesPage.tsx | 49 ++++++++++++++----- packages/app/src/chat/index.ts | 36 +++++++------- packages/app/src/chat/nip17.ts | 15 ++++-- packages/app/src/lang.json | 6 +-- packages/app/src/translations/en.json | 2 +- 7 files changed, 74 insertions(+), 44 deletions(-) diff --git a/packages/app/config/iris.json b/packages/app/config/iris.json index fb8029de..12979b79 100644 --- a/packages/app/config/iris.json +++ b/packages/app/config/iris.json @@ -44,9 +44,7 @@ "wss://relay.nostr.band/": { "read": true, "write": true }, "wss://relay.damus.io/": { "read": true, "write": true } }, - "chatChannels": [ - { "type": "telegram", "value": "https://t.me/irismessenger" } - ], + "chatChannels": [{ "type": "telegram", "value": "https://t.me/irismessenger" }], "alby": { "clientId": "5rYcHDrlDb", "clientSecret": "QAI3QmgiaPH3BfTMzzFd" diff --git a/packages/app/src/Pages/Messages/DM.tsx b/packages/app/src/Pages/Messages/DM.tsx index 7cf28574..96222400 100644 --- a/packages/app/src/Pages/Messages/DM.tsx +++ b/packages/app/src/Pages/Messages/DM.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; import { useInView } from "react-intersection-observer"; import { FormattedMessage, useIntl } from "react-intl"; -import { Chat, ChatMessage, ChatType, setLastReadIn } from "@/chat"; +import { Chat, ChatMessage, ChatType } from "@/chat"; import NoteTime from "@/Components/Event/Note/NoteTime"; import messages from "@/Components/messages"; import Text from "@/Components/Text/Text"; @@ -31,9 +31,7 @@ export default function DM(props: DMProps) { if (publisher) { const decrypted = await msg.decrypt(publisher); setContent(decrypted || ""); - if (!isMe) { - setLastReadIn(msg.id); - } + props.chat.markRead(msg.id); } } diff --git a/packages/app/src/Pages/Messages/MessagesPage.tsx b/packages/app/src/Pages/Messages/MessagesPage.tsx index 5129378c..a8abeca4 100644 --- a/packages/app/src/Pages/Messages/MessagesPage.tsx +++ b/packages/app/src/Pages/Messages/MessagesPage.tsx @@ -4,11 +4,13 @@ import { FormattedMessage, useIntl } from "react-intl"; import { useNavigate, useParams } from "react-router-dom"; import { Chat, ChatType, useChatSystems } from "@/chat"; +import { CollapsedSection } from "@/Components/Collapsed"; import NoteTime from "@/Components/Event/Note/NoteTime"; import NoteToSelf from "@/Components/User/NoteToSelf"; import ProfileImage from "@/Components/User/ProfileImage"; import useLogin from "@/Hooks/useLogin"; import usePageDimensions from "@/Hooks/usePageDimensions"; +import useWoT from "@/Hooks/useWoT"; import { ChatParticipantProfile } from "@/Pages/Messages/ChatParticipant"; import DmWindow from "@/Pages/Messages/DmWindow"; import NewChatWindow from "@/Pages/Messages/NewChatWindow"; @@ -24,8 +26,12 @@ export default function MessagesPage() { const { width: pageWidth } = usePageDimensions(); const chats = useChatSystems(); + const wot = useWoT(); + const trustedChats = chats.filter(a => wot.followDistance(a.participants[0].id) <= 2); + const otherChats = chats.filter(a => wot.followDistance(a.participants[0].id) > 2); - const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unread, 0), [chats]); + const unreadTrustedCount = useMemo(() => trustedChats.reduce((p, c) => p + c.unread, 0), [trustedChats]); + const unreadOtherCount = useMemo(() => otherChats.reduce((p, c) => p + c.unread, 0), [otherChats]); function openChat(e: React.MouseEvent, type: ChatType, id: string) { e.stopPropagation(); @@ -81,26 +87,45 @@ export default function MessagesPage() { ); } + function sortMessages(a: Chat, b: Chat) { + const aSelf = a.participants.length === 1 && a.participants[0].id === login.publicKey; + const bSelf = b.participants.length === 1 && b.participants[0].id === login.publicKey; + if (aSelf || bSelf) { + return aSelf ? -1 : 1; + } + return b.lastMessage > a.lastMessage ? 1 : -1; + } + return (
{(pageWidth >= TwoCol || !id) && (
-
- {chats - .sort((a, b) => { - const aSelf = a.participants.length === 1 && a.participants[0].id === login.publicKey; - const bSelf = b.participants.length === 1 && b.participants[0].id === login.publicKey; - if (aSelf || bSelf) { - return aSelf ? -1 : 1; - } - return b.lastMessage > a.lastMessage ? 1 : -1; - }) - .map(conversation)} + {trustedChats.sort(sortMessages).map(conversation)} + {otherChats.sort(sortMessages).length > 0 && ( + <> + + + {unreadOtherCount > 0 &&
} +
+ }> + {otherChats.map(conversation)} +
+ + )}
)} {id ? : pageWidth >= TwoCol &&
} diff --git a/packages/app/src/chat/index.ts b/packages/app/src/chat/index.ts index 8e8932d0..3b82a221 100644 --- a/packages/app/src/chat/index.ts +++ b/packages/app/src/chat/index.ts @@ -1,4 +1,4 @@ -import { unixNow, unwrap } from "@snort/shared"; +import { ExternalStore, unixNow, unwrap } from "@snort/shared"; import { encodeTLVEntries, EventKind, @@ -13,7 +13,7 @@ import { UserMetadata, } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useSyncExternalStore } from "react"; import useEventPublisher from "@/Hooks/useEventPublisher"; import useLogin from "@/Hooks/useLogin"; @@ -55,6 +55,7 @@ export interface Chat { messages: Array; createMessage(msg: string, pub: EventPublisher): Promise>; sendMessage(ev: Array, system: SystemInterface): void | Promise; + markRead(id?: string): void; } export interface ChatSystem { @@ -104,10 +105,13 @@ export function lastReadInChat(id: string) { return parseInt(window.localStorage.getItem(k) ?? "0"); } -export function setLastReadIn(id: string) { - const now = unixNow(); +export function setLastReadIn(id: string, time?: number) { + const now = time ?? unixNow(); const k = `dm:seen:${id}`; - window.localStorage.setItem(k, now.toString()); + const current = lastReadInChat(id); + if (current < now) { + window.localStorage.setItem(k, now.toString()); + } } export function createChatLink(type: ChatType, ...params: Array) { @@ -144,24 +148,28 @@ export function createEmptyChatObject(id: string) { throw new Error("Cant create new empty chat, unknown id"); } -export function useChatSystem(chat: ChatSystem) { +export function useChatSystem>>(sys: T) { const login = useLogin(); const { publisher } = useEventPublisher(); + const chat = useSyncExternalStore( + s => sys.hook(s), + () => sys.snapshot(), + ); const sub = useMemo(() => { - return chat.subscription(login); - }, [chat, login]); + return sys.subscription(login); + }, [login]); const data = useRequestBuilder(sub); const { isMuted } = useModeration(); useEffect(() => { if (publisher) { - chat.processEvents(publisher, data); + sys.processEvents(publisher, data); } }, [data, publisher]); return useMemo(() => { if (login.publicKey) { - return chat.listChats( + return sys.listChats( login.publicKey, data.filter(a => !isMuted(a.pubkey)), ); @@ -177,12 +185,6 @@ export function useChatSystems() { } export function useChat(id: string) { - const getStore = () => { - if (id.startsWith(NostrPrefix.Chat17)) { - return Nip17Chats; - } - throw new Error("Unsupported chat system"); - }; - const ret = useChatSystem(getStore()).find(a => a.id === id); + const ret = useChatSystem(Nip17Chats).find(a => a.id === id); return ret; } diff --git a/packages/app/src/chat/nip17.ts b/packages/app/src/chat/nip17.ts index 20e3f60a..cd4cd990 100644 --- a/packages/app/src/chat/nip17.ts +++ b/packages/app/src/chat/nip17.ts @@ -14,7 +14,7 @@ import { import { GiftsCache } from "@/Cache"; import { GiftWrapCache } from "@/Cache/GiftWrapCache"; -import { Chat, ChatSystem, ChatType, lastReadInChat } from "@/chat"; +import { Chat, ChatSystem, ChatType, lastReadInChat, setLastReadIn } from "@/chat"; import { UnwrappedGift } from "@/Db"; import { LoginSession } from "@/Utils/Login"; import { GetPowWorker } from "@/Utils/wasm"; @@ -100,8 +100,8 @@ export class Nip17ChatSystem extends ExternalStore> implements ChatS type: ChatType.PrivateDirectMessage, id, title: title.title, - unread: messages.reduce((acc, v) => (v.created_at > last ? acc++ : acc), 0), - lastMessage: messages.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0), + unread: messages.reduce((acc, v) => (v.inner.created_at > last ? acc + 1 : acc), 0), + lastMessage: messages.reduce((acc, v) => (v.inner.created_at > acc ? v.created_at : acc), 0), participants, messages: messages.map(m => ({ id: m.id, @@ -128,11 +128,18 @@ export class Nip17ChatSystem extends ExternalStore> implements ChatS messages.push(recvSealedN); } messages.push(pub.giftWrap(await pub.sealRumor(gossip, pub.pubKey), pub.pubKey, powTarget, GetPowWorker())); - return await Promise.all(messages); + const ret = await Promise.all(messages); + Nip17Chats.notifyChange(); + return ret; }, sendMessage: (ev, system) => { ev.forEach(a => system.BroadcastEvent(a)); }, + markRead: msgId => { + const msg = messages.find(a => a.id === msgId); + setLastReadIn(id, msg?.inner.created_at); + Nip17Chats.notifyChange(); + }, } as Chat; } diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index 1b811902..5d61cdb1 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -98,6 +98,9 @@ "0mch2Y": { "defaultMessage": "name has disallowed characters" }, + "0oMk/p": { + "defaultMessage": "Other Chats" + }, "0siT4z": { "defaultMessage": "Politics" }, @@ -2407,9 +2410,6 @@ "wc9st7": { "defaultMessage": "Media Attachments" }, - "whSrs+": { - "defaultMessage": "Nostr Public Chat" - }, "wih7iJ": { "defaultMessage": "name is blocked" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index c76c257a..6f51edbc 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -32,6 +32,7 @@ "0jOEtS": "Invalid LNURL", "0kOBMu": "Handling Mentions", "0mch2Y": "name has disallowed characters", + "0oMk/p": "Other Chats", "0siT4z": "Politics", "0uoY11": "Show Status", "0yO7wF": "{n} secs", @@ -799,7 +800,6 @@ "wOyDTB": "File storage server list", "wSZR47": "Submit", "wc9st7": "Media Attachments", - "whSrs+": "Nostr Public Chat", "wih7iJ": "name is blocked", "wlWMuh": "Patches", "wofVHy": "Moderation",