feat: nip-28

This commit is contained in:
Kieran 2023-09-25 11:59:02 +01:00
parent 2833813ef3
commit 8e414a10c5
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
19 changed files with 318 additions and 59 deletions

View File

@ -1,6 +1,7 @@
{ {
"recommendations": [ "recommendations": [
"arcanis.vscode-zipfs", "arcanis.vscode-zipfs",
"dbaeumer.vscode-eslint" "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
] ]
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "eslint", "name": "eslint",
"version": "8.44.0-sdk", "version": "8.48.0-sdk",
"main": "./lib/api.js", "main": "./lib/api.js",
"type": "commonjs" "type": "commonjs"
} }

20
.yarn/sdks/prettier/index.js vendored Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require prettier
require(absPnpApiPath).setup();
}
}
// Defer to the real prettier your application uses
module.exports = absRequire(`prettier`);

6
.yarn/sdks/prettier/package.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"name": "prettier",
"version": "3.0.3-sdk",
"main": "./index.js",
"type": "commonjs"
}

View File

@ -1,6 +1,6 @@
{ {
"name": "typescript", "name": "typescript",
"version": "5.1.6-sdk", "version": "5.2.2-sdk",
"main": "./lib/typescript.js", "main": "./lib/typescript.js",
"type": "commonjs" "type": "commonjs"
} }

View File

@ -9,11 +9,6 @@
"test": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/app test && yarn workspace @snort/system test", "test": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/app test && yarn workspace @snort/system test",
"pre:commit": "yarn workspace @snort/app intl-extract && yarn workspace @snort/app intl-compile && yarn prettier --write ." "pre:commit": "yarn workspace @snort/app intl-extract && yarn workspace @snort/app intl-compile && yarn prettier --write ."
}, },
"devDependencies": {
"@cloudflare/workers-types": "^4.20230307.0",
"@tauri-apps/cli": "^1.2.3",
"prettier": "^3.0.0"
},
"prettier": { "prettier": {
"printWidth": 120, "printWidth": 120,
"bracketSameLine": true, "bracketSameLine": true,
@ -21,7 +16,10 @@
}, },
"packageManager": "yarn@3.6.3", "packageManager": "yarn@3.6.3",
"dependencies": { "dependencies": {
"@cloudflare/workers-types": "^4.20230307.0",
"@tauri-apps/cli": "^1.2.3",
"eslint": "^8.48.0", "eslint": "^8.48.0",
"prettier": "^3.0.3",
"typescript": "^5.2.2" "typescript": "^5.2.2"
} }
} }

View File

@ -0,0 +1,12 @@
import { ChatParticipant } from "chat";
import NoteToSelf from "./NoteToSelf";
import ProfileImage from "./ProfileImage";
import useLogin from "Hooks/useLogin";
export function ChatParticipantProfile({ participant }: { participant: ChatParticipant }) {
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
if (participant.id === publicKey) {
return <NoteToSelf className="f-grow" pubkey={participant.id} />;
}
return <ProfileImage pubkey={participant.id} className="f-grow" profile={participant.profile} />;
}

View File

