fix(chat): fix TypeScript errors and add GroupLink Communikey fallback

- Fix test errors by making tests async and using rejects.toThrow()
- Fix pool.connectedRelays → pool.relays (correct API)
- Export isCommunikey and isValidPubkey helpers for reuse
- Add Communikey detection to GroupLink component
  - When clicking a group from kind 10009 list
  - Checks if group ID is pubkey with kind 10222
  - Automatically uses Communikey protocol
- Fix unused parameter warning in communikey adapter

This enables Communikey fallback both from:
1. Command line: chat relay.com'<pubkey>
2. Group lists: clicking groups stored in kind 10009
This commit is contained in:
Claude
2026-01-20 12:21:07 +00:00
parent 9bf66c0fd2
commit f72991e7c7
4 changed files with 74 additions and 54 deletions

View File

@@ -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 (

View File

@@ -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 <identifier>",
);
});
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");
});

View File

@@ -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<boolean> {
@@ -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");

View File

@@ -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)