mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 19:35:28 +00:00
Merge pull request #1407 from PR0M3TH3AN/codex/add-mark-read-controls-to-dm-ui
DM: add per-conversation and global mark-read actions
This commit is contained in:
5 files changed
+192
-1
No files matched your search
@@ -28,6 +28,7 @@ function normalizeConversationState(conversation) {
|
||||
conversation_id: conversation.conversation_id || "",
|
||||
opened_until: sanitizeTimestamp(conversation.opened_until),
|
||||
downloaded_until: sanitizeTimestamp(conversation.downloaded_until),
|
||||
last_message_at: sanitizeTimestamp(conversation.last_message_at),
|
||||
unseen_count: Number.isFinite(Number(conversation.unseen_count))
|
||||
? Number(conversation.unseen_count)
|
||||
: 0,
|
||||
|
||||
@@ -127,6 +127,8 @@ export class AppShell {
|
||||
onSelectConversation,
|
||||
onSendMessage,
|
||||
onSendZap,
|
||||
onMarkConversationRead,
|
||||
onMarkAllRead,
|
||||
} = {}) {
|
||||
if (!doc) {
|
||||
throw new Error("AppShell requires a document reference.");
|
||||
@@ -165,6 +167,7 @@ export class AppShell {
|
||||
activeId: activeConversationId,
|
||||
state: conversationState,
|
||||
onSelect: onSelectConversation,
|
||||
onMarkAllRead,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -187,6 +190,7 @@ export class AppShell {
|
||||
conversationZapTotalSats:
|
||||
totalsByConversation.get(activeConversation.id) || 0,
|
||||
profileZapTotalSats: totalsByProfile.get(activeConversation.pubkey) || 0,
|
||||
onMarkRead: onMarkConversationRead,
|
||||
}),
|
||||
);
|
||||
main.appendChild(
|
||||
|
||||
@@ -21,6 +21,7 @@ export function ConversationList({
|
||||
activeId = "",
|
||||
state = "idle",
|
||||
onSelect,
|
||||
onMarkAllRead,
|
||||
} = {}) {
|
||||
if (!doc) {
|
||||
throw new Error("ConversationList requires a document reference.");
|
||||
@@ -29,6 +30,21 @@ export function ConversationList({
|
||||
const root = createElement(doc, "section", "dm-conversation-list");
|
||||
const header = createElement(doc, "div", "dm-conversation-list__header");
|
||||
header.appendChild(createElement(doc, "h2", "dm-conversation-list__title", "Messages"));
|
||||
if (typeof onMarkAllRead === "function") {
|
||||
const markAllButton = createElement(
|
||||
doc,
|
||||
"button",
|
||||
"btn-ghost",
|
||||
"Mark all as read",
|
||||
);
|
||||
markAllButton.type = "button";
|
||||
markAllButton.dataset.size = "sm";
|
||||
markAllButton.disabled = !conversations.length;
|
||||
markAllButton.addEventListener("click", () => {
|
||||
onMarkAllRead();
|
||||
});
|
||||
header.appendChild(markAllButton);
|
||||
}
|
||||
root.appendChild(header);
|
||||
|
||||
const list = createElement(doc, "div", "dm-conversation-list__items");
|
||||
|
||||
@@ -24,6 +24,7 @@ export function MessageThread({
|
||||
zapReceipts = [],
|
||||
conversationZapTotalSats = 0,
|
||||
profileZapTotalSats = 0,
|
||||
onMarkRead,
|
||||
} = {}) {
|
||||
if (!doc) {
|
||||
throw new Error("MessageThread requires a document reference.");
|
||||
@@ -82,6 +83,23 @@ export function MessageThread({
|
||||
),
|
||||
);
|
||||
header.appendChild(zapSummary);
|
||||
|
||||
if (typeof onMarkRead === "function") {
|
||||
const actions = createElement(doc, "div", "ml-auto flex items-center gap-2");
|
||||
const markReadButton = createElement(doc, "button", "btn-ghost", "Mark read");
|
||||
markReadButton.type = "button";
|
||||
markReadButton.dataset.size = "sm";
|
||||
const conversationId =
|
||||
typeof contact.id === "string" ? contact.id.trim() : "";
|
||||
if (!conversationId) {
|
||||
markReadButton.disabled = true;
|
||||
}
|
||||
markReadButton.addEventListener("click", () => {
|
||||
onMarkRead(contact);
|
||||
});
|
||||
actions.appendChild(markReadButton);
|
||||
header.appendChild(actions);
|
||||
}
|
||||
root.appendChild(header);
|
||||
|
||||
const timeline = createElement(doc, "div", "dm-message-thread__timeline");
|
||||
|
||||
@@ -3879,6 +3879,40 @@ export class ProfileModalController {
|
||||
return "nip04";
|
||||
}
|
||||
|
||||
getDirectMessagesForConversation(conversationId, actorPubkey = null) {
|
||||
const actor = this.normalizeHexPubkey(actorPubkey || this.resolveActiveDmActor());
|
||||
if (!actor || !conversationId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const remote = this.resolveRemoteForConversationId(conversationId, actor);
|
||||
if (!remote) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (Array.isArray(this.directMessagesCache) ? this.directMessagesCache : [])
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry &&
|
||||
entry.ok === true &&
|
||||
this.resolveDirectMessageRemote(entry, actor) === remote,
|
||||
)
|
||||
.sort((a, b) => (a?.timestamp || 0) - (b?.timestamp || 0));
|
||||
}
|
||||
|
||||
getLatestDirectMessageTimestampForConversation(conversationId, actorPubkey = null) {
|
||||
const messages = this.getDirectMessagesForConversation(
|
||||
conversationId,
|
||||
actorPubkey,
|
||||
);
|
||||
if (!messages.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const last = messages[messages.length - 1];
|
||||
return Number(last?.timestamp) || 0;
|
||||
}
|
||||
|
||||
buildDmMessageTimeline(messages, { actorPubkey, remotePubkey } = {}) {
|
||||
const actor = this.normalizeHexPubkey(actorPubkey);
|
||||
const remote = this.normalizeHexPubkey(remotePubkey);
|
||||
@@ -4383,6 +4417,12 @@ export class ProfileModalController {
|
||||
onSendMessage: (messageText, payload) => {
|
||||
void this.handleDmAppShellSendMessage(messageText, payload);
|
||||
},
|
||||
onMarkConversationRead: (conversation) => {
|
||||
void this.handleDmConversationMarkRead(conversation);
|
||||
},
|
||||
onMarkAllRead: () => {
|
||||
void this.handleDmMarkAllConversationsRead();
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.dmAppShell = null;
|
||||
@@ -4403,9 +4443,14 @@ export class ProfileModalController {
|
||||
container.appendChild(root);
|
||||
|
||||
if (actor && activeConversationId) {
|
||||
const renderedUntil =
|
||||
this.getLatestDirectMessageTimestampForConversation(
|
||||
activeConversationId,
|
||||
actor,
|
||||
) || Date.now() / 1000;
|
||||
void this.nostrService?.acknowledgeRenderedDirectMessages?.(
|
||||
activeConversationId,
|
||||
Date.now() / 1000,
|
||||
renderedUntil,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4460,6 +4505,113 @@ export class ProfileModalController {
|
||||
});
|
||||
}
|
||||
|
||||
async handleDmConversationMarkRead(conversation) {
|
||||
const conversationId =
|
||||
conversation && typeof conversation.id === "string"
|
||||
? conversation.id.trim()
|
||||
: this.activeDmConversationId;
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = this.resolveActiveDmActor();
|
||||
if (
|
||||
!actor ||
|
||||
!this.nostrService ||
|
||||
typeof this.nostrService.acknowledgeRenderedDirectMessages !== "function"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderedUntil = this.getLatestDirectMessageTimestampForConversation(
|
||||
conversationId,
|
||||
actor,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.nostrService.acknowledgeRenderedDirectMessages(
|
||||
conversationId,
|
||||
renderedUntil,
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.warn("[profileModal] Failed to mark conversation read:", error);
|
||||
}
|
||||
|
||||
const recipient = this.resolveRemoteForConversationId(conversationId, actor);
|
||||
const messages = this.getDirectMessagesForConversation(conversationId, actor);
|
||||
if (recipient && messages.length) {
|
||||
void this.maybePublishReadReceipt(messages, {
|
||||
recipientPubkey: recipient,
|
||||
});
|
||||
}
|
||||
|
||||
await this.renderDmAppShell(this.directMessagesCache, {
|
||||
actorPubkey: actor,
|
||||
});
|
||||
}
|
||||
|
||||
async handleDmMarkAllConversationsRead() {
|
||||
if (
|
||||
!this.nostrService ||
|
||||
typeof this.nostrService.acknowledgeRenderedDirectMessages !== "function"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = this.resolveActiveDmActor();
|
||||
if (!actor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const summaries =
|
||||
typeof this.nostrService.listDirectMessageConversationSummaries === "function"
|
||||
? await this.nostrService.listDirectMessageConversationSummaries()
|
||||
: [];
|
||||
const list = Array.isArray(summaries) ? summaries : [];
|
||||
|
||||
for (const summary of list) {
|
||||
const conversationId =
|
||||
typeof summary?.conversation_id === "string"
|
||||
? summary.conversation_id.trim()
|
||||
: typeof summary?.conversationId === "string"
|
||||
? summary.conversationId.trim()
|
||||
: "";
|
||||
if (!conversationId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const renderedUntil =
|
||||
Number(summary?.last_message_at) ||
|
||||
Number(summary?.downloaded_until) ||
|
||||
Number(summary?.opened_until) ||
|
||||
this.getLatestDirectMessageTimestampForConversation(conversationId, actor);
|
||||
|
||||
try {
|
||||
await this.nostrService.acknowledgeRenderedDirectMessages(
|
||||
conversationId,
|
||||
renderedUntil,
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[profileModal] Failed to mark conversation read:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
const recipient = this.resolveRemoteForConversationId(conversationId, actor);
|
||||
const messages = this.getDirectMessagesForConversation(conversationId, actor);
|
||||
if (recipient && messages.length) {
|
||||
void this.maybePublishReadReceipt(messages, {
|
||||
recipientPubkey: recipient,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.renderDmAppShell(this.directMessagesCache, {
|
||||
actorPubkey: actor,
|
||||
});
|
||||
}
|
||||
|
||||
async handleDmAppShellSendMessage(messageText, payload = {}) {
|
||||
const normalizedPayload =
|
||||
payload && typeof payload === "object" ? payload : {};
|
||||
|
||||
Reference in new issue
Block a user