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.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"
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="flex flex-1 md:h-screen md:overflow-hidden">
|
<div className="flex flex-1 md:h-screen md:overflow-hidden">
|
||||||
{(pageWidth >= TwoCol || !id) && (
|
{(pageWidth >= TwoCol || !id) && (
|
||||||
<div className="overflow-y-auto md:h-screen p-1 w-full md:w-1/3 flex-shrink-0">
|
<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">
|
<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" />
|
<FormattedMessage defaultMessage="Mark all read" />
|
||||||
</button>
|
</button>
|
||||||
<NewChatWindow />
|
<NewChatWindow />
|
||||||
</div>
|
</div>
|
||||||
{chats
|
{trustedChats.sort(sortMessages).map(conversation)}
|
||||||
.sort((a, b) => {
|
{otherChats.sort(sortMessages).length > 0 && (
|
||||||
const aSelf = a.participants.length === 1 && a.participants[0].id === login.publicKey;
|
<>
|
||||||
const bSelf = b.participants.length === 1 && b.participants[0].id === login.publicKey;
|
<CollapsedSection
|
||||||
if (aSelf || bSelf) {
|
title={
|
||||||
return aSelf ? -1 : 1;
|
<div className="text-xl flex items-center gap-4">
|
||||||
}
|
<FormattedMessage defaultMessage="Other Chats" />
|
||||||
return b.lastMessage > a.lastMessage ? 1 : -1;
|
{unreadOtherCount > 0 && <div className="has-unread" />}
|
||||||
})
|
</div>
|
||||||
.map(conversation)}
|
}>
|
||||||
|
{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>}
|
||||||
|
@ -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}`;
|
||||||
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>) {
|
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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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",
|
||||||
|
Reference in New Issue
Block a user