mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-16 17:48:34 +02:00
fix: Complete NIP-59 gift wrap implementation with API fixes
This commit resolves all build errors in the NIP-59 gift wrap feature: - Created Switch UI component using simple toggle button pattern - Fixed InboxViewer to use <label> elements instead of custom Label component - Corrected createTimelineLoader API usage (4 args + call loader() to get observable) - Fixed ISigner nip44 decryption API (use signer.nip44.decrypt instead of signer.nip44Decrypt) - Updated mock signer in tests to match ISigner interface - Added event handling via eventStore timeline subscription All tests passing, build successful.
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
|
||||
import { Button } from "./ui/button";
|
||||
import { Label } from "./ui/label";
|
||||
import { Switch } from "./ui/switch";
|
||||
import giftWrapLoader from "@/services/gift-wrap-loader";
|
||||
import { toast } from "sonner";
|
||||
@@ -172,9 +171,12 @@ export function InboxViewer() {
|
||||
{/* Enable Private Messages */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label htmlFor="enable-private-messages">
|
||||
<label
|
||||
htmlFor="enable-private-messages"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
||||
>
|
||||
Enable Private Messages
|
||||
</Label>
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Fetch and store encrypted gift wraps from DM relays
|
||||
</p>
|
||||
@@ -190,7 +192,12 @@ export function InboxViewer() {
|
||||
{privateMessagesEnabled && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label htmlFor="auto-decrypt">Auto-Decrypt Messages</Label>
|
||||
<label
|
||||
htmlFor="auto-decrypt"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
||||
>
|
||||
Auto-Decrypt Messages
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Automatically decrypt gift wraps as they arrive
|
||||
</p>
|
||||
|
||||
47
src/components/ui/switch.tsx
Normal file
47
src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface SwitchProps {
|
||||
id?: string;
|
||||
checked?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple toggle switch component
|
||||
* Styled as a sliding toggle for boolean states
|
||||
*/
|
||||
export const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
|
||||
(
|
||||
{ id, checked = false, onCheckedChange, disabled = false, className },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<button
|
||||
id={id}
|
||||
ref={ref}
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
disabled={disabled}
|
||||
onClick={() => !disabled && onCheckedChange?.(!checked)}
|
||||
className={cn(
|
||||
"relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
|
||||
checked ? "bg-primary" : "bg-input",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"pointer-events-none inline-block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition duration-200 ease-in-out",
|
||||
checked ? "translate-x-5" : "translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Switch.displayName = "Switch";
|
||||
@@ -163,17 +163,12 @@ class GiftWrapLoader {
|
||||
};
|
||||
|
||||
// Create timeline loader for gift wraps
|
||||
const timeline = createTimelineLoader(pool, {
|
||||
const loader = createTimelineLoader(pool, inboxRelays, [filter], {
|
||||
eventStore,
|
||||
relays: inboxRelays,
|
||||
filters: [filter],
|
||||
});
|
||||
|
||||
// Subscribe to timeline
|
||||
this.subscription = timeline.subscribe({
|
||||
next: (event: NostrEvent) => {
|
||||
void this.handleGiftWrap(event);
|
||||
},
|
||||
this.subscription = loader().subscribe({
|
||||
error: (error: Error) => {
|
||||
console.error("[GiftWrapLoader] Timeline error:", error);
|
||||
this.state$.next({
|
||||
@@ -192,6 +187,23 @@ class GiftWrapLoader {
|
||||
},
|
||||
});
|
||||
|
||||
// Handle events from timeline via eventStore subscription
|
||||
// Timeline loader automatically adds events to eventStore
|
||||
const eventSub = eventStore.timeline([filter]).subscribe((events) => {
|
||||
events.forEach((event) => {
|
||||
void this.handleGiftWrap(event);
|
||||
});
|
||||
});
|
||||
|
||||
// Store both subscriptions for cleanup
|
||||
const originalUnsub = this.subscription.unsubscribe.bind(
|
||||
this.subscription,
|
||||
);
|
||||
this.subscription.unsubscribe = () => {
|
||||
originalUnsub();
|
||||
eventSub.unsubscribe();
|
||||
};
|
||||
|
||||
// Process any pending gift wraps from database
|
||||
await this.processPendingGiftWraps();
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,19 +8,20 @@ import type { NostrEvent } from "@/types/nostr";
|
||||
import type { ISigner } from "applesauce-signers";
|
||||
|
||||
// Mock signer for testing
|
||||
function createMockSigner(decryptResponses: Map<string, string>): ISigner & {
|
||||
nip44Decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
|
||||
} {
|
||||
function createMockSigner(decryptResponses: Map<string, string>): ISigner {
|
||||
return {
|
||||
getPublicKey: vi.fn().mockResolvedValue("mock-pubkey"),
|
||||
signEvent: vi.fn(),
|
||||
nip44Decrypt: vi.fn(async (pubkey: string, ciphertext: string) => {
|
||||
const response = decryptResponses.get(`${pubkey}:${ciphertext}`);
|
||||
if (!response) {
|
||||
throw new Error("Mock decryption failed: no response configured");
|
||||
}
|
||||
return response;
|
||||
}),
|
||||
nip44: {
|
||||
encrypt: vi.fn(),
|
||||
decrypt: vi.fn(async (pubkey: string, ciphertext: string) => {
|
||||
const response = decryptResponses.get(`${pubkey}:${ciphertext}`);
|
||||
if (!response) {
|
||||
throw new Error("Mock decryption failed: no response configured");
|
||||
}
|
||||
return response;
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ async function unwrapGiftWrap(
|
||||
): Promise<NostrEvent> {
|
||||
validateGiftWrap(giftWrap);
|
||||
|
||||
if (!signer.nip44Decrypt) {
|
||||
if (!signer.nip44?.decrypt) {
|
||||
throw new GiftWrapError(
|
||||
"Signer does not support NIP-44 decryption",
|
||||
"NO_SIGNER",
|
||||
@@ -130,7 +130,7 @@ async function unwrapGiftWrap(
|
||||
|
||||
try {
|
||||
// Decrypt using the gift wrap author's pubkey (ephemeral key)
|
||||
const decryptedContent = await signer.nip44Decrypt(
|
||||
const decryptedContent = await signer.nip44.decrypt(
|
||||
giftWrap.pubkey,
|
||||
giftWrap.content,
|
||||
);
|
||||
@@ -165,7 +165,7 @@ async function unsealSeal(
|
||||
): Promise<NostrEvent> {
|
||||
validateSeal(seal);
|
||||
|
||||
if (!signer.nip44Decrypt) {
|
||||
if (!signer.nip44?.decrypt) {
|
||||
throw new GiftWrapError(
|
||||
"Signer does not support NIP-44 decryption",
|
||||
"NO_SIGNER",
|
||||
@@ -174,7 +174,7 @@ async function unsealSeal(
|
||||
|
||||
try {
|
||||
// Decrypt using the seal author's pubkey (sender's real key)
|
||||
const decryptedContent = await signer.nip44Decrypt(
|
||||
const decryptedContent = await signer.nip44.decrypt(
|
||||
seal.pubkey,
|
||||
seal.content,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user