Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m45s
Details
CI/CD / deploy (push) Successful in 2m45s
Details
This commit is contained in:
commit
24c0905c03
|
|
@ -226,6 +226,11 @@ export class CommentPinManager {
|
||||||
this.#notifyMentions(pinId, did, name, mentionedDids);
|
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
|
// Re-render popover if open
|
||||||
if (this.#openPinId === pinId) {
|
if (this.#openPinId === pinId) {
|
||||||
this.#openPinPopover(pinId, false);
|
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 ──
|
// ── Reminder ──
|
||||||
|
|
||||||
async #createReminder(pinId: string) {
|
async #createReminder(pinId: string) {
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export class TasksLocalFirstClient {
|
||||||
const docId = boardDocId(this.#space, boardId) as DocumentId;
|
const docId = boardDocId(this.#space, boardId) as DocumentId;
|
||||||
this.#sync.change<BoardDoc>(docId, `Update task ${taskId}`, (d) => {
|
this.#sync.change<BoardDoc>(docId, `Update task ${taskId}`, (d) => {
|
||||||
if (!d.tasks[taskId]) {
|
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 {
|
} else {
|
||||||
Object.assign(d.tasks[taskId], changes);
|
Object.assign(d.tasks[taskId], changes);
|
||||||
d.tasks[taskId].updatedAt = Date.now();
|
d.tasks[taskId].updatedAt = Date.now();
|
||||||
|
|
|
||||||
|
|
@ -915,7 +915,7 @@ app.get("/api/modules/:moduleId/landing", (c) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Comment Pin API ──
|
// ── Comment Pin API ──
|
||||||
import { listAllUsersWithTrust } from "../src/encryptid/db";
|
import { listAllUsersWithTrust, listSpaceMembers } from "../src/encryptid/db";
|
||||||
|
|
||||||
// Space members for @mention autocomplete
|
// Space members for @mention autocomplete
|
||||||
app.get("/:space/api/space-members", async (c) => {
|
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
|
// Mention notification
|
||||||
app.post("/:space/api/comment-pins/notify-mention", async (c) => {
|
app.post("/:space/api/comment-pins/notify-mention", async (c) => {
|
||||||
const space = c.req.param("space");
|
const space = c.req.param("space");
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ export type NotificationEventType =
|
||||||
| 'nest_request' | 'nest_created' | 'space_invite'
|
| 'nest_request' | 'nest_created' | 'space_invite'
|
||||||
// Module
|
// Module
|
||||||
| 'inbox_new_mail' | 'inbox_approval_needed' | 'choices_result'
|
| 'inbox_new_mail' | 'inbox_approval_needed' | 'choices_result'
|
||||||
| 'notes_shared' | 'canvas_mention'
|
| 'notes_shared' | 'canvas_mention' | 'canvas_comment'
|
||||||
// System
|
// System
|
||||||
| 'guardian_invite' | 'guardian_accepted' | 'recovery_initiated'
|
| 'guardian_invite' | 'guardian_accepted' | 'recovery_initiated'
|
||||||
| 'recovery_approved' | 'device_linked' | 'security_alert'
|
| 'recovery_approved' | 'device_linked' | 'security_alert'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue