import { unixNow, unwrap } from "@snort/shared"; import { encodeTLVEntries, EventKind, EventPublisher, NostrEvent, NostrPrefix, RequestBuilder, SystemInterface, TaggedNostrEvent, TLVEntry, TLVEntryType, UserMetadata, } from "@snort/system"; import { useSyncExternalStore } from "react"; import { Chats, GiftsCache } from "@/Cache"; import useLogin from "@/Hooks/useLogin"; import useModeration from "@/Hooks/useModeration"; import { findTag } from "@/Utils"; import { LoginSession } from "@/Utils/Login"; import { Nip4ChatSystem } from "./nip4"; import { Nip24ChatSystem } from "./nip24"; import { Nip28ChatSystem } from "./nip28"; import { Nip29ChatSystem } from "./nip29"; export enum ChatType { DirectMessage = 1, PublicGroupChat = 2, PrivateGroupChat = 3, PrivateDirectMessage = 4, } export interface ChatMessage { id: string; from: string; created_at: number; tags: Array>; needsDecryption: boolean; content: string; decrypt: (pub: EventPublisher) => Promise; } export interface ChatParticipant { type: "pubkey" | "generic"; id: string; profile?: UserMetadata; } export interface Chat { type: ChatType; id: string; title?: string; unread: number; lastMessage: number; participants: Array; messages: Array; createMessage(msg: string, pub: EventPublisher): Promise>; sendMessage(ev: Array, system: SystemInterface): void | Promise; } 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; } 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 */ export function chatTo(e: NostrEvent) { if (e.kind === EventKind.DirectMessage) { return unwrap(findTag(e, "p")); } else if (e.kind === EventKind.SimpleChatMessage) { const gt = unwrap(e.tags.find(a => a[0] === "g")); return `${gt[2]}${gt[1]}`; } throw new Error("Not a chat message"); } export function inChatWith(e: NostrEvent, myPk: string) { if (e.pubkey === myPk) { return chatTo(e); } else { return e.pubkey; } } export function selfChat(e: NostrEvent, myPk: string) { return chatTo(e) === myPk && e.pubkey === myPk; } export function lastReadInChat(id: string) { const k = `dm:seen:${id}`; return parseInt(window.localStorage.getItem(k) ?? "0"); } export function setLastReadIn(id: string) { const now = unixNow(); const k = `dm:seen:${id}`; window.localStorage.setItem(k, now.toString()); } export function createChatLink(type: ChatType, ...params: Array) { switch (type) { case ChatType.DirectMessage: { if (params.length > 1) throw new Error("Must only contain one pubkey"); return `/messages/${encodeTLVEntries( "chat4" as NostrPrefix, { type: TLVEntryType.Author, length: params[0].length, value: params[0], } as TLVEntry, )}`; } case ChatType.PrivateDirectMessage: { if (params.length > 1) throw new Error("Must only contain one pubkey"); return `/messages/${encodeTLVEntries( "chat24" as NostrPrefix, { type: TLVEntryType.Author, length: params[0].length, value: params[0], } as TLVEntry, )}`; } case ChatType.PrivateGroupChat: { return `/messages/${encodeTLVEntries( "chat24" as NostrPrefix, ...params.map( a => ({ type: TLVEntryType.Author, length: a.length, value: a, }) as TLVEntry, ), )}`; } case ChatType.PublicGroupChat: { return `/messages/${Nip28ChatSystem.chatId(params[0])}`; } } throw new Error("Unknown chat type"); } export function createEmptyChatObject(id: string) { if (id.startsWith("chat41")) { return Nip4ChatSystem.createChatObj(id, []); } if (id.startsWith("chat241")) { return Nip24ChatSystem.createChatObj(id, []); } if (id.startsWith("chat281")) { return Nip28ChatSystem.createChatObj(id, []); } throw new Error("Cant create new empty chat, unknown id"); } export function useNip4Chat() { const { publicKey } = useLogin(s => ({ 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(); 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)); }); }