@ -3,33 +3,19 @@ import { useMemo } from "react";
import ProfileImage from "Element/ProfileImage"; import ProfileImage from "Element/ProfileImage";
import DM from "Element/DM"; import DM from "Element/DM";
import NoteToSelf from "Element/NoteToSelf";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import WriteMessage from "Element/WriteMessage"; import WriteMessage from "Element/WriteMessage";
import { Chat, ChatParticipant, createEmptyChatObject, useChatSystem } from "chat"; import { Chat, createEmptyChatObject, useChatSystem } from "chat";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { ChatParticipantProfile } from "./ChatParticipant";
export default function DmWindow({ id }: { id: string }) { export default function DmWindow({ id }: { id: string }) {
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
const dms = useChatSystem(); const dms = useChatSystem();
const chat = dms.find(a => a.id === id) ?? createEmptyChatObject(id); const chat = dms.find(a => a.id === id) ?? createEmptyChatObject(id);
function participant(p: ChatParticipant) {
if (p.id === publicKey) {
return <NoteToSelf className="f-grow mb-10" pubkey={p.id} />;
}
if (p.type === "pubkey") {
return <ProfileImage pubkey={p.id} className="f-grow mb10" />;
}
if (p?.profile) {
return <ProfileImage pubkey={p.id} className="f-grow mb10" profile={p.profile} />;
}
return <ProfileImage pubkey={p.id} className="f-grow mb10" overrideUsername={p.id} />;
}
function sender() { function sender() {
if (chat.participants.length === 1) { if (chat.participants.length === 1) {
return participant(chat.participants[0]); return <ChatParticipantProfile participant={chat.participants[0]} />;
} else { } else {
return ( return (
<div className="flex pfp-overlap mb10"> <div className="flex pfp-overlap mb10">

View File

@ -1,6 +1,6 @@
import "./ProfilePreview.css"; import "./ProfilePreview.css";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { HexKey } from "@snort/system"; import { HexKey, UserMetadata } from "@snort/system";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
@ -13,6 +13,7 @@ export interface ProfilePreviewProps {
about?: boolean; about?: boolean;
linkToProfile?: boolean; linkToProfile?: boolean;
}; };
profile?: UserMetadata;
actions?: ReactNode; actions?: ReactNode;
className?: string; className?: string;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void; onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
@ -41,6 +42,7 @@ export default function ProfilePreview(props: ProfilePreviewProps) {
<> <>
<ProfileImage <ProfileImage
pubkey={pubkey} pubkey={pubkey}
profile={props.profile}
link={options.linkToProfile ?? true ? undefined : ""} link={options.linkToProfile ?? true ? undefined : ""}
subHeader={options.about ? <div className="about">{user?.about}</div> : undefined} subHeader={options.about ? <div className="about">{user?.about}</div> : undefined}
/> />

View File

@ -25,7 +25,7 @@ import { SubscriptionEvent } from "Subscription";
import useRelaysFeedFollows from "./RelaysFeedFollows"; import useRelaysFeedFollows from "./RelaysFeedFollows";
import { FollowsFeed, GiftsCache, Notifications, UserRelays } from "Cache"; import { FollowsFeed, GiftsCache, Notifications, UserRelays } from "Cache";
import { System } from "index"; import { System } from "index";
import { Nip4Chats } from "chat"; import { Nip28Chats, Nip4Chats } from "chat";
import { useRefreshFeedCache } from "Hooks/useRefreshFeedcache"; import { useRefreshFeedCache } from "Hooks/useRefreshFeedcache";
/** /**
@ -42,7 +42,7 @@ export default function useLoginFeed() {
useRefreshFeedCache(GiftsCache, true); useRefreshFeedCache(GiftsCache, true);
const subLogin = useMemo(() => { const subLogin = useMemo(() => {
if (!pubKey) return null; if (!login || !pubKey) return null;
const b = new RequestBuilder(`login:${pubKey.slice(0, 12)}`); const b = new RequestBuilder(`login:${pubKey.slice(0, 12)}`);
b.withOptions({ b.withOptions({
@ -57,10 +57,16 @@ export default function useLoginFeed() {
.tag("p", [pubKey]) .tag("p", [pubKey])
.limit(1); .limit(1);
b.add(Nip4Chats.subscription(pubKey)); const n4Sub = Nip4Chats.subscription(login);
if (n4Sub) {
b.add(n4Sub);
}
const n28Sub = Nip28Chats.subscription(login);
if (n28Sub) {
b.add(n28Sub);
}
return b; return b;
}, [pubKey]); }, [login]);
const subLists = useMemo(() => { const subLists = useMemo(() => {
if (!pubKey) return null; if (!pubKey) return null;
@ -94,6 +100,7 @@ export default function useLoginFeed() {
} }
Nip4Chats.onEvent(loginFeed.data); Nip4Chats.onEvent(loginFeed.data);
Nip28Chats.onEvent(loginFeed.data);
const subs = loginFeed.data.filter( const subs = loginFeed.data.filter(
a => a.kind === EventKind.SnortSubscriptions && a.pubkey === bech32ToHex(SnortPubKey), a => a.kind === EventKind.SnortSubscriptions && a.pubkey === bech32ToHex(SnortPubKey),

View File

@ -123,4 +123,9 @@ export interface LoginSession {
* Snort application data * Snort application data
*/ */
appData: Newest<SnortAppData>; appData: Newest<SnortAppData>;
/**
* A list of chats which we have joined (NIP-28/NIP-29)
*/
extraChats: Array<string>;
} }

View File

@ -52,6 +52,7 @@ const LoggedOut = {
}, },
timestamp: 0, timestamp: 0,
}, },
extraChats: [],
} as LoginSession; } as LoginSession;
export class MultiAccountStore extends ExternalStore<LoginSession> { export class MultiAccountStore extends ExternalStore<LoginSession> {
@ -79,10 +80,11 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
} }
v.appData ??= { v.appData ??= {
item: { item: {
mutedWords: [] mutedWords: [],
}, },
timestamp: 0 timestamp: 0,
} };
v.extraChats ??= [];
} }
} }

