feat(comments): notify all space members on new comments and replies
Broadcasts in-app, push, and email notifications to all space members when a comment pin is created or replied to. @mentioned users get their specific mention notification instead (no double-notify). Fixes pre-existing TS error in rtasks local-first-client (missing dueDate default). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
85cf54b811
commit
de698a2aa3
|
|
@ -226,6 +226,11 @@ export class CommentPinManager {
|
|||
this.#notifyMentions(pinId, did, name, mentionedDids);
|
||||
}
|
||||
|
||||
// Fire-and-forget notification to all space members
|
||||
const pin = this.#sync.doc.commentPins?.[pinId];
|
||||
const isReply = (pin?.messages?.length ?? 0) > 1;
|
||||
this.#notifySpaceMembers(pinId, text, isReply, mentionedDids);
|
||||
|
||||
// Re-render popover if open
|
||||
if (this.#openPinId === pinId) {
|
||||
this.#openPinPopover(pinId, false);
|
||||
|
|
@ -836,6 +841,41 @@ export class CommentPinManager {
|
|||
}
|
||||
}
|
||||
|
||||
async #notifySpaceMembers(
|
||||
pinId: string,
|
||||
text: string,
|
||||
isReply: boolean,
|
||||
mentionedDids?: string[],
|
||||
) {
|
||||
const did = this.#getLocalDID();
|
||||
const name = this.#getLocalUsername();
|
||||
const pins = this.#sync.doc.commentPins || {};
|
||||
const sortedPins = Object.values(pins).sort((a, b) => a.createdAt - b.createdAt);
|
||||
const pinIndex = sortedPins.findIndex((p) => p.id === pinId) + 1;
|
||||
|
||||
try {
|
||||
const sess = JSON.parse(localStorage.getItem("encryptid_session") || "{}");
|
||||
await fetch(`${getModuleApiBase("rspace")}/api/comment-pins/notify`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(sess?.accessToken ? { Authorization: `Bearer ${sess.accessToken}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pinId,
|
||||
authorDid: did,
|
||||
authorName: name,
|
||||
text,
|
||||
pinIndex,
|
||||
isReply,
|
||||
mentionedDids: mentionedDids || [],
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// Silent fail — notification is best-effort
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reminder ──
|
||||
|
||||
async #createReminder(pinId: string) {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export class TasksLocalFirstClient {
|
|||
const docId = boardDocId(this.#space, boardId) as DocumentId;
|
||||
this.#sync.change<BoardDoc>(docId, `Update task ${taskId}`, (d) => {
|
||||
if (!d.tasks[taskId]) {
|
||||
d.tasks[taskId] = { id: taskId, spaceId: boardId, title: '', description: '', status: 'TODO', priority: null, labels: [], assigneeId: null, createdBy: null, sortOrder: 0, createdAt: Date.now(), updatedAt: Date.now(), ...changes };
|
||||
d.tasks[taskId] = { id: taskId, spaceId: boardId, title: '', description: '', status: 'TODO', priority: null, labels: [], assigneeId: null, createdBy: null, sortOrder: 0, dueDate: null, createdAt: Date.now(), updatedAt: Date.now(), ...changes };
|
||||
} else {
|
||||
Object.assign(d.tasks[taskId], changes);
|
||||
d.tasks[taskId].updatedAt = Date.now();
|
||||
|
|
|
|||
|
|
@ -915,7 +915,7 @@ app.get("/api/modules/:moduleId/landing", (c) => {
|
|||
});
|
||||
|
||||
// ── Comment Pin API ──
|
||||
import { listAllUsersWithTrust } from "../src/encryptid/db";
|
||||
import { listAllUsersWithTrust, listSpaceMembers } from "../src/encryptid/db";
|
||||
|
||||
// Space members for @mention autocomplete
|
||||
app.get("/:space/api/space-members", async (c) => {
|
||||
|
|
@ -934,6 +934,61 @@ app.get("/:space/api/space-members", async (c) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Space-wide comment notification (all members)
|
||||
app.post("/:space/api/comment-pins/notify", async (c) => {
|
||||
const space = c.req.param("space");
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { pinId, authorDid, authorName, text, pinIndex, isReply, mentionedDids } = body;
|
||||
if (!pinId || !authorDid) {
|
||||
return c.json({ error: "Missing fields" }, 400);
|
||||
}
|
||||
|
||||
const members = await listSpaceMembers(space);
|
||||
const excludeDids = new Set<string>([authorDid, ...(mentionedDids || [])]);
|
||||
const title = isReply
|
||||
? `${authorName || "Someone"} replied to a comment`
|
||||
: `New comment from ${authorName || "Someone"}`;
|
||||
const preview = text ? text.slice(0, 120) + (text.length > 120 ? "..." : "") : "";
|
||||
|
||||
// In-app + push notifications for all members (excluding author and @mentioned)
|
||||
await Promise.all(
|
||||
members
|
||||
.filter((m) => !excludeDids.has(m.userDID))
|
||||
.map((m) =>
|
||||
notify({
|
||||
userDid: m.userDID,
|
||||
category: "module",
|
||||
eventType: "canvas_comment",
|
||||
title,
|
||||
body: preview || `Comment pin #${pinIndex || "?"} in ${space}`,
|
||||
spaceSlug: space,
|
||||
moduleId: "rspace",
|
||||
actionUrl: `/rspace#pin-${pinId}`,
|
||||
actorDid: authorDid,
|
||||
actorUsername: authorName,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Email notification to all space members (fire-and-forget)
|
||||
import("../modules/rinbox/agent-notify")
|
||||
.then(({ sendSpaceNotification }) => {
|
||||
sendSpaceNotification(
|
||||
space,
|
||||
title,
|
||||
`<p>${preview}</p><p><a href="https://${space}.rspace.online/rspace#pin-${pinId}">View comment</a></p>`,
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return c.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error("[comment-pins] notify-all error:", err);
|
||||
return c.json({ error: "Failed to send notifications" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Mention notification
|
||||
app.post("/:space/api/comment-pins/notify-mention", async (c) => {
|
||||
const space = c.req.param("space");
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export type NotificationEventType =
|
|||
| 'nest_request' | 'nest_created' | 'space_invite'
|
||||
// Module
|
||||
| 'inbox_new_mail' | 'inbox_approval_needed' | 'choices_result'
|
||||
| 'notes_shared' | 'canvas_mention'
|
||||
| 'notes_shared' | 'canvas_mention' | 'canvas_comment'
|
||||
// System
|
||||
| 'guardian_invite' | 'guardian_accepted' | 'recovery_initiated'
|
||||
| 'recovery_approved' | 'device_linked' | 'security_alert'
|
||||
|
|
|
|||
Loading…
Reference in New Issue