mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-16 17:48:34 +02:00
fix: NIP-17 chat fixes and inbox UX improvements
Enable NIP-17 Support: - Uncomment Nip17Adapter in ChatViewer getAdapter function - Import Nip17Adapter and register in protocol switch - Update adapter documentation to list NIP-17 as supported Fix Auto-Decrypt Behavior: - Add autoDecrypt parameter to processGiftWrap (default true) - Only decrypt gift wraps when autoDecrypt=true, otherwise store as pending - Pass autoDecrypt from state to startSync and loadOlderGiftWraps - Update useAccountSync to pass autoDecrypt setting - Update InboxViewer to pass autoDecrypt to loadOlderGiftWraps - Prevents unwanted decryption attempts when setting is off Fix Stats Persistence: - Add initializeStats() method to load stats from DB on first access - Add statsInitialized flag to prevent duplicate initialization - Call initializeStats() in getStats() before returning observable - Stats now persist across page reloads even when sync is disabled Inbox UX Improvements: - Increase message preview from 50 to 100 characters - Remove flex-1 from preview span to eliminate large gap before timestamp - Add ml-auto to timestamp to push it to the right - Change gap from gap-1 to gap-2 for better spacing - Messages now show more content with timestamp properly aligned All tests passing (980/980). No new lint errors.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="flex cursor-pointer items-center gap-1 border-b px-3 py-1.5 hover:bg-muted/30 last:border-b-0 font-mono text-xs"
|
||||
className="flex cursor-pointer items-center gap-2 border-b px-3 py-1.5 hover:bg-muted/30 last:border-b-0 font-mono text-xs"
|
||||
>
|
||||
{/* Name */}
|
||||
<div className="w-28 shrink-0 truncate">
|
||||
<UserName pubkey={otherPubkey} className="text-xs font-medium" />
|
||||
</div>
|
||||
|
||||
{/* Message preview */}
|
||||
<span className="min-w-0 flex-1 truncate text-muted-foreground/70">
|
||||
{/* Message preview - no flex-1 to avoid big gap */}
|
||||
<span className="truncate text-muted-foreground/70">
|
||||
{preview}
|
||||
{truncated && "..."}
|
||||
</span>
|
||||
|
||||
{/* Timestamp */}
|
||||
<span className="shrink-0 text-muted-foreground/50">{timeStr}</span>
|
||||
{/* Timestamp - push to end with ml-auto */}
|
||||
<span className="shrink-0 ml-auto text-muted-foreground/50">
|
||||
{timeStr}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -62,14 +62,26 @@ class GiftWrapManager {
|
||||
private authenticated = new Set<string>(); // 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<void> {
|
||||
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<void> {
|
||||
async startSync(autoDecrypt = true): Promise<void> {
|
||||
// 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<number> {
|
||||
async loadOlderGiftWraps(autoDecrypt = true): Promise<number> {
|
||||
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<void> {
|
||||
private async processExistingGiftWraps(
|
||||
pubkey: string,
|
||||
autoDecrypt = true,
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
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<GiftWrapStats> {
|
||||
// Initialize stats from database on first access
|
||||
this.initializeStats();
|
||||
return this.stats$.asObservable();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user