View File

@ -3,7 +3,7 @@ import "./MessagesPage.css";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { TLVEntryType, decodeTLV } from "@snort/system"; import { NostrLink, NostrPrefix, TLVEntryType, UserMetadata, decodeTLV } from "@snort/system";
import { useUserProfile, useUserSearch } from "@snort/system-react"; import { useUserProfile, useUserSearch } from "@snort/system-react";
import UnreadCount from "Element/UnreadCount"; import UnreadCount from "Element/UnreadCount";
@ -21,6 +21,10 @@ import Text from "Element/Text";
import { Chat, ChatType, createChatLink, useChatSystem } from "chat"; import { Chat, ChatType, createChatLink, useChatSystem } from "chat";
import Modal from "Element/Modal"; import Modal from "Element/Modal";
import ProfilePreview from "Element/ProfilePreview"; import ProfilePreview from "Element/ProfilePreview";
import { useEventFeed } from "Feed/EventFeed";
import { LoginSession, LoginStore } from "Login";
import { Nip28ChatSystem } from "chat/nip28";
import { ChatParticipantProfile } from "Element/ChatParticipant";
const TwoCol = 768; const TwoCol = 768;
const ThreeCol = 1500; const ThreeCol = 1500;
@ -57,18 +61,12 @@ export default function MessagesPage() {
function conversationIdent(cx: Chat) { function conversationIdent(cx: Chat) {
if (cx.participants.length === 1) { if (cx.participants.length === 1) {
const p = cx.participants[0]; return <ChatParticipantProfile participant={cx.participants[0]} />;
if (p.type === "pubkey") {
return <ProfileImage pubkey={p.id} className="f-grow" link="" />;
} else {
return <ProfileImage pubkey={""} overrideUsername={p.id} className="f-grow" link="" />;
}
} else { } else {
return ( return (
<div className="flex f-grow pfp-overlap"> <div className="flex f-grow pfp-overlap">
{cx.participants.map(v => ( {cx.participants.map(v => (
<ProfileImage pubkey={v.id} link="" showUsername={false} /> <ProfileImage pubkey={v.id} link="" showUsername={false} profile={v.profile} />
))} ))}
{cx.title ?? <FormattedMessage defaultMessage="Group Chat" />} {cx.title ?? <FormattedMessage defaultMessage="Group Chat" />}
</div> </div>
@ -168,17 +166,17 @@ function ProfileDmActions({ id }: { id: string }) {
function NewChatWindow() { function NewChatWindow() {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [newChat, setNewChat] = useState<string[]>([]); const [newChat, setNewChat] = useState<Array<string>>([]);
const [results, setResults] = useState<string[]>([]); const [results, setResults] = useState<Array<string>>([]);
const [term, setSearchTerm] = useState(""); const [term, setSearchTerm] = useState("");
const navigate = useNavigate(); const navigate = useNavigate();
const search = useUserSearch(); const search = useUserSearch();
const { follows } = useLogin(); const login = useLogin();
useEffect(() => { useEffect(() => {
setNewChat([]); setNewChat([]);
setSearchTerm(""); setSearchTerm("");
setResults(follows.item); setResults(login.follows.item);
}, [show]); }, [show]);
useEffect(() => { useEffect(() => {
@ -186,7 +184,7 @@ function NewChatWindow() {
if (term) { if (term) {
search(term).then(setResults); search(term).then(setResults);
} else { } else {
setResults(follows.item); setResults(login.follows.item);
} }
}); });
}, [term]); }, [term]);
@ -259,6 +257,19 @@ function NewChatWindow() {
/> />
); );
})} })}
{results.length === 1 && (
<Nip28ChatProfile
id={results[0]}
onClick={id => {
setShow(false);
LoginStore.updateSession({
...login,
extraChats: appendDedupe(login.extraChats, [Nip28ChatSystem.chatId(id)]),
} as LoginSession);
navigate(createChatLink(ChatType.PublicGroupChat, id));
}}
/>
)}
</div> </div>
</div> </div>
</div> </div>
@ -267,3 +278,19 @@ function NewChatWindow() {
</> </>
); );
} }
export function Nip28ChatProfile({ id, onClick }: { id: string; onClick: (id: string) => void }) {
const channel = useEventFeed(new NostrLink(NostrPrefix.Event, id, 40));
if (channel?.data) {
const meta = JSON.parse(channel.data.content) as UserMetadata;
return (
<ProfilePreview
pubkey=""
profile={meta}
options={{ about: false, linkToProfile: false }}
actions={<></>}
onClick={() => onClick(id)}
/>
);
}
}

