diff --git a/packages/app/src/Cache/ProfileWorkeCache.ts b/packages/app/src/Cache/ProfileWorkerCache.ts similarity index 99% rename from packages/app/src/Cache/ProfileWorkeCache.ts rename to packages/app/src/Cache/ProfileWorkerCache.ts index aa1774ef..88d86a6d 100644 --- a/packages/app/src/Cache/ProfileWorkeCache.ts +++ b/packages/app/src/Cache/ProfileWorkerCache.ts @@ -88,6 +88,7 @@ export class ProfileCacheRelayWorker extends EventEmitter implement async bulkSet(obj: CachedMetadata[] | readonly CachedMetadata[]) { const mapped = obj.map(a => this.key(a)); mapped.forEach(a => this.#keys.add(a)); + // todo: store in cache this.emit("change", mapped); } diff --git a/packages/app/src/Cache/index.ts b/packages/app/src/Cache/index.ts index 73c98604..5bafffe3 100644 --- a/packages/app/src/Cache/index.ts +++ b/packages/app/src/Cache/index.ts @@ -3,10 +3,9 @@ import { SnortSystemDb } from "@snort/system-web"; import { WorkerRelayInterface } from "@snort/worker-relay"; import WorkerRelayPath from "@snort/worker-relay/dist/worker?worker&url"; -import { ChatCache } from "./ChatCache"; import { EventCacheWorker } from "./EventCacheWorker"; import { GiftWrapCache } from "./GiftWrapCache"; -import { ProfileCacheRelayWorker } from "./ProfileWorkeCache"; +import { ProfileCacheRelayWorker } from "./ProfileWorkerCache"; export const Relay = new WorkerRelayInterface(WorkerRelayPath); export async function initRelayWorker() { @@ -24,13 +23,11 @@ export const RelayMetrics = new RelayMetricCache(SystemDb.relayMetrics); export const UserCache = new ProfileCacheRelayWorker(Relay); export const EventsCache = new EventCacheWorker(Relay); -export const Chats = new ChatCache(); export const GiftsCache = new GiftWrapCache(); export async function preload(follows?: Array) { const preloads = [ UserCache.preload(), - Chats.preload(), RelayMetrics.preload(), GiftsCache.preload(), UserRelays.preload(follows), diff --git a/packages/app/src/Feed/LoginFeed.ts b/packages/app/src/Feed/LoginFeed.ts index 05d80b22..b8904be9 100644 --- a/packages/app/src/Feed/LoginFeed.ts +++ b/packages/app/src/Feed/LoginFeed.ts @@ -3,7 +3,6 @@ import { useRequestBuilder } from "@snort/system-react"; import { usePrevious } from "@uidotdev/usehooks"; import { useEffect, useMemo } from "react"; -import { Nip4Chats, Nip28Chats } from "@/chat"; import { Nip28ChatSystem } from "@/chat/nip28"; import useEventPublisher from "@/Hooks/useEventPublisher"; import useLogin from "@/Hooks/useLogin"; @@ -77,14 +76,6 @@ export default function useLoginFeed() { .limit(10); } - const n4Sub = Nip4Chats.subscription(login); - if (n4Sub) { - b.add(n4Sub); - } - const n28Sub = Nip28Chats.subscription(login); - if (n28Sub) { - b.add(n28Sub); - } return b; }, [login]); @@ -105,9 +96,6 @@ export default function useLoginFeed() { setRelays(login, Object.fromEntries(parsedRelays), relays.created_at * 1000); } - Nip4Chats.onEvent(loginFeed); - Nip28Chats.onEvent(loginFeed); - if (publisher) { const subs = loginFeed.filter( a => a.kind === EventKind.SnortSubscriptions && a.pubkey === bech32ToHex(SnortPubKey), diff --git a/packages/app/src/Pages/Messages/MessagesPage.tsx b/packages/app/src/Pages/Messages/MessagesPage.tsx index 24518174..544ab6b5 100644 --- a/packages/app/src/Pages/Messages/MessagesPage.tsx +++ b/packages/app/src/Pages/Messages/MessagesPage.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useNavigate, useParams } from "react-router-dom"; -import { Chat, ChatType, useChatSystem } from "@/chat"; +import { Chat, ChatType, useChatSystems } from "@/chat"; import NoteTime from "@/Components/Event/Note/NoteTime"; import NoteToSelf from "@/Components/User/NoteToSelf"; import ProfileImage from "@/Components/User/ProfileImage"; @@ -23,7 +23,7 @@ export default function MessagesPage() { const { id } = useParams(); const pageWidth = usePageWidth(); - const chats = useChatSystem(); + const chats = useChatSystems(); const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unread, 0), [chats]); @@ -83,7 +83,7 @@ export default function MessagesPage() { return (
- {pageWidth >= TwoCol && !id && ( + {pageWidth >= TwoCol && (
diff --git a/packages/app/src/Utils/Login/Functions.ts b/packages/app/src/Utils/Login/Functions.ts index 48164486..f92bbd30 100644 --- a/packages/app/src/Utils/Login/Functions.ts +++ b/packages/app/src/Utils/Login/Functions.ts @@ -12,7 +12,7 @@ import { UserMetadata, } from "@snort/system"; -import { Chats, GiftsCache } from "@/Cache"; +import { GiftsCache } from "@/Cache"; import SnortApi from "@/External/SnortApi"; import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils"; import { Blasters } from "@/Utils/Const"; @@ -68,7 +68,6 @@ export function updatePreferences(id: string, p: UserPreferences) { export function logout(id: string) { LoginStore.removeSession(id); GiftsCache.clear(); - Chats.clear(); deleteRefCode(); localStorage.clear(); } diff --git a/packages/app/src/chat/index.ts b/packages/app/src/chat/index.ts index d399d932..1a9e3af8 100644 --- a/packages/app/src/chat/index.ts +++ b/packages/app/src/chat/index.ts @@ -12,19 +12,18 @@ import { TLVEntryType, UserMetadata, } from "@snort/system"; -import { useSyncExternalStore } from "react"; +import { useRequestBuilder } from "@snort/system-react"; +import { useMemo } from "react"; -import { Chats, GiftsCache } from "@/Cache"; import { useEmptyChatSystem } from "@/Hooks/useEmptyChatSystem"; import useLogin from "@/Hooks/useLogin"; import useModeration from "@/Hooks/useModeration"; import { findTag } from "@/Utils"; import { LoginSession } from "@/Utils/Login"; -import { Nip4ChatSystem } from "./nip4"; +import { Nip4Chats, Nip4ChatSystem } from "./nip4"; import { Nip24ChatSystem } from "./nip24"; -import { Nip28ChatSystem } from "./nip28"; -import { Nip29ChatSystem } from "./nip29"; +import { Nip28Chats, Nip28ChatSystem } from "./nip28"; export enum ChatType { DirectMessage = 1, @@ -66,16 +65,13 @@ export interface ChatSystem { * Create a request for this system to get updates */ subscription(session: LoginSession): RequestBuilder | undefined; - onEvent(evs: readonly TaggedNostrEvent[]): Promise | void; - listChats(pk: string): Array; + /** + * Create a list of chats for a given pubkey and set of events + */ + listChats(pk: string, evs: Array): Array; } -export const Nip4Chats = new Nip4ChatSystem(Chats); -export const Nip29Chats = new Nip29ChatSystem(Chats); -export const Nip24Chats = new Nip24ChatSystem(GiftsCache); -export const Nip28Chats = new Nip28ChatSystem(Chats); - /** * Extract the P tag of the event */ @@ -169,46 +165,30 @@ export function createEmptyChatObject(id: string, messages?: Array ({ publicKey: s.publicKey })); - return useSyncExternalStore( - c => Nip4Chats.hook(c), - () => Nip4Chats.snapshot(publicKey), - ); -} - -export function useNip29Chat() { - return useSyncExternalStore( - c => Nip29Chats.hook(c), - () => Nip29Chats.snapshot(), - ); -} - -export function useNip24Chat() { - const { publicKey } = useLogin(s => ({ publicKey: s.publicKey })); - return useSyncExternalStore( - c => Nip24Chats.hook(c), - () => Nip24Chats.snapshot(publicKey), - ); -} - -export function useNip28Chat() { - return useSyncExternalStore( - c => Nip28Chats.hook(c), - () => Nip28Chats.snapshot(), - ); -} - -export function useChatSystem() { - const nip4 = useNip4Chat(); - //const nip24 = useNip24Chat(); - const nip28 = useNip28Chat(); +export function useChatSystem(chat: ChatSystem) { + const login = useLogin(); + const sub = useMemo(() => { + return chat.subscription(login); + }, [login.publicKey]); + const data = useRequestBuilder(sub); const { isBlocked } = useModeration(); - return [...nip4, ...nip28].filter(a => { - const authors = a.participants.filter(a => a.type === "pubkey").map(a => a.id); - return authors.length === 0 || !authors.every(a => isBlocked(a)); - }); + return useMemo(() => { + if (login.publicKey) { + return chat.listChats( + login.publicKey, + data.filter(a => !isBlocked(a.pubkey)), + ); + } + return []; + }, [login.publicKey, data]); +} + +export function useChatSystems() { + const nip4 = useChatSystem(Nip4Chats); + const nip28 = useChatSystem(Nip28Chats); + + return [...nip4, ...nip28]; } export function useChat(id: string) { @@ -221,13 +201,7 @@ export function useChat(id: string) { } throw new Error("Unsupported chat system"); }; - const store = getStore(); - const ret = useSyncExternalStore( - c => store.hook(c), - () => { - return store.snapshot().find(a => a.id === id); - }, - ); + const ret = useChatSystem(getStore()).find(a => a.id === id); const emptyChat = useEmptyChatSystem(ret === undefined ? id : undefined); return ret ?? emptyChat; } diff --git a/packages/app/src/chat/nip28.ts b/packages/app/src/chat/nip28.ts index e7f34259..0eb12868 100644 --- a/packages/app/src/chat/nip28.ts +++ b/packages/app/src/chat/nip28.ts @@ -1,4 +1,4 @@ -import { ExternalStore, FeedCache, unwrap } from "@snort/shared"; +import { unwrap } from "@snort/shared"; import { decodeTLV, encodeTLVEntries, @@ -11,15 +11,12 @@ import { TLVEntryType, UserMetadata, } from "@snort/system"; -import debug from "debug"; import { Chat, ChatParticipant, ChatSystem, ChatType, lastReadInChat } from "@/chat"; import { findTag } from "@/Utils"; import { LoginSession } from "@/Utils/Login"; -export class Nip28ChatSystem extends ExternalStore> implements ChatSystem { - #cache: FeedCache; - #log = debug("NIP-28"); +export class Nip28ChatSystem implements ChatSystem { readonly ChannelKinds = [ EventKind.PublicChatChannel, EventKind.PublicChatMessage, @@ -28,44 +25,26 @@ export class Nip28ChatSystem extends ExternalStore> implements ChatS EventKind.PublicChatMuteUser, ]; - constructor(cache: FeedCache) { - super(); - this.#cache = cache; - } - subscription(session: LoginSession): RequestBuilder | undefined { const chats = (session.extraChats ?? []).filter(a => a.startsWith("chat281")); if (chats.length === 0) return; const chatId = (v: string) => unwrap(decodeTLV(v).find(a => a.type === TLVEntryType.Special)).value as string; - const messages = this.#chatChannels(); const rb = new RequestBuilder(`nip28:${session.id}`); rb.withFilter() .ids(chats.map(v => chatId(v))) .kinds([EventKind.PublicChatChannel, EventKind.PublicChatMetadata]); for (const c of chats) { const id = chatId(c); - const lastMessage = messages[id]?.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0) ?? 0; - rb.withFilter() - .tag("e", [id]) - .since(lastMessage === 0 ? undefined : lastMessage) - .kinds(this.ChannelKinds); + rb.withFilter().tag("e", [id]).kinds(this.ChannelKinds); } return rb; } - async onEvent(evs: readonly TaggedNostrEvent[]) { - const dms = evs.filter(a => this.ChannelKinds.includes(a.kind)); - if (dms.length > 0) { - await this.#cache.bulkSet(dms); - this.notifyChange(); - } - } - - listChats(): Chat[] { - const chats = this.#chatChannels(); + listChats(pk: string, evs: Array): Chat[] { + const chats = this.#chatChannels(evs); const ret = Object.entries(chats).map(([k, v]) => { return Nip28ChatSystem.createChatObj(Nip28ChatSystem.chatId(k), v); }); @@ -121,10 +100,6 @@ export class Nip28ChatSystem extends ExternalStore> implements ChatS } as Chat; } - takeSnapshot(): Chat[] { - return this.listChats(); - } - static #chatProfileFromMessages(messages: Array) { const chatDefs = messages.filter( a => a.kind === EventKind.PublicChatChannel || a.kind === EventKind.PublicChatMetadata, @@ -136,9 +111,8 @@ export class Nip28ChatSystem extends ExternalStore> implements ChatS return chatDef ? (JSON.parse(chatDef.content) as UserMetadata) : undefined; } - #chatChannels() { - const messages = this.#cache.snapshot(); - const chats = messages.reduce( + #chatChannels(evs: Array) { + const chats = evs.reduce( (acc, v) => { const k = this.#chatId(v); if (k) { @@ -162,3 +136,5 @@ export class Nip28ChatSystem extends ExternalStore> implements ChatS } } } + +export const Nip28Chats = new Nip28ChatSystem(); diff --git a/packages/app/src/chat/nip4.ts b/packages/app/src/chat/nip4.ts index eba2c72c..8574f94e 100644 --- a/packages/app/src/chat/nip4.ts +++ b/packages/app/src/chat/nip4.ts @@ -1,4 +1,3 @@ -import { ExternalStore, FeedCache } from "@snort/shared"; import { decodeTLV, encodeTLVEntries, @@ -10,51 +9,23 @@ import { TaggedNostrEvent, TLVEntryType, } from "@snort/system"; -import { debug } from "debug"; import { Chat, ChatSystem, ChatType, inChatWith, lastReadInChat } from "@/chat"; import { LoginSession } from "@/Utils/Login"; -export class Nip4ChatSystem extends ExternalStore> implements ChatSystem { - #cache: FeedCache; - #log = debug("NIP-04"); - - constructor(cache: FeedCache) { - super(); - this.#cache = cache; - } - - async onEvent(evs: readonly TaggedNostrEvent[]) { - const dms = evs.filter(a => a.kind === EventKind.DirectMessage && a.tags.some(b => b[0] === "p")); - if (dms.length > 0) { - await this.#cache.bulkSet(dms); - this.notifyChange(); - } - } - +export class Nip4ChatSystem implements ChatSystem { subscription(session: LoginSession) { const pk = session.publicKey; if (!pk || session.readonly) return; const rb = new RequestBuilder(`nip4:${pk.slice(0, 12)}`); - const dms = this.#cache.snapshot(); - const dmSince = dms.reduce( - (acc, v) => (v.created_at > acc && v.kind === EventKind.DirectMessage ? (acc = v.created_at) : acc), - 0, - ); - - this.#log("Loading DMS since %s", new Date(dmSince * 1000)); - rb.withFilter().authors([pk]).kinds([EventKind.DirectMessage]).since(dmSince); - rb.withFilter().kinds([EventKind.DirectMessage]).tag("p", [pk]).since(dmSince); + rb.withFilter().authors([pk]).kinds([EventKind.DirectMessage]); + rb.withFilter().kinds([EventKind.DirectMessage]).tag("p", [pk]); return rb; } - takeSnapshot(p: string) { - return this.listChats(p); - } - - listChats(pk: string): Chat[] { - const myDms = this.#nip4Events(); + listChats(pk: string, evs: Array): Chat[] { + const myDms = this.#nip4Events(evs); const chats = myDms.reduce( (acc, v) => { const chatId = inChatWith(v, pk); @@ -110,7 +81,9 @@ export class Nip4ChatSystem extends ExternalStore> implements ChatSy } as Chat; } - #nip4Events() { - return this.#cache.snapshot().filter(a => a.kind === EventKind.DirectMessage); + #nip4Events(evs: Array) { + return evs.filter(a => a.kind === EventKind.DirectMessage); } } + +export const Nip4Chats = new Nip4ChatSystem(); diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index 73829652..fc594874 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -330,9 +330,6 @@ "9wO4wJ": { "defaultMessage": "Lightning Invoice" }, - "ABAQyo": { - "defaultMessage": "Chats" - }, "ADmfQT": { "defaultMessage": "Parent", "description": "Link to parent note in thread" diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index e5cf15b7..c0d24dd0 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -109,7 +109,6 @@ "9kSari": "Retry publishing", "9pMqYs": "Nostr Address", "9wO4wJ": "Lightning Invoice", - "ABAQyo": "Chats", "ADmfQT": "Parent", "ALdW69": "Note by {name}", "AN0Z7Q": "Muted Words",