feat: improve messages:
1. WoT filter 2. React to read status
This commit is contained in:
@ -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"
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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",
|
||||
|
Reference in New Issue
Block a user