feat: improve messages:

1. WoT filter
2. React to read status
This commit is contained in:
2025-05-06 12:34:42 +01:00
parent d442166846
commit e4446962ac
7 changed files with 74 additions and 44 deletions

View File

@ -44,9 +44,7 @@
"wss://relay.nostr.band/": { "read": true, "write": true }, "wss://relay.nostr.band/": { "read": true, "write": true },
"wss://relay.damus.io/": { "read": true, "write": true } "wss://relay.damus.io/": { "read": true, "write": true }
}, },
"chatChannels": [ "chatChannels": [{ "type": "telegram", "value": "https://t.me/irismessenger" }],
{ "type": "telegram", "value": "https://t.me/irismessenger" }
],
"alby": { "alby": {
"clientId": "5rYcHDrlDb", "clientId": "5rYcHDrlDb",
"clientSecret": "QAI3QmgiaPH3BfTMzzFd" "clientSecret": "QAI3QmgiaPH3BfTMzzFd"

View File

@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { FormattedMessage, useIntl } from "react-intl"; 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 NoteTime from "@/Components/Event/Note/NoteTime";
import messages from "@/Components/messages"; import messages from "@/Components/messages";
import Text from "@/Components/Text/Text"; import Text from "@/Components/Text/Text";
@ -31,9 +31,7 @@ export default function DM(props: DMProps) {
if (publisher) { if (publisher) {
const decrypted = await msg.decrypt(publisher); const decrypted = await msg.decrypt(publisher);
setContent(decrypted || "<ERROR>"); setContent(decrypted || "<ERROR>");
if (!isMe) { props.chat.markRead(msg.id);
setLastReadIn(msg.id);
}
} }
} }

View File

@ -4,11 +4,13 @@ import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { Chat, ChatType, useChatSystems } from "@/chat"; import { Chat, ChatType, useChatSystems } from "@/chat";
import { CollapsedSection } from "@/Components/Collapsed";
import NoteTime from "@/Components/Event/Note/NoteTime"; import NoteTime from "@/Components/Event/Note/NoteTime";
import NoteToSelf from "@/Components/User/NoteToSelf"; import NoteToSelf from "@/Components/User/NoteToSelf";
import ProfileImage from "@/Components/User/ProfileImage"; import ProfileImage from "@/Components/User/ProfileImage";
import useLogin from "@/Hooks/useLogin"; import useLogin from "@/Hooks/useLogin";
import usePageDimensions from "@/Hooks/usePageDimensions"; import usePageDimensions from "@/Hooks/usePageDimensions";
import useWoT from "@/Hooks/useWoT";
import { ChatParticipantProfile } from "@/Pages/Messages/ChatParticipant"; import { ChatParticipantProfile } from "@/Pages/Messages/ChatParticipant";
import DmWindow from "@/Pages/Messages/DmWindow"; import DmWindow from "@/Pages/Messages/DmWindow";
import NewChatWindow from "@/Pages/Messages/NewChatWindow"; import NewChatWindow from "@/Pages/Messages/NewChatWindow";
@ -24,8 +26,12 @@ export default function MessagesPage() {
const { width: pageWidth } = usePageDimensions(); const { width: pageWidth } = usePageDimensions();
const chats = useChatSystems(); 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<HTMLDivElement>, type: ChatType, id: string) { function openChat(e: React.MouseEvent<HTMLDivElement>, type: ChatType, id: string) {
e.stopPropagation(); e.stopPropagation();
@ -81,26 +87,45 @@ export default function MessagesPage() {
); );
} }
return ( function sortMessages(a: Chat, b: Chat) {
<div className="flex flex-1 md:h-screen md:overflow-hidden">
{(pageWidth >= TwoCol || !id) && (
<div className="overflow-y-auto md:h-screen p-1 w-full md:w-1/3 flex-shrink-0">
<div className="flex items-center justify-between p-2">
<button disabled={unreadCount <= 0} type="button" className="text-sm font-semibold">
<FormattedMessage defaultMessage="Mark all read" />
</button>
<NewChatWindow />
</div>
{chats
.sort((a, b) => {
const aSelf = a.participants.length === 1 && a.participants[0].id === login.publicKey; const aSelf = a.participants.length === 1 && a.participants[0].id === login.publicKey;
const bSelf = b.participants.length === 1 && b.participants[0].id === login.publicKey; const bSelf = b.participants.length === 1 && b.participants[0].id === login.publicKey;
if (aSelf || bSelf) { if (aSelf || bSelf) {
return aSelf ? -1 : 1; return aSelf ? -1 : 1;
} }
return b.lastMessage > a.lastMessage ? 1 : -1; return b.lastMessage > a.lastMessage ? 1 : -1;
}) }
.map(conversation)}
return (
<div className="flex flex-1 md:h-screen md:overflow-hidden">
{(pageWidth >= TwoCol || !id) && (
<div className="overflow-y-auto md:h-screen p-1 w-full md:w-1/3 flex-shrink-0">
<div className="flex items-center justify-between p-2">
<button
disabled={unreadTrustedCount <= 0}
type="button"
className="text-sm font-semibold"
onClick={() => {
chats.forEach(c => c.markRead());
}}>
<FormattedMessage defaultMessage="Mark all read" />
</button>
<NewChatWindow />
</div>
{trustedChats.sort(sortMessages).map(conversation)}
{otherChats.sort(sortMessages).length > 0 && (
<>
<CollapsedSection
title={
<div className="text-xl flex items-center gap-4">
<FormattedMessage defaultMessage="Other Chats" />
{unreadOtherCount > 0 && <div className="has-unread" />}
</div>
}>
{otherChats.map(conversation)}
</CollapsedSection>
</>
)}
</div> </div>
)} )}
{id ? <DmWindow id={id} /> : pageWidth >= TwoCol && <div className="flex-1 rt-border"></div>} {id ? <DmWindow id={id} /> : pageWidth >= TwoCol && <div className="flex-1 rt-border"></div>}

View File

@ -1,4 +1,4 @@
import { unixNow, unwrap } from "@snort/shared"; import { ExternalStore, unixNow, unwrap } from "@snort/shared";
import { import {
encodeTLVEntries, encodeTLVEntries,
EventKind, EventKind,
@ -13,7 +13,7 @@ import {
UserMetadata, UserMetadata,
} from "@snort/system"; } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo, useSyncExternalStore } from "react";
import useEventPublisher from "@/Hooks/useEventPublisher"; import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin"; import useLogin from "@/Hooks/useLogin";
@ -55,6 +55,7 @@ export interface Chat {
messages: Array<ChatMessage>; messages: Array<ChatMessage>;
createMessage(msg: string, pub: EventPublisher): Promise<Array<NostrEvent>>; createMessage(msg: string, pub: EventPublisher): Promise<Array<NostrEvent>>;
sendMessage(ev: Array<NostrEvent>, system: SystemInterface): void | Promise<void>; sendMessage(ev: Array<NostrEvent>, system: SystemInterface): void | Promise<void>;
markRead(id?: string): void;
} }
export interface ChatSystem { export interface ChatSystem {
@ -104,10 +105,13 @@ export function lastReadInChat(id: string) {
return parseInt(window.localStorage.getItem(k) ?? "0"); return parseInt(window.localStorage.getItem(k) ?? "0");
} }
export function setLastReadIn(id: string) { export function setLastReadIn(id: string, time?: number) {
const now = unixNow(); const now = time ?? unixNow();
const k = `dm:seen:${id}`; const k = `dm:seen:${id}`;
const current = lastReadInChat(id);
if (current < now) {
window.localStorage.setItem(k, now.toString()); window.localStorage.setItem(k, now.toString());
}
} }
export function createChatLink(type: ChatType, ...params: Array<string>) { export function createChatLink(type: ChatType, ...params: Array<string>) {
@ -144,24 +148,28 @@ export function createEmptyChatObject(id: string) {
throw new Error("Cant create new empty chat, unknown id"); throw new Error("Cant create new empty chat, unknown id");
} }
export function useChatSystem(chat: ChatSystem) { export function useChatSystem<T extends ChatSystem & ExternalStore<Array<Chat>>>(sys: T) {
const login = useLogin(); const login = useLogin();
const { publisher } = useEventPublisher(); const { publisher } = useEventPublisher();
const chat = useSyncExternalStore(
s => sys.hook(s),
() => sys.snapshot(),
);
const sub = useMemo(() => { const sub = useMemo(() => {
return chat.subscription(login); return sys.subscription(login);
}, [chat, login]); }, [login]);
const data = useRequestBuilder(sub); const data = useRequestBuilder(sub);
const { isMuted } = useModeration(); const { isMuted } = useModeration();
useEffect(() => { useEffect(() => {
if (publisher) { if (publisher) {
chat.processEvents(publisher, data); sys.processEvents(publisher, data);
} }
}, [data, publisher]); }, [data, publisher]);
return useMemo(() => { return useMemo(() => {
if (login.publicKey) { if (login.publicKey) {
return chat.listChats( return sys.listChats(
login.publicKey, login.publicKey,
data.filter(a => !isMuted(a.pubkey)), data.filter(a => !isMuted(a.pubkey)),
); );
@ -177,12 +185,6 @@ export function useChatSystems() {
} }
export function useChat(id: string) { export function useChat(id: string) {
const getStore = () => { const ret = useChatSystem(Nip17Chats).find(a => a.id === id);
if (id.startsWith(NostrPrefix.Chat17)) {
return Nip17Chats;
}
throw new Error("Unsupported chat system");
};
const ret = useChatSystem(getStore()).find(a => a.id === id);
return ret; return ret;
} }

View File

@ -14,7 +14,7 @@ import {
import { GiftsCache } from "@/Cache"; import { GiftsCache } from "@/Cache";
import { GiftWrapCache } from "@/Cache/GiftWrapCache"; 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 { UnwrappedGift } from "@/Db";
import { LoginSession } from "@/Utils/Login"; import { LoginSession } from "@/Utils/Login";
import { GetPowWorker } from "@/Utils/wasm"; import { GetPowWorker } from "@/Utils/wasm";
@ -100,8 +100,8 @@ export class Nip17ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
type: ChatType.PrivateDirectMessage, type: ChatType.PrivateDirectMessage,
id, id,
title: title.title, title: title.title,
unread: messages.reduce((acc, v) => (v.created_at > last ? acc++ : acc), 0), unread: messages.reduce((acc, v) => (v.inner.created_at > last ? acc + 1 : acc), 0),
lastMessage: messages.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0), lastMessage: messages.reduce((acc, v) => (v.inner.created_at > acc ? v.created_at : acc), 0),
participants, participants,
messages: messages.map(m => ({ messages: messages.map(m => ({
id: m.id, id: m.id,
@ -128,11 +128,18 @@ export class Nip17ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
messages.push(recvSealedN); messages.push(recvSealedN);
} }
messages.push(pub.giftWrap(await pub.sealRumor(gossip, pub.pubKey), pub.pubKey, powTarget, GetPowWorker())); 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) => { sendMessage: (ev, system) => {
ev.forEach(a => system.BroadcastEvent(a)); 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; } as Chat;
} }

View File

@ -98,6 +98,9 @@
"0mch2Y": { "0mch2Y": {
"defaultMessage": "name has disallowed characters" "defaultMessage": "name has disallowed characters"
}, },
"0oMk/p": {
"defaultMessage": "Other Chats"
},
"0siT4z": { "0siT4z": {
"defaultMessage": "Politics" "defaultMessage": "Politics"
}, },
@ -2407,9 +2410,6 @@
"wc9st7": { "wc9st7": {
"defaultMessage": "Media Attachments" "defaultMessage": "Media Attachments"
}, },
"whSrs+": {
"defaultMessage": "Nostr Public Chat"
},
"wih7iJ": { "wih7iJ": {
"defaultMessage": "name is blocked" "defaultMessage": "name is blocked"
}, },

View File

@ -32,6 +32,7 @@
"0jOEtS": "Invalid LNURL", "0jOEtS": "Invalid LNURL",
"0kOBMu": "Handling Mentions", "0kOBMu": "Handling Mentions",
"0mch2Y": "name has disallowed characters", "0mch2Y": "name has disallowed characters",
"0oMk/p": "Other Chats",
"0siT4z": "Politics", "0siT4z": "Politics",
"0uoY11": "Show Status", "0uoY11": "Show Status",
"0yO7wF": "{n} secs", "0yO7wF": "{n} secs",
@ -799,7 +800,6 @@
"wOyDTB": "File storage server list", "wOyDTB": "File storage server list",
"wSZR47": "Submit", "wSZR47": "Submit",
"wc9st7": "Media Attachments", "wc9st7": "Media Attachments",
"whSrs+": "Nostr Public Chat",
"wih7iJ": "name is blocked", "wih7iJ": "name is blocked",
"wlWMuh": "Patches", "wlWMuh": "Patches",
"wofVHy": "Moderation", "wofVHy": "Moderation",