212 lines
5.5 KiB
TypeScript
212 lines
5.5 KiB
TypeScript
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<Array<string>>;
|
|
needsDecryption: boolean;
|
|
content: string;
|
|
decrypt: (pub: EventPublisher) => Promise<string>;
|
|
}
|
|
|
|
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<ChatParticipant>;
|
|
messages: Array<ChatMessage>;
|
|
createMessage(msg: string, pub: EventPublisher): Promise<Array<NostrEvent>>;
|
|
sendMessage(ev: Array<NostrEvent>, system: SystemInterface): void | Promise<void>;
|
|
}
|
|
|
|
export interface ChatSystem {
|
|
/**
|
|
* Create a request for this system to get updates
|
|
*/
|
|
subscription(session: LoginSession): RequestBuilder | undefined;
|
|
onEvent(evs: readonly TaggedNostrEvent[]): Promise<void> | void;
|
|
|
|
listChats(pk: string): Array<Chat>;
|
|
}
|
|
|
|
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<string>) {
|
|
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));
|
|
});
|
|
}
|