diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx
index af29920..df81198 100644
--- a/src/components/ChatViewer.tsx
+++ b/src/components/ChatViewer.tsx
@@ -26,6 +26,7 @@ import type {
import { CHAT_KINDS } from "@/types/chat";
// import { NipC7Adapter } from "@/lib/chat/adapters/nip-c7-adapter"; // Coming soon
import { Nip10Adapter } from "@/lib/chat/adapters/nip-10-adapter";
+import { Nip17Adapter } from "@/lib/chat/adapters/nip-17-adapter";
import { Nip29Adapter } from "@/lib/chat/adapters/nip-29-adapter";
import { Nip53Adapter } from "@/lib/chat/adapters/nip-53-adapter";
import type { ChatProtocolAdapter } from "@/lib/chat/adapters/base-adapter";
@@ -1138,21 +1139,24 @@ export function ChatViewer({
/**
* Get the appropriate adapter for a protocol
- * Currently NIP-10 (thread chat), NIP-29 (relay-based groups) and NIP-53 (live activity chat) are supported
- * Other protocols will be enabled in future phases
+ * Currently supported:
+ * - NIP-10 (thread chat)
+ * - NIP-17 (encrypted DMs with gift wrap)
+ * - NIP-29 (relay-based groups)
+ * - NIP-53 (live activity chat)
*/
function getAdapter(protocol: ChatProtocol): ChatProtocolAdapter {
switch (protocol) {
case "nip-10":
return new Nip10Adapter();
+ case "nip-17":
+ return new Nip17Adapter();
// case "nip-c7": // Phase 1 - Simple chat (coming soon)
// return new NipC7Adapter();
- case "nip-29":
- return new Nip29Adapter();
- // case "nip-17": // Phase 2 - Encrypted DMs (coming soon)
- // return new Nip17Adapter();
// case "nip-28": // Phase 3 - Public channels (coming soon)
// return new Nip28Adapter();
+ case "nip-29":
+ return new Nip29Adapter();
case "nip-53":
return new Nip53Adapter();
default:
diff --git a/src/components/InboxViewer.tsx b/src/components/InboxViewer.tsx
index 91942e4..30e3a43 100644
--- a/src/components/InboxViewer.tsx
+++ b/src/components/InboxViewer.tsx
@@ -173,7 +173,7 @@ export function InboxViewer(_props: InboxViewerProps) {
const handleLoadOlderGiftWraps = async () => {
setIsLoadingOlder(true);
try {
- const count = await giftWrapManager.loadOlderGiftWraps();
+ const count = await giftWrapManager.loadOlderGiftWraps(autoDecrypt);
if (count > 0) {
toast.success(`Loaded ${count} older gift wraps`);
} else {
@@ -542,28 +542,30 @@ function ConversationRow({
day: "numeric",
});
- // Truncate content preview
- const preview = latestMessage.content.slice(0, 50);
- const truncated = latestMessage.content.length > 50;
+ // Truncate content preview - show more text
+ const preview = latestMessage.content.slice(0, 100);
+ const truncated = latestMessage.content.length > 100;
return (
{/* Name */}
- {/* Message preview */}
-
+ {/* Message preview - no flex-1 to avoid big gap */}
+
{preview}
{truncated && "..."}
- {/* Timestamp */}
- {timeStr}
+ {/* Timestamp - push to end with ml-auto */}
+
+ {timeStr}
+
);
}
diff --git a/src/hooks/useAccountSync.ts b/src/hooks/useAccountSync.ts
index 47bda42..83278af 100644
--- a/src/hooks/useAccountSync.ts
+++ b/src/hooks/useAccountSync.ts
@@ -131,6 +131,7 @@ export function useAccountSync() {
// Start gift wrap sync (NIP-17) when account changes
useEffect(() => {
const syncEnabled = grimoire.state.giftWrapSettings?.syncEnabled ?? false;
+ const autoDecrypt = grimoire.state.giftWrapSettings?.autoDecrypt ?? false;
if (!activeAccount?.pubkey || !syncEnabled) {
// Stop sync when no account is active or sync is disabled
@@ -139,7 +140,7 @@ export function useAccountSync() {
}
// Start syncing gift wraps for this account
- giftWrapManager.startSync().catch((error) => {
+ giftWrapManager.startSync(autoDecrypt).catch((error) => {
console.error("[useAccountSync] Failed to start gift wrap sync:", error);
});
@@ -147,5 +148,9 @@ export function useAccountSync() {
return () => {
giftWrapManager.stopSync();
};
- }, [activeAccount?.pubkey, grimoire.state.giftWrapSettings?.syncEnabled]);
+ }, [
+ activeAccount?.pubkey,
+ grimoire.state.giftWrapSettings?.syncEnabled,
+ grimoire.state.giftWrapSettings?.autoDecrypt,
+ ]);
}
diff --git a/src/services/gift-wrap.ts b/src/services/gift-wrap.ts
index 2cccbb7..f8bec68 100644
--- a/src/services/gift-wrap.ts
+++ b/src/services/gift-wrap.ts
@@ -62,14 +62,26 @@ class GiftWrapManager {
private authenticated = new Set(); // Track which relays are authenticated
private isSyncing = false; // Prevent concurrent sync attempts
private statsUpdateTimer: NodeJS.Timeout | null = null; // Debounce stats updates
+ private statsInitialized = false; // Track if stats have been loaded from DB
+
+ /**
+ * Initialize stats from database
+ * Called automatically when stats are first accessed
+ */
+ private async initializeStats(): Promise {
+ if (this.statsInitialized) return;
+ this.statsInitialized = true;
+ await this.updateStats();
+ }
/**
* Start syncing gift wraps for the active account
* 1. Gets DM relays
* 2. Authenticates with dummy REQ (triggers NIP-42 AUTH)
* 3. Subscribes to gift wraps with pagination
+ * @param autoDecrypt - If false, stores gift wraps without decrypting
*/
- async startSync(): Promise {
+ async startSync(autoDecrypt = true): Promise {
// Prevent concurrent sync attempts
if (this.isSyncing) {
console.log("[GiftWrap] Sync already in progress, skipping");
@@ -150,12 +162,14 @@ class GiftWrapManager {
`[GiftWrap] Received gift wrap: ${response.id.slice(0, 8)}...`,
);
// Process gift wrap asynchronously
- this.processGiftWrap(response, pubkey).catch((error) => {
- console.error(
- `[GiftWrap] Error processing ${response.id.slice(0, 8)}:`,
- error,
- );
- });
+ this.processGiftWrap(response, pubkey, autoDecrypt).catch(
+ (error) => {
+ console.error(
+ `[GiftWrap] Error processing ${response.id.slice(0, 8)}:`,
+ error,
+ );
+ },
+ );
}
},
error: (error) => {
@@ -166,7 +180,7 @@ class GiftWrapManager {
this.subscriptions.set(pubkey, subscription);
// Process any existing gift wraps in the event store (from previous sessions)
- await this.processExistingGiftWraps(pubkey);
+ await this.processExistingGiftWraps(pubkey, autoDecrypt);
// Update stats
await this.updateStats();
@@ -261,8 +275,9 @@ class GiftWrapManager {
/**
* Load older gift wraps for pagination
* Fetches gift wraps before the oldest currently loaded
+ * @param autoDecrypt - If false, stores gift wraps without decrypting
*/
- async loadOlderGiftWraps(): Promise {
+ async loadOlderGiftWraps(autoDecrypt = true): Promise {
const account = accountManager.active$.value;
if (!account) {
console.log("[GiftWrap] No active account");
@@ -331,12 +346,14 @@ class GiftWrapManager {
resolve();
} else {
count++;
- this.processGiftWrap(response, pubkey).catch((error) => {
- console.error(
- `[GiftWrap] Error processing ${response.id.slice(0, 8)}:`,
- error,
- );
- });
+ this.processGiftWrap(response, pubkey, autoDecrypt).catch(
+ (error) => {
+ console.error(
+ `[GiftWrap] Error processing ${response.id.slice(0, 8)}:`,
+ error,
+ );
+ },
+ );
}
},
error: () => {
@@ -525,7 +542,10 @@ class GiftWrapManager {
/**
* Process existing gift wraps from event store
*/
- private async processExistingGiftWraps(pubkey: string): Promise {
+ private async processExistingGiftWraps(
+ pubkey: string,
+ autoDecrypt = true,
+ ): Promise {
console.log("[GiftWrap] Processing existing gift wraps...");
// Get all kind 1059 events addressed to us
@@ -539,7 +559,7 @@ class GiftWrapManager {
// Process each gift wrap
for (const giftWrap of giftWraps) {
try {
- await this.processGiftWrap(giftWrap, pubkey);
+ await this.processGiftWrap(giftWrap, pubkey, autoDecrypt);
} catch (error) {
console.error(
`[GiftWrap] Error processing ${giftWrap.id.slice(0, 8)}:`,
@@ -551,10 +571,12 @@ class GiftWrapManager {
/**
* Process a single gift wrap event
+ * @param autoDecrypt - If false, only stores gift wrap without attempting decryption
*/
private async processGiftWrap(
giftWrap: NostrEvent,
recipientPubkey: string,
+ autoDecrypt = true,
): Promise {
const giftWrapId = giftWrap.id;
@@ -569,6 +591,24 @@ class GiftWrapManager {
// Failed too many times, don't retry
return;
}
+ // If autoDecrypt is false and we have a pending record, don't retry
+ if (!autoDecrypt && existing.decryptionState === "pending") {
+ return;
+ }
+ }
+
+ // If autoDecrypt is false, just store as pending without attempting decryption
+ if (!autoDecrypt) {
+ const decryption: GiftWrapDecryption = {
+ giftWrapId,
+ recipientPubkey,
+ decryptionState: "pending",
+ lastAttempt: Math.floor(Date.now() / 1000),
+ attempts: existing?.attempts || 0,
+ };
+ await db.giftWrapDecryptions.put(decryption);
+ this.debouncedUpdateStats();
+ return;
}
// Create or update decryption record
@@ -800,8 +840,11 @@ class GiftWrapManager {
/**
* Get statistics observable
+ * Initializes stats from database on first call
*/
getStats(): Observable {
+ // Initialize stats from database on first access
+ this.initializeStats();
return this.stats$.asObservable();
}