mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-13 17:07:27 +02:00
feat: client tag (#183)
* feat: show client tag in event header Display "via <client>" after the timestamp in BaseEventContainer when the event has a client tag. Uses compact 10px font with reduced opacity to minimize visual noise. * feat: make client tag link to NIP-89 app definition When the client tag has a third element with a valid 31990 address (NIP-89 app handler), make the client name clickable to open the app definition event. * fix: use parseAddressPointer from nip89-helpers instead of non-existent parseCoordinate * feat: add NIP-89 app address to client tag - Add GRIMOIRE_APP_ADDRESS and GRIMOIRE_CLIENT_TAG constants - Update all client tag usages to include the 31990 app definition address - Update tests to verify the app address is included - Update spell.ts documentation --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { PublishSpellbook } from "./publish-spellbook";
|
||||
import type { ActionContext } from "applesauce-actions";
|
||||
import type { GrimoireState } from "@/types/app";
|
||||
import type { NostrEvent } from "nostr-tools/core";
|
||||
import { GRIMOIRE_APP_ADDRESS } from "@/constants/app";
|
||||
|
||||
// Mock accountManager
|
||||
vi.mock("@/services/accounts", () => ({
|
||||
@@ -192,6 +193,7 @@ describe("PublishSpellbook action", () => {
|
||||
expect(descTag?.[1]).toBe("Test description");
|
||||
expect(clientTag).toBeDefined();
|
||||
expect(clientTag?.[1]).toBe("grimoire");
|
||||
expect(clientTag?.[2]).toBe(GRIMOIRE_APP_ADDRESS);
|
||||
expect(altTag).toBeDefined();
|
||||
expect(altTag?.[1]).toBe("Grimoire Spellbook: Test Spellbook");
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createSpellbook, slugify } from "@/lib/spellbook-manager";
|
||||
import { SpellbookEvent } from "@/types/spell";
|
||||
import { GrimoireState } from "@/types/app";
|
||||
import { SpellbookContent } from "@/types/spell";
|
||||
import { GRIMOIRE_CLIENT_TAG } from "@/constants/app";
|
||||
import accountManager from "@/services/accounts";
|
||||
import type { ActionContext } from "applesauce-actions";
|
||||
|
||||
@@ -74,7 +75,7 @@ export function PublishSpellbook(options: PublishSpellbookOptions) {
|
||||
tags: [
|
||||
["d", slugify(title)],
|
||||
["title", title],
|
||||
["client", "grimoire"],
|
||||
GRIMOIRE_CLIENT_TAG,
|
||||
] as [string, string, ...string[]][],
|
||||
};
|
||||
if (description) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import { JsonViewer } from "@/components/JsonViewer";
|
||||
import { formatTimestamp } from "@/hooks/useLocale";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { getTagValue } from "applesauce-core/helpers";
|
||||
import { parseAddressPointer } from "@/lib/nip89-helpers";
|
||||
import { getSeenRelays } from "applesauce-core/helpers/relays";
|
||||
import { EventFooter } from "@/components/EventFooter";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -496,7 +497,7 @@ export function BaseEventContainer({
|
||||
label?: string;
|
||||
};
|
||||
}) {
|
||||
const { locale } = useGrimoire();
|
||||
const { locale, addWindow } = useGrimoire();
|
||||
|
||||
// Format relative time for display
|
||||
const relativeTime = formatTimestamp(
|
||||
@@ -515,6 +516,23 @@ export function BaseEventContainer({
|
||||
// Use author override if provided, otherwise use event author
|
||||
const displayPubkey = authorOverride?.pubkey || event.pubkey;
|
||||
|
||||
// Get client tag if present: ["client", "<name>", "<31990:pubkey:d-tag>"]
|
||||
const clientTag = event.tags.find((t) => t[0] === "client");
|
||||
const clientName = clientTag?.[1];
|
||||
const clientAddress = clientTag?.[2];
|
||||
const parsedClientAddress = clientAddress
|
||||
? parseAddressPointer(clientAddress)
|
||||
: null;
|
||||
const clientAppPointer =
|
||||
parsedClientAddress?.kind === 31990 ? parsedClientAddress : null;
|
||||
|
||||
const handleClientClick = (e: React.MouseEvent) => {
|
||||
if (clientAppPointer) {
|
||||
e.stopPropagation();
|
||||
addWindow("open", { pointer: clientAppPointer });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EventContextMenu event={event}>
|
||||
<div className="flex flex-col gap-2 p-3 border-b border-border/50 last:border-0">
|
||||
@@ -527,6 +545,21 @@ export function BaseEventContainer({
|
||||
>
|
||||
{relativeTime}
|
||||
</span>
|
||||
{clientName && (
|
||||
<span className="text-[10px] text-muted-foreground/70">
|
||||
via{" "}
|
||||
{clientAppPointer ? (
|
||||
<button
|
||||
onClick={handleClientClick}
|
||||
className="hover:underline hover:text-foreground cursor-crosshair"
|
||||
>
|
||||
{clientName}
|
||||
</button>
|
||||
) : (
|
||||
clientName
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<EventMenu event={event} />
|
||||
</div>
|
||||
|
||||
20
src/constants/app.ts
Normal file
20
src/constants/app.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Grimoire app constants
|
||||
*/
|
||||
|
||||
/**
|
||||
* Grimoire NIP-89 app definition address (kind 31990)
|
||||
* Format: "kind:pubkey:identifier"
|
||||
*/
|
||||
export const GRIMOIRE_APP_ADDRESS =
|
||||
"31990:7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194:k50nvf8d85";
|
||||
|
||||
/**
|
||||
* Client tag for events published by Grimoire
|
||||
* Format: ["client", "<name>", "<31990:pubkey:d-tag>"]
|
||||
*/
|
||||
export const GRIMOIRE_CLIENT_TAG: [string, string, string] = [
|
||||
"client",
|
||||
"grimoire",
|
||||
GRIMOIRE_APP_ADDRESS,
|
||||
];
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { encodeSpell, decodeSpell } from "./spell-conversion";
|
||||
import type { SpellEvent } from "@/types/spell";
|
||||
import { GRIMOIRE_CLIENT_TAG } from "@/constants/app";
|
||||
|
||||
describe("Spell Conversion", () => {
|
||||
describe("encodeSpell", () => {
|
||||
@@ -11,7 +12,7 @@ describe("Spell Conversion", () => {
|
||||
});
|
||||
|
||||
expect(result.tags).toContainEqual(["cmd", "REQ"]);
|
||||
expect(result.tags).toContainEqual(["client", "grimoire"]);
|
||||
expect(result.tags).toContainEqual(GRIMOIRE_CLIENT_TAG);
|
||||
expect(result.tags).toContainEqual(["k", "1"]);
|
||||
expect(result.tags).toContainEqual(["k", "3"]);
|
||||
expect(result.tags).toContainEqual(["k", "7"]);
|
||||
@@ -253,12 +254,7 @@ describe("Spell Conversion", () => {
|
||||
pubkey: "test-pubkey",
|
||||
created_at: 1234567890,
|
||||
kind: 777,
|
||||
tags: [
|
||||
["cmd", "REQ"],
|
||||
["client", "grimoire"],
|
||||
["k", "1"],
|
||||
["k", "3"],
|
||||
],
|
||||
tags: [["cmd", "REQ"], GRIMOIRE_CLIENT_TAG, ["k", "1"], ["k", "3"]],
|
||||
content: "Test spell",
|
||||
sig: "test-sig",
|
||||
};
|
||||
@@ -360,7 +356,7 @@ describe("Spell Conversion", () => {
|
||||
kind: 777,
|
||||
tags: [
|
||||
["cmd", "REQ"],
|
||||
["client", "grimoire"],
|
||||
GRIMOIRE_CLIENT_TAG,
|
||||
["k", "1"],
|
||||
["authors", "abc123", "def456"],
|
||||
],
|
||||
@@ -382,7 +378,7 @@ describe("Spell Conversion", () => {
|
||||
kind: 777,
|
||||
tags: [
|
||||
["cmd", "REQ"],
|
||||
["client", "grimoire"],
|
||||
GRIMOIRE_CLIENT_TAG,
|
||||
["k", "1"],
|
||||
["tag", "t", "bitcoin", "nostr"],
|
||||
["tag", "p", "abc123"],
|
||||
@@ -410,7 +406,7 @@ describe("Spell Conversion", () => {
|
||||
kind: 777,
|
||||
tags: [
|
||||
["cmd", "REQ"],
|
||||
["client", "grimoire"],
|
||||
GRIMOIRE_CLIENT_TAG,
|
||||
["k", "1"],
|
||||
["since", "7d"],
|
||||
["until", "now"],
|
||||
@@ -433,7 +429,7 @@ describe("Spell Conversion", () => {
|
||||
kind: 777,
|
||||
tags: [
|
||||
["cmd", "REQ"],
|
||||
["client", "grimoire"],
|
||||
GRIMOIRE_CLIENT_TAG,
|
||||
["k", "1"],
|
||||
["t", "bitcoin"],
|
||||
["t", "news"],
|
||||
@@ -456,7 +452,7 @@ describe("Spell Conversion", () => {
|
||||
kind: 777,
|
||||
tags: [
|
||||
["cmd", "REQ"],
|
||||
["client", "grimoire"],
|
||||
GRIMOIRE_CLIENT_TAG,
|
||||
["k", "1"],
|
||||
["e", "abc123def456"],
|
||||
],
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
SpellEvent,
|
||||
} from "@/types/spell";
|
||||
import type { NostrFilter } from "@/types/nostr";
|
||||
import { GRIMOIRE_CLIENT_TAG } from "@/constants/app";
|
||||
|
||||
/**
|
||||
* Simple tokenization that doesn't expand shell variables
|
||||
@@ -116,7 +117,7 @@ export function encodeSpell(options: CreateSpellOptions): EncodedSpell {
|
||||
// Start with required tags
|
||||
const tags: [string, string, ...string[]][] = [
|
||||
["cmd", cmdType],
|
||||
["client", "grimoire"],
|
||||
GRIMOIRE_CLIENT_TAG,
|
||||
];
|
||||
|
||||
// Add name tag if provided
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "./spellbook-manager";
|
||||
import { GrimoireState, WindowInstance, Workspace } from "@/types/app";
|
||||
import { SPELLBOOK_KIND, SpellbookEvent } from "@/types/spell";
|
||||
import { GRIMOIRE_CLIENT_TAG } from "@/constants/app";
|
||||
|
||||
// Mock Data
|
||||
const mockWindow1: WindowInstance = {
|
||||
@@ -138,7 +139,7 @@ describe("Spellbook Manager", () => {
|
||||
"description",
|
||||
"Test description",
|
||||
]);
|
||||
expect(eventProps.tags).toContainEqual(["client", "grimoire"]);
|
||||
expect(eventProps.tags).toContainEqual(GRIMOIRE_CLIENT_TAG);
|
||||
|
||||
// Check referenced spells (e tags)
|
||||
expect(referencedSpells).toContain("spell-1");
|
||||
|
||||
@@ -2,6 +2,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import type { MosaicNode } from "react-mosaic-component";
|
||||
import type { GrimoireState, WindowInstance, Workspace } from "@/types/app";
|
||||
import { SPELLBOOK_KIND } from "@/constants/kinds";
|
||||
import { GRIMOIRE_CLIENT_TAG } from "@/constants/app";
|
||||
import {
|
||||
type SpellbookContent,
|
||||
type SpellbookEvent,
|
||||
@@ -127,7 +128,7 @@ export function createSpellbook(
|
||||
const tags: [string, string, ...string[]][] = [
|
||||
["d", slugify(title)],
|
||||
["title", title],
|
||||
["client", "grimoire"],
|
||||
GRIMOIRE_CLIENT_TAG,
|
||||
];
|
||||
|
||||
if (description) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export { SPELL_KIND, SPELLBOOK_KIND };
|
||||
* - ["cmd", "REQ"] - Command type
|
||||
*
|
||||
* METADATA:
|
||||
* - ["client", "grimoire"] - Client identifier
|
||||
* - ["client", "grimoire", "<31990:pubkey:d-tag>"] - Client identifier with NIP-89 app address
|
||||
* - ["alt", "description"] - NIP-31 human-readable description
|
||||
* - ["name", "My Spell"] - Optional spell name (metadata only, not unique identifier)
|
||||
* - ["t", "bitcoin"], ["t", "news"] - Topic tags for categorization
|
||||
|
||||
Reference in New Issue
Block a user