Add copy chat ID button to header (#103)

* feat: add copy chat ID button to chat header

Add a button next to the chat title that copies the chat identifier
to clipboard. The identifier can be used with the `chat` command to
reopen the same conversation.

- For NIP-29 groups: copies relay'group-id format
- For NIP-53 live activities: copies naddr encoding

The button shows a check icon for feedback when copied.

* refactor: simplify copy chat ID button styling

- Use CopyCheck icon instead of Check for consistency with CodeCopyButton
- Remove tooltip to reduce UI noise
- Keep hover state styling (muted to foreground)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-15 11:43:01 +01:00
committed by GitHub
parent 72eca99c2e
commit 1ce784561a

View File

@@ -9,7 +9,10 @@ import {
AlertTriangle,
RefreshCw,
Paperclip,
Copy,
CopyCheck,
} from "lucide-react";
import { nip19 } from "nostr-tools";
import { getZapRequest } from "applesauce-common/helpers/zap";
import { toast } from "sonner";
import accountManager from "@/services/accounts";
@@ -45,6 +48,7 @@ import {
} from "./editor/MentionEditor";
import { useProfileSearch } from "@/hooks/useProfileSearch";
import { useEmojiSearch } from "@/hooks/useEmojiSearch";
import { useCopy } from "@/hooks/useCopy";
import { Label } from "./ui/label";
import {
Tooltip,
@@ -129,6 +133,43 @@ function isLiveActivityMetadata(value: unknown): value is LiveActivityMetadata {
);
}
/**
* Get the chat command identifier for a conversation
* Returns a string that can be passed to the `chat` command to open this conversation
*
* For NIP-29 groups: relay'group-id (without wss:// prefix)
* For NIP-53 live activities: naddr1... encoding
*/
function getChatIdentifier(conversation: Conversation): string | null {
if (conversation.protocol === "nip-29") {
const groupId = conversation.metadata?.groupId;
const relayUrl = conversation.metadata?.relayUrl;
if (!groupId || !relayUrl) return null;
// Strip wss:// or ws:// prefix for cleaner identifier
const cleanRelay = relayUrl.replace(/^wss?:\/\//, "");
return `${cleanRelay}'${groupId}`;
}
if (conversation.protocol === "nip-53") {
const activityAddress = conversation.metadata?.activityAddress;
if (!activityAddress) return null;
// Get relay hints from live activity metadata
const liveActivity = conversation.metadata?.liveActivity;
const relays = liveActivity?.relays || [];
return nip19.naddrEncode({
kind: activityAddress.kind,
pubkey: activityAddress.pubkey,
identifier: activityAddress.identifier,
relays: relays.slice(0, 3), // Limit relay hints to keep naddr short
});
}
return null;
}
/**
* Conversation resolution result - either success with conversation or error
*/
@@ -360,6 +401,9 @@ export function ChatViewer({
// Emoji search for custom emoji autocomplete
const { searchEmojis } = useEmojiSearch();
// Copy chat identifier to clipboard
const { copy: copyChatId, copied: chatIdCopied } = useCopy();
// Ref to MentionEditor for programmatic submission
const editorRef = useRef<MentionEditorHandle>(null);
@@ -811,6 +855,23 @@ export function ChatViewer({
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/* Copy Chat ID button */}
{getChatIdentifier(conversation) && (
<button
onClick={() => {
const chatId = getChatIdentifier(conversation);
if (chatId) copyChatId(chatId);
}}
className="text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
aria-label="Copy chat ID"
>
{chatIdCopied ? (
<CopyCheck className="size-3.5" />
) : (
<Copy className="size-3.5" />
)}
</button>
)}
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground p-1">
<MembersDropdown participants={derivedParticipants} />