View File

@ -20,6 +20,8 @@ import { Nip29ChatSystem } from "./nip29";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { Nip24ChatSystem } from "./nip24"; import { Nip24ChatSystem } from "./nip24";
import { LoginSession } from "Login";
import { Nip28ChatSystem } from "./nip28";
export enum ChatType { export enum ChatType {
DirectMessage = 1, DirectMessage = 1,
@ -60,7 +62,7 @@ export interface ChatSystem {
/** /**
* Create a request for this system to get updates * Create a request for this system to get updates
*/ */
subscription(id: string): RequestBuilder | undefined; subscription(session: LoginSession): RequestBuilder | undefined;
onEvent(evs: readonly TaggedNostrEvent[]): Promise<void> | void; onEvent(evs: readonly TaggedNostrEvent[]): Promise<void> | void;
listChats(pk: string): Array<Chat>; listChats(pk: string): Array<Chat>;
@ -69,6 +71,7 @@ export interface ChatSystem {
export const Nip4Chats = new Nip4ChatSystem(Chats); export const Nip4Chats = new Nip4ChatSystem(Chats);
export const Nip29Chats = new Nip29ChatSystem(Chats); export const Nip29Chats = new Nip29ChatSystem(Chats);
export const Nip24Chats = new Nip24ChatSystem(GiftsCache); export const Nip24Chats = new Nip24ChatSystem(GiftsCache);
export const Nip28Chats = new Nip28ChatSystem(Chats);
/** /**
* Extract the P tag of the event * Extract the P tag of the event
@ -143,17 +146,23 @@ export function createChatLink(type: ChatType, ...params: Array<string>) {
), ),
)}`; )}`;
} }
case ChatType.PublicGroupChat: {
return `/messages/${Nip28ChatSystem.chatId(params[0])}`;
}
} }
throw new Error("Unknown chat type"); throw new Error("Unknown chat type");
} }
export function createEmptyChatObject(id: string) { export function createEmptyChatObject(id: string) {
if (id.startsWith("chat4")) { if (id.startsWith("chat41")) {
return Nip4ChatSystem.createChatObj(id, []); return Nip4ChatSystem.createChatObj(id, []);
} }
if (id.startsWith("chat24")) { if (id.startsWith("chat241")) {
return Nip24ChatSystem.createChatObj(id, []); return Nip24ChatSystem.createChatObj(id, []);
} }
if (id.startsWith("chat281")) {
return Nip28ChatSystem.createChatObj(id, []);
}
throw new Error("Cant create new empty chat, unknown id"); throw new Error("Cant create new empty chat, unknown id");
} }
@ -180,10 +189,18 @@ export function useNip24Chat() {
); );
} }
export function useNip28Chat() {
return useSyncExternalStore(
c => Nip28Chats.hook(c),
() => Nip28Chats.snapshot(),
);
}
export function useChatSystem() { export function useChatSystem() {
const nip4 = useNip4Chat(); const nip4 = useNip4Chat();
//const nip24 = useNip24Chat(); //const nip24 = useNip24Chat();
const nip28 = useNip28Chat();
const { muted, blocked } = useModeration(); const { muted, blocked } = useModeration();
return [...nip4].filter(a => !(muted.includes(a.id) || blocked.includes(a.id))); return [...nip4, ...nip28].filter(a => !(muted.includes(a.id) || blocked.includes(a.id)));
} }

View File

@ -0,0 +1,164 @@
import debug from "debug";
import { ExternalStore, FeedCache, unixNow, unwrap } from "@snort/shared";
import {
EventKind,
NostrEvent,
NostrPrefix,
RequestBuilder,
SystemInterface,
TLVEntryType,
TaggedNostrEvent,
UserMetadata,
decodeTLV,
encodeTLVEntries,
} from "@snort/system";
import { LoginSession } from "Login";
import { findTag } from "SnortUtils";
import { Chat, ChatParticipant, ChatSystem, ChatType, lastReadInChat } from "chat";
import { Day } from "Const";
export class Nip28ChatSystem extends ExternalStore<Array<Chat>> implements ChatSystem {
#cache: FeedCache<NostrEvent>;
#log = debug("NIP-04");
readonly ChannelKinds = [
EventKind.PublicChatChannel,
EventKind.PublicChatMessage,
EventKind.PublicChatMetadata,
EventKind.PublicChatMuteMessage,
EventKind.PublicChatMuteUser,
];
constructor(cache: FeedCache<NostrEvent>) {
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 ? unixNow() - 2 * Day : lastMessage)
.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();
return Object.entries(chats).map(([k, v]) => {
return Nip28ChatSystem.createChatObj(Nip28ChatSystem.chatId(k), v);
});
}
static chatId(id: string) {
return encodeTLVEntries("chat28" as NostrPrefix, {
type: TLVEntryType.Special,
value: id,
length: id.length,
});
}
static createChatObj(id: string, messages: Array<NostrEvent>) {
const last = lastReadInChat(id);
const participants = decodeTLV(id)
.filter(v => v.type === TLVEntryType.Special)
.map(
v =>
({
type: "generic",
id: v.value as string,
profile: this.#chatProfileFromMessages(messages),
}) as ChatParticipant,
);
return {
type: ChatType.PublicGroupChat,
id,
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),
participants,
messages: messages
.filter(a => a.kind === EventKind.PublicChatMessage)
.map(m => ({
id: m.id,
created_at: m.created_at,
from: m.pubkey,
tags: m.tags,
content: m.content,
needsDecryption: false,
})),
createMessage: async (msg, pub) => {
return [
await pub.generic(eb => {
return eb.kind(EventKind.PublicChatMessage).content(msg).tag(["e", participants[0].id, "", "root"]);
}),
];
},
sendMessage: (ev, system: SystemInterface) => {
ev.forEach(a => system.BroadcastEvent(a));
},
} as Chat;
}
takeSnapshot(): Chat[] {
return this.listChats();
}
static #chatProfileFromMessages(messages: Array<NostrEvent>) {
const chatDefs = messages.filter(
a => a.kind === EventKind.PublicChatChannel || a.kind === EventKind.PublicChatMetadata,
);
const chatDef =
chatDefs.length > 0
? chatDefs.reduce((acc, v) => (acc.created_at > v.created_at ? acc : v), chatDefs[0])
: undefined;
return chatDef ? (JSON.parse(chatDef.content) as UserMetadata) : undefined;
}
#chatChannels() {
const messages = this.#cache.snapshot();
const chats = messages.reduce(
(acc, v) => {
const k = this.#chatId(v);
if (k) {
acc[k] ??= [];
acc[k].push(v);
}
return acc;
},
{} as Record<string, Array<NostrEvent>>,
);
return chats;
}
#chatId(ev: NostrEvent) {
if (ev.kind === EventKind.PublicChatChannel) {
return ev.id;
} else if (ev.kind === EventKind.PublicChatMetadata) {
return findTag(ev, "e");
} else if (this.ChannelKinds.includes(ev.kind)) {
return ev.tags.find(a => a[0] === "e" && a[3] === "root")?.[1];
}
}
}

View File

@ -1,5 +1,6 @@
import { ExternalStore, FeedCache, dedupe } from "@snort/shared"; import { ExternalStore, FeedCache, dedupe } from "@snort/shared";
import { RequestBuilder, NostrEvent, EventKind, SystemInterface, TaggedNostrEvent } from "@snort/system"; import { RequestBuilder, NostrEvent, EventKind, SystemInterface, TaggedNostrEvent } from "@snort/system";
import { LoginSession } from "Login";
import { unwrap } from "SnortUtils"; import { unwrap } from "SnortUtils";
import { Chat, ChatSystem, ChatType, lastReadInChat } from "chat"; import { Chat, ChatSystem, ChatType, lastReadInChat } from "chat";
@ -15,7 +16,9 @@ export class Nip29ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
return this.listChats(); return this.listChats();
} }
subscription(id: string) { subscription(session: LoginSession) {
const id = session.publicKey;
if (!id) return;
const gs = id.split("/", 2); const gs = id.split("/", 2);
const rb = new RequestBuilder(`nip29:${id}`); const rb = new RequestBuilder(`nip29:${id}`);
const last = this.listChats().find(a => a.id === id)?.lastMessage; const last = this.listChats().find(a => a.id === id)?.lastMessage;

View File

@ -10,6 +10,7 @@ import {
TaggedNostrEvent, TaggedNostrEvent,
decodeTLV, decodeTLV,
} from "@snort/system"; } from "@snort/system";
import { LoginSession } from "Login";
import { Chat, ChatSystem, ChatType, inChatWith, lastReadInChat } from "chat"; import { Chat, ChatSystem, ChatType, inChatWith, lastReadInChat } from "chat";
import { debug } from "debug"; import { debug } from "debug";
@ -30,7 +31,10 @@ export class Nip4ChatSystem extends ExternalStore<Array<Chat>> implements ChatSy
} }
} }
subscription(pk: string) { subscription(session: LoginSession) {
const pk = session.publicKey;
if (!pk) return;
const rb = new RequestBuilder(`nip4:${pk.slice(0, 12)}`); const rb = new RequestBuilder(`nip4:${pk.slice(0, 12)}`);
const dms = this.#cache.snapshot(); const dms = this.#cache.snapshot();
const dmSince = dms.reduce( const dmSince = dms.reduce(

View File

@ -12,6 +12,11 @@ enum EventKind {
SimpleChatMessage = 9, // NIP-29 SimpleChatMessage = 9, // NIP-29
SealedRumor = 13, // NIP-59 SealedRumor = 13, // NIP-59
ChatRumor = 14, // NIP-24 ChatRumor = 14, // NIP-24
PublicChatChannel = 40, // NIP-28
PublicChatMetadata = 41, // NIP-28
PublicChatMessage = 42, // NIP-28
PublicChatMuteMessage = 43, // NIP-28
PublicChatMuteUser = 44, // NIP-28
SnortSubscriptions = 1000, // NIP-XX SnortSubscriptions = 1000, // NIP-XX
Polls = 6969, // NIP-69 Polls = 6969, // NIP-69
GiftWrap = 1059, // NIP-59 GiftWrap = 1059, // NIP-59

View File

@ -10949,12 +10949,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"prettier@npm:^3.0.0": "prettier@npm:^3.0.3":
version: 3.0.2 version: 3.0.3
resolution: "prettier@npm:3.0.2" resolution: "prettier@npm:3.0.3"
bin: bin:
prettier: bin/prettier.cjs prettier: bin/prettier.cjs
checksum: 118b59ddb6c80abe2315ab6d0f4dd1b253be5cfdb20622fa5b65bb1573dcd362e6dd3dcf2711dd3ebfe64aecf7bdc75de8a69dc2422dcd35bdde7610586b677a checksum: e10b9af02b281f6c617362ebd2571b1d7fc9fb8a3bd17e371754428cda992e5e8d8b7a046e8f7d3e2da1dcd21aa001e2e3c797402ebb6111b5cd19609dd228e0
languageName: node languageName: node
linkType: hard linkType: hard
@ -11726,7 +11726,7 @@ __metadata:
"@cloudflare/workers-types": ^4.20230307.0 "@cloudflare/workers-types": ^4.20230307.0
"@tauri-apps/cli": ^1.2.3 "@tauri-apps/cli": ^1.2.3
eslint: ^8.48.0 eslint: ^8.48.0
prettier: ^3.0.0 prettier: ^3.0.3
typescript: ^5.2.2 typescript: ^5.2.2
languageName: unknown languageName: unknown
linkType: soft linkType: soft