diff --git a/src/components/nostr/GroupLink.tsx b/src/components/nostr/GroupLink.tsx index 35036af..4938a39 100644 --- a/src/components/nostr/GroupLink.tsx +++ b/src/components/nostr/GroupLink.tsx @@ -2,6 +2,7 @@ import { MessageSquare } from "lucide-react"; import { useGrimoire } from "@/core/state"; import { cn } from "@/lib/utils"; import { getTagValue } from "applesauce-core/helpers"; +import { isCommunikey } from "@/lib/chat-parser"; import type { NostrEvent } from "@/types/nostr"; /** @@ -56,16 +57,29 @@ export function GroupLink({ ? getTagValue(metadata, "picture") : undefined; - const handleClick = () => { - // Open chat with properly structured ProtocolIdentifier - addWindow("chat", { - protocol: "nip-29", - identifier: { - type: "group", - value: groupId, - relays: [relayUrl], - }, - }); + const handleClick = async () => { + // Check if this is a Communikey (group ID is pubkey with kind 10222) + if (await isCommunikey(groupId, [relayUrl])) { + console.log(`[GroupLink] Detected Communikey: ${groupId.slice(0, 8)}...`); + addWindow("chat", { + protocol: "communikey", + identifier: { + type: "communikey", + value: groupId, + relays: [relayUrl], + }, + }); + } else { + // Standard NIP-29 group + addWindow("chat", { + protocol: "nip-29", + identifier: { + type: "group", + value: groupId, + relays: [relayUrl], + }, + }); + } }; return ( diff --git a/src/lib/chat-parser.test.ts b/src/lib/chat-parser.test.ts index d0f430c..c1caa7c 100644 --- a/src/lib/chat-parser.test.ts +++ b/src/lib/chat-parser.test.ts @@ -4,8 +4,8 @@ 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"]); + it("should parse NIP-29 group ID without protocol (single arg)", async () => { + const result = await parseChatCommand(["groups.0xchat.com'chachi"]); expect(result.protocol).toBe("nip-29"); expect(result.identifier).toEqual({ @@ -16,9 +16,9 @@ describe("parseChatCommand", () => { expect(result.adapter.protocol).toBe("nip-29"); }); - it("should parse NIP-29 group ID when split by shell-quote", () => { + it("should parse NIP-29 group ID when split by shell-quote", async () => { // shell-quote splits on ' so "groups.0xchat.com'chachi" becomes ["groups.0xchat.com", "chachi"] - const result = parseChatCommand(["groups.0xchat.com", "chachi"]); + const result = await parseChatCommand(["groups.0xchat.com", "chachi"]); expect(result.protocol).toBe("nip-29"); expect(result.identifier).toEqual({ @@ -29,8 +29,8 @@ describe("parseChatCommand", () => { 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"]); + it("should parse NIP-29 group ID with wss:// protocol (single arg)", async () => { + const result = await parseChatCommand(["wss://groups.0xchat.com'chachi"]); expect(result.protocol).toBe("nip-29"); expect(result.identifier).toEqual({ @@ -40,8 +40,11 @@ describe("parseChatCommand", () => { }); }); - it("should parse NIP-29 group ID with wss:// when split by shell-quote", () => { - const result = parseChatCommand(["wss://groups.0xchat.com", "chachi"]); + it("should parse NIP-29 group ID with wss:// when split by shell-quote", async () => { + const result = await parseChatCommand([ + "wss://groups.0xchat.com", + "chachi", + ]); expect(result.protocol).toBe("nip-29"); expect(result.identifier).toEqual({ @@ -51,24 +54,27 @@ describe("parseChatCommand", () => { }); }); - it("should parse NIP-29 group with different relay and group-id (single arg)", () => { - const result = parseChatCommand(["relay.example.com'bitcoin-dev"]); + it("should parse NIP-29 group with different relay and group-id (single arg)", async () => { + const result = await 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"]); + it("should parse NIP-29 group with different relay when split", async () => { + const result = await 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"]); + it("should parse NIP-29 group from nos.lol", async () => { + const result = await parseChatCommand(["nos.lol'welcome"]); expect(result.protocol).toBe("nip-29"); expect(result.identifier.value).toBe("welcome"); @@ -77,39 +83,39 @@ describe("parseChatCommand", () => { }); describe("error handling", () => { - it("should throw error when no identifier provided", () => { - expect(() => parseChatCommand([])).toThrow( + it("should throw error when no identifier provided", async () => { + await expect(parseChatCommand([])).rejects.toThrow( "Chat identifier required. Usage: chat ", ); }); - it("should throw error for unsupported identifier format", () => { - expect(() => parseChatCommand(["unsupported-format"])).toThrow( + it("should throw error for unsupported identifier format", async () => { + await expect(parseChatCommand(["unsupported-format"])).rejects.toThrow( /Unable to determine chat protocol/, ); }); - it("should throw error for npub (NIP-C7 disabled)", () => { - expect(() => parseChatCommand(["npub1xyz"])).toThrow( + it("should throw error for npub (NIP-C7 disabled)", async () => { + await expect(parseChatCommand(["npub1xyz"])).rejects.toThrow( /Unable to determine chat protocol/, ); }); - it("should throw error for note/nevent (NIP-28 not implemented)", () => { - expect(() => parseChatCommand(["note1xyz"])).toThrow( + it("should throw error for note/nevent (NIP-28 not implemented)", async () => { + await expect(parseChatCommand(["note1xyz"])).rejects.toThrow( /Unable to determine chat protocol/, ); }); - it("should throw error for malformed naddr", () => { - expect(() => parseChatCommand(["naddr1xyz"])).toThrow( + it("should throw error for malformed naddr", async () => { + await expect(parseChatCommand(["naddr1xyz"])).rejects.toThrow( /Unable to determine chat protocol/, ); }); }); describe("NIP-53 live activity chat", () => { - it("should parse NIP-53 live activity naddr", () => { + it("should parse NIP-53 live activity naddr", async () => { const naddr = nip19.naddrEncode({ kind: 30311, pubkey: @@ -118,7 +124,7 @@ describe("parseChatCommand", () => { relays: ["wss://relay.example.com"], }); - const result = parseChatCommand([naddr]); + const result = await parseChatCommand([naddr]); expect(result.protocol).toBe("nip-53"); expect(result.identifier).toEqual({ @@ -134,7 +140,7 @@ describe("parseChatCommand", () => { expect(result.adapter.protocol).toBe("nip-53"); }); - it("should parse NIP-53 live activity naddr with multiple relays", () => { + it("should parse NIP-53 live activity naddr with multiple relays", async () => { const naddr = nip19.naddrEncode({ kind: 30311, pubkey: @@ -143,7 +149,7 @@ describe("parseChatCommand", () => { relays: ["wss://relay1.example.com", "wss://relay2.example.com"], }); - const result = parseChatCommand([naddr]); + const result = await parseChatCommand([naddr]); expect(result.protocol).toBe("nip-53"); expect(result.identifier.value).toEqual({ @@ -158,7 +164,7 @@ describe("parseChatCommand", () => { ]); }); - it("should not parse NIP-29 group naddr as NIP-53", () => { + it("should not parse NIP-29 group naddr as NIP-53", async () => { const naddr = nip19.naddrEncode({ kind: 39000, pubkey: @@ -168,7 +174,7 @@ describe("parseChatCommand", () => { }); // NIP-29 adapter should handle kind 39000 - const result = parseChatCommand([naddr]); + const result = await parseChatCommand([naddr]); expect(result.protocol).toBe("nip-29"); }); diff --git a/src/lib/chat-parser.ts b/src/lib/chat-parser.ts index e9e8aa3..8b37ff7 100644 --- a/src/lib/chat-parser.ts +++ b/src/lib/chat-parser.ts @@ -17,15 +17,16 @@ import { toArray } from "rxjs/operators"; /** * Check if a string is a valid hex pubkey (64 hex characters) */ -function isValidPubkey(str: string): boolean { +export function isValidPubkey(str: string): boolean { return /^[0-9a-f]{64}$/i.test(str); } /** * Try to detect if a group ID is actually a Communikey (kind 10222) * Returns true if kind 10222 event found, false otherwise + * Exported for use by GroupLink and other components */ -async function isCommunikey( +export async function isCommunikey( pubkey: string, relayHints: string[], ): Promise { @@ -45,10 +46,10 @@ async function isCommunikey( try { // Use available relays for detection (relay hints + some connected relays) - const relays = [ - ...relayHints, - ...Array.from(pool.connectedRelays.keys()).slice(0, 3), - ].filter((r) => r); + const connectedRelays = Array.from(pool.relays.keys()).slice(0, 3); + const relays = [...relayHints, ...connectedRelays].filter( + (r): r is string => !!r, + ); if (relays.length === 0) { console.log("[Chat Parser] No relays available for Communikey detection"); diff --git a/src/lib/chat/adapters/communikey-adapter.ts b/src/lib/chat/adapters/communikey-adapter.ts index debb8d5..f062990 100644 --- a/src/lib/chat/adapters/communikey-adapter.ts +++ b/src/lib/chat/adapters/communikey-adapter.ts @@ -88,15 +88,14 @@ export class CommunikeyAdapter extends ChatProtocolAdapter { // Use user's outbox/general relays for fetching // TODO: Could use more sophisticated relay selection + const fallbackRelays = + identifier.relays.length > 0 + ? identifier.relays + : Array.from(pool.relays.keys()).slice(0, 5); + const definitionEvents = await firstValueFrom( pool - .request( - identifier.relays.length > 0 - ? identifier.relays - : Array.from(pool.connectedRelays.keys()).slice(0, 5), - [definitionFilter], - { eventStore }, - ) + .request(fallbackRelays, [definitionFilter], { eventStore }) .pipe(toArray()), ); @@ -441,7 +440,7 @@ export class CommunikeyAdapter extends ChatProtocolAdapter { * Get available actions for Communikey groups * Currently only bookmark/unbookmark (no join/leave - open participation) */ - getActions(options?: GetActionsOptions): ChatAction[] { + getActions(_options?: GetActionsOptions): ChatAction[] { const actions: ChatAction[] = []; // Bookmark/unbookmark actions (same as NIP-29)