Files
grimoire/src/lib/chat-parser.test.ts
Alejandro 5bc89386ea Add NIP-53 live event chat adapter (#56)
* feat: add NIP-53 live activity chat adapter

Add support for joining live stream chat via naddr (kind 30311):
- Create Nip53Adapter with parseIdentifier, resolveConversation, loadMessages, sendMessage
- Show live activity status badge (LIVE/UPCOMING/ENDED) in chat header
- Display host name and stream metadata from the live activity event
- Support kind 1311 live chat messages with a-tag references
- Use relays from activity's relays tag or naddr relay hints
- Add tests for adapter identifier parsing and chat-parser integration

* ui: derive live chat participants from messages, icon-only status badge

- Derive participants list from unique pubkeys in chat messages for NIP-53
- Move status badge after title with hideLabel for compact icon-only display

* feat: show zaps in NIP-53 live chat with gradient border

- Fetch kind 9735 zaps with #a tag matching the live activity
- Combine zaps and chat messages in the timeline, sorted by timestamp
- Display zap messages with gradient border (yellow → orange → purple → cyan)
- Show zapper, amount, recipient, and optional comment
- Add "zap" message type with zapAmount and zapRecipient metadata

* fix: use RichText for zap comments and remove arrow in chat

- Use RichText with zap request event for zap comments (renders emoji tags)
- Remove the arrow (→) between zapper and recipient in zap messages

* refactor: simplify zap message rendering in chat

- Put timestamp right next to recipient (removed ml-auto)
- Use RichText with content prop and event for emoji resolution
- Inline simple expressions, remove unnecessary variables
- Follow codebase patterns from ZapCompactPreview

* docs: update chat command to include NIP-53 live activity

- Update synopsis to use generic <identifier>
- Add NIP-53 live activity chat to description
- Update option description to cover both protocols
- Add naddr example for live activity chat
- Add 'live' to seeAlso references

* fix: use host outbox relays for NIP-53 live chat events

Combine activity relays, naddr hints, and host's outbox relays when
subscribing to chat messages and zaps. This ensures events are fetched
from all relevant sources where they may be published.

* ui: show host first in members list, all relays in dropdown

- derivedParticipants now puts host first with 'host' role
- Other participants from messages follow as 'member'
- RelaysDropdown shows all NIP-53 liveActivity.relays

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 13:05:09 +01:00

177 lines
5.8 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { nip19 } from "nostr-tools";
import { parseChatCommand } from "./chat-parser";
describe("parseChatCommand", () => {
describe("NIP-29 relay groups", () => {
it("should parse NIP-29 group ID without protocol (single arg)", () => {
const result = parseChatCommand(["groups.0xchat.com'chachi"]);
expect(result.protocol).toBe("nip-29");
expect(result.identifier).toEqual({
type: "group",
value: "chachi",
relays: ["wss://groups.0xchat.com"],
});
expect(result.adapter.protocol).toBe("nip-29");
});
it("should parse NIP-29 group ID when split by shell-quote", () => {
// shell-quote splits on ' so "groups.0xchat.com'chachi" becomes ["groups.0xchat.com", "chachi"]
const result = parseChatCommand(["groups.0xchat.com", "chachi"]);
expect(result.protocol).toBe("nip-29");
expect(result.identifier).toEqual({
type: "group",
value: "chachi",
relays: ["wss://groups.0xchat.com"],
});
expect(result.adapter.protocol).toBe("nip-29");
});
it("should parse NIP-29 group ID with wss:// protocol (single arg)", () => {
const result = parseChatCommand(["wss://groups.0xchat.com'chachi"]);
expect(result.protocol).toBe("nip-29");
expect(result.identifier).toEqual({
type: "group",
value: "chachi",
relays: ["wss://groups.0xchat.com"],
});
});
it("should parse NIP-29 group ID with wss:// when split by shell-quote", () => {
const result = parseChatCommand(["wss://groups.0xchat.com", "chachi"]);
expect(result.protocol).toBe("nip-29");
expect(result.identifier).toEqual({
type: "group",
value: "chachi",
relays: ["wss://groups.0xchat.com"],
});
});
it("should parse NIP-29 group with different relay and group-id (single arg)", () => {
const result = parseChatCommand(["relay.example.com'bitcoin-dev"]);
expect(result.protocol).toBe("nip-29");
expect(result.identifier.value).toBe("bitcoin-dev");
expect(result.identifier.relays).toEqual(["wss://relay.example.com"]);
});
it("should parse NIP-29 group with different relay when split", () => {
const result = parseChatCommand(["relay.example.com", "bitcoin-dev"]);
expect(result.protocol).toBe("nip-29");
expect(result.identifier.value).toBe("bitcoin-dev");
expect(result.identifier.relays).toEqual(["wss://relay.example.com"]);
});
it("should parse NIP-29 group from nos.lol", () => {
const result = parseChatCommand(["nos.lol'welcome"]);
expect(result.protocol).toBe("nip-29");
expect(result.identifier.value).toBe("welcome");
expect(result.identifier.relays).toEqual(["wss://nos.lol"]);
});
});
describe("error handling", () => {
it("should throw error when no identifier provided", () => {
expect(() => parseChatCommand([])).toThrow(
"Chat identifier required. Usage: chat <identifier>",
);
});
it("should throw error for unsupported identifier format", () => {
expect(() => parseChatCommand(["unsupported-format"])).toThrow(
/Unable to determine chat protocol/,
);
});
it("should throw error for npub (NIP-C7 disabled)", () => {
expect(() => parseChatCommand(["npub1xyz"])).toThrow(
/Unable to determine chat protocol/,
);
});
it("should throw error for note/nevent (NIP-28 not implemented)", () => {
expect(() => parseChatCommand(["note1xyz"])).toThrow(
/Unable to determine chat protocol/,
);
});
it("should throw error for malformed naddr", () => {
expect(() => parseChatCommand(["naddr1xyz"])).toThrow(
/Unable to determine chat protocol/,
);
});
});
describe("NIP-53 live activity chat", () => {
it("should parse NIP-53 live activity naddr", () => {
const naddr = nip19.naddrEncode({
kind: 30311,
pubkey:
"0000000000000000000000000000000000000000000000000000000000000001",
identifier: "my-stream",
relays: ["wss://relay.example.com"],
});
const result = parseChatCommand([naddr]);
expect(result.protocol).toBe("nip-53");
expect(result.identifier).toEqual({
type: "live-activity",
value: {
kind: 30311,
pubkey:
"0000000000000000000000000000000000000000000000000000000000000001",
identifier: "my-stream",
},
relays: ["wss://relay.example.com"],
});
expect(result.adapter.protocol).toBe("nip-53");
});
it("should parse NIP-53 live activity naddr with multiple relays", () => {
const naddr = nip19.naddrEncode({
kind: 30311,
pubkey:
"0000000000000000000000000000000000000000000000000000000000000001",
identifier: "podcast-episode-42",
relays: ["wss://relay1.example.com", "wss://relay2.example.com"],
});
const result = parseChatCommand([naddr]);
expect(result.protocol).toBe("nip-53");
expect(result.identifier.value).toEqual({
kind: 30311,
pubkey:
"0000000000000000000000000000000000000000000000000000000000000001",
identifier: "podcast-episode-42",
});
expect(result.identifier.relays).toEqual([
"wss://relay1.example.com",
"wss://relay2.example.com",
]);
});
it("should not parse NIP-29 group naddr as NIP-53", () => {
const naddr = nip19.naddrEncode({
kind: 39000,
pubkey:
"0000000000000000000000000000000000000000000000000000000000000001",
identifier: "test-group",
relays: ["wss://relay.example.com"],
});
// NIP-29 adapter should handle kind 39000
const result = parseChatCommand([naddr]);
expect(result.protocol).toBe("nip-29");
});
});
});