mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-16 17:48:34 +02:00
feat: add live gift wrap sync and group DM support
- Add dedicated live subscription for new gift wraps (stays open after EOSE) - Separate historical sync from live subscription for better reliability - Remove total gift wrap count from stats display (shown in tooltip) - Fix load older to await gift wrap processing before updating stats - Add group DM support in inbox conversation list - Parse conversation keys to extract all participants - Display multiple participant names with comma separation - Support opening group DM chats with comma-separated pubkeys
This commit is contained in:
@@ -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 */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Stats - Compact numbers only */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-muted-foreground/80">
|
||||
{stats.totalGiftWraps}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Total gift wraps</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-green-600/70">
|
||||
@@ -491,13 +490,13 @@ export function InboxViewer(_props: InboxViewerProps) {
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
{conversationsList.map(({ key, latestMessage, otherPubkey }) => (
|
||||
{conversationsList.map(({ key, latestMessage, otherPubkeys }) => (
|
||||
<ConversationRow
|
||||
key={key}
|
||||
conversationKey={key}
|
||||
otherPubkey={otherPubkey}
|
||||
otherPubkeys={otherPubkeys}
|
||||
latestMessage={latestMessage}
|
||||
onClick={() => handleOpenConversation(key, otherPubkey)}
|
||||
onClick={() => handleOpenConversation(key, otherPubkeys)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -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 */}
|
||||
<div className="shrink-0">
|
||||
<UserName
|
||||
pubkey={otherPubkey}
|
||||
className="text-xs font-medium truncate"
|
||||
/>
|
||||
{/* Name(s) - no fixed width */}
|
||||
<div className="shrink-0 flex items-center gap-1">
|
||||
{otherPubkeys.map((pubkey, index) => (
|
||||
<span key={pubkey} className="flex items-center">
|
||||
<UserName
|
||||
pubkey={pubkey}
|
||||
className="text-xs font-medium truncate"
|
||||
/>
|
||||
{index < otherPubkeys.length - 1 && (
|
||||
<span className="text-muted-foreground/50">,</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Message preview - use CSS truncation and RichText with pointer-events-none */}
|
||||
|
||||
@@ -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<void>((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<void>[] = [];
|
||||
|
||||
await new Promise<void>((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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user