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.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"

View File

@ -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 || "<ERROR>");
if (!isMe) {
setLastReadIn(msg.id);
}
props.chat.markRead(msg.id);
}
}

View File

@ -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<HTMLDivElement>, 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 (
<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">
<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>
{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 && (
<>
<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>
)}
{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 {
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<ChatMessage>;
createMessage(msg: string, pub: EventPublisher): Promise<Array<NostrEvent>>;
sendMessage(ev: Array<NostrEvent>, system: SystemInterface): void | Promise<void>;
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<string>) {
@ -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<T extends ChatSystem & ExternalStore<Array<Chat>>>(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;
}

View File

@ -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<Array<Chat>> 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<Array<Chat>> 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;
}

View File

@ -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"
},

View File

@ -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",