diff --git a/src/components/InboxViewer.tsx b/src/components/InboxViewer.tsx
index cdbc77c..403a0f7 100644
--- a/src/components/InboxViewer.tsx
+++ b/src/components/InboxViewer.tsx
@@ -111,14 +111,20 @@ export function InboxViewer(_props: InboxViewerProps) {
};
const allConversations = Array.from(conversations.entries())
- .map(([key, latestMessage]) => ({
- key,
- latestMessage,
- otherPubkey:
- latestMessage.senderPubkey === pubkey
- ? latestMessage.recipientPubkey
- : latestMessage.senderPubkey,
- }))
+ .map(([key, latestMessage]) => {
+ // Parse conversation key to get all participants
+ // Key format: "pubkey1:pubkey2:..." (sorted)
+ const participants = key.split(":");
+
+ // Filter out current user to get other participants
+ const otherPubkeys = participants.filter((p) => p !== pubkey);
+
+ return {
+ key,
+ latestMessage,
+ otherPubkeys,
+ };
+ })
.sort((a, b) => b.latestMessage.createdAt - a.latestMessage.createdAt);
const pageSize = CONVERSATIONS_PAGE_SIZE * conversationsPage;
@@ -147,14 +153,18 @@ export function InboxViewer(_props: InboxViewerProps) {
const handleOpenConversation = (
_conversationKey: string,
- otherPubkey: string,
+ otherPubkeys: string[],
) => {
- // Open chat window with the other participant using NIP-17
+ // Open chat window with the other participant(s) using NIP-17
+ // For group DMs, join pubkeys with commas
+ const recipientValue =
+ otherPubkeys.length === 1 ? otherPubkeys[0] : otherPubkeys.join(",");
+
addWindow("chat", {
protocol: "nip-17",
identifier: {
type: "dm-recipient",
- value: otherPubkey,
+ value: recipientValue,
},
});
};
@@ -261,17 +271,6 @@ export function InboxViewer(_props: InboxViewerProps) {
{/* Center: Stats */}
{/* Stats - Compact numbers only */}
-
-
-
- {stats.totalGiftWraps}
-
-
-
- Total gift wraps
-
-
-
@@ -491,13 +490,13 @@ export function InboxViewer(_props: InboxViewerProps) {
) : (
<>
- {conversationsList.map(({ key, latestMessage, otherPubkey }) => (
+ {conversationsList.map(({ key, latestMessage, otherPubkeys }) => (
handleOpenConversation(key, otherPubkey)}
+ onClick={() => handleOpenConversation(key, otherPubkeys)}
/>
))}
@@ -523,13 +522,13 @@ export function InboxViewer(_props: InboxViewerProps) {
interface ConversationRowProps {
conversationKey: string;
- otherPubkey: string;
+ otherPubkeys: string[];
latestMessage: any;
onClick: () => void;
}
function ConversationRow({
- otherPubkey,
+ otherPubkeys,
latestMessage,
onClick,
}: ConversationRowProps) {
@@ -538,12 +537,19 @@ function ConversationRow({
onClick={onClick}
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 - no fixed width */}
-
-
+ {/* Name(s) - no fixed width */}
+
+ {otherPubkeys.map((pubkey, index) => (
+
+
+ {index < otherPubkeys.length - 1 && (
+ ,
+ )}
+
+ ))}
{/* Message preview - use CSS truncation and RichText with pointer-events-none */}
diff --git a/src/services/gift-wrap.ts b/src/services/gift-wrap.ts
index f1dc1e1..629e0ba 100644
--- a/src/services/gift-wrap.ts
+++ b/src/services/gift-wrap.ts
@@ -139,27 +139,69 @@ class GiftWrapManager {
);
}
- // Step 3: Subscribe to gift wraps with pagination
- const filter: Filter = {
+ // Step 3: Subscribe to historical gift wraps (if initial sync)
+ if (isInitialSync) {
+ const historicalFilter: Filter = {
+ kinds: [1059],
+ "#p": [pubkey],
+ since,
+ until: now,
+ limit: GIFT_WRAP_CONFIG.INITIAL_LIMIT,
+ };
+
+ await new Promise
((resolve) => {
+ const historicalSub = pool
+ .subscription(dmRelays, [historicalFilter], {
+ eventStore,
+ })
+ .subscribe({
+ next: (response) => {
+ if (typeof response === "string") {
+ console.log("[GiftWrap] Historical EOSE received");
+ historicalSub.unsubscribe();
+ resolve();
+ } else {
+ console.log(
+ `[GiftWrap] Historical gift wrap: ${response.id.slice(0, 8)}...`,
+ );
+ this.processGiftWrap(response, pubkey, autoDecrypt).catch(
+ (error) => {
+ console.error(
+ `[GiftWrap] Error processing ${response.id.slice(0, 8)}:`,
+ error,
+ );
+ },
+ );
+ }
+ },
+ error: () => {
+ historicalSub.unsubscribe();
+ resolve();
+ },
+ });
+ });
+ }
+
+ // Step 4: Create live subscription for new gift wraps
+ // This subscription stays open and listens for new events
+ const liveFilter: Filter = {
kinds: [1059],
"#p": [pubkey],
- since,
- limit: isInitialSync ? GIFT_WRAP_CONFIG.INITIAL_LIMIT : undefined,
+ since: now, // Only new events from now onwards
};
- const subscription = pool
- .subscription(dmRelays, [filter], {
- eventStore, // Automatically add to event store
+ const liveSubscription = pool
+ .subscription(dmRelays, [liveFilter], {
+ eventStore,
})
.subscribe({
next: (response) => {
if (typeof response === "string") {
- console.log("[GiftWrap] EOSE received");
- // Update last sync timestamp after EOSE
+ console.log("[GiftWrap] Live subscription EOSE received");
this.lastSyncTimestamp = now;
} else {
console.log(
- `[GiftWrap] Received gift wrap: ${response.id.slice(0, 8)}...`,
+ `[GiftWrap] New gift wrap: ${response.id.slice(0, 8)}...`,
);
// Process gift wrap asynchronously
this.processGiftWrap(response, pubkey, autoDecrypt).catch(
@@ -173,11 +215,11 @@ class GiftWrapManager {
}
},
error: (error) => {
- console.error("[GiftWrap] Subscription error:", error);
+ console.error("[GiftWrap] Live subscription error:", error);
},
});
- this.subscriptions.set(pubkey, subscription);
+ this.subscriptions.set(pubkey, liveSubscription);
// Process any existing gift wraps in the event store (from previous sessions)
await this.processExistingGiftWraps(pubkey, autoDecrypt);
@@ -350,6 +392,7 @@ class GiftWrapManager {
};
let count = 0;
+ const processingPromises: Promise[] = [];
await new Promise((resolve) => {
const subscription = pool
@@ -364,14 +407,18 @@ class GiftWrapManager {
resolve();
} else {
count++;
- this.processGiftWrap(response, pubkey, autoDecrypt).catch(
- (error) => {
- console.error(
- `[GiftWrap] Error processing ${response.id.slice(0, 8)}:`,
- error,
- );
- },
- );
+ // Collect all processing promises to await them later
+ const promise = this.processGiftWrap(
+ response,
+ pubkey,
+ autoDecrypt,
+ ).catch((error) => {
+ console.error(
+ `[GiftWrap] Error processing ${response.id.slice(0, 8)}:`,
+ error,
+ );
+ });
+ processingPromises.push(promise);
}
},
error: () => {
@@ -381,6 +428,9 @@ class GiftWrapManager {
});
});
+ // Wait for all gift wraps to be processed before updating stats
+ await Promise.all(processingPromises);
+
// Update stats
await this.updateStats();