mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
243 lines
6.7 KiB
JavaScript
243 lines
6.7 KiB
JavaScript
import {
|
|
getConversation,
|
|
listConversations,
|
|
updateConversationFromMessage,
|
|
updateConversationOpenState,
|
|
} from "../storage/dmDb.js";
|
|
import { userLogger } from "../utils/logger.js";
|
|
|
|
function sanitizeTimestamp(value) {
|
|
const numeric = Number(value);
|
|
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
return 0;
|
|
}
|
|
return Math.floor(numeric);
|
|
}
|
|
|
|
function normalizeConversationState(conversation) {
|
|
if (!conversation || typeof conversation !== "object") {
|
|
return {
|
|
conversation_id: "",
|
|
opened_until: 0,
|
|
downloaded_until: 0,
|
|
unseen_count: 0,
|
|
};
|
|
}
|
|
|
|
return {
|
|
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,
|
|
};
|
|
}
|
|
|
|
export class DmNotificationManager {
|
|
constructor({ logger } = {}) {
|
|
this.logger = logger || userLogger;
|
|
this.conversationCache = new Map();
|
|
this.updateQueue = new Map();
|
|
this.focusedConversationId = "";
|
|
}
|
|
|
|
getFocusedConversationId() {
|
|
return this.focusedConversationId;
|
|
}
|
|
|
|
enqueueUpdate(conversationId, task) {
|
|
const key = conversationId || "global";
|
|
const previous = this.updateQueue.get(key) || Promise.resolve();
|
|
const next = previous
|
|
.then(() => task())
|
|
.catch((error) => {
|
|
this.logger.warn(
|
|
`[dmNotifications] Update task failed for ${key}:`,
|
|
error,
|
|
);
|
|
});
|
|
this.updateQueue.set(key, next);
|
|
return next;
|
|
}
|
|
|
|
reset() {
|
|
this.conversationCache.clear();
|
|
this.updateQueue.clear();
|
|
this.focusedConversationId = "";
|
|
}
|
|
|
|
setFocusedConversation(conversationId, isFocused = true) {
|
|
if (typeof conversationId !== "string") {
|
|
return;
|
|
}
|
|
|
|
const normalized = conversationId.trim();
|
|
if (!normalized) {
|
|
return;
|
|
}
|
|
|
|
if (isFocused) {
|
|
this.focusedConversationId = normalized;
|
|
return;
|
|
}
|
|
|
|
if (this.focusedConversationId === normalized) {
|
|
this.focusedConversationId = "";
|
|
}
|
|
}
|
|
|
|
async ensureConversationState(conversationId) {
|
|
if (typeof conversationId !== "string" || !conversationId.trim()) {
|
|
return null;
|
|
}
|
|
|
|
const normalized = conversationId.trim();
|
|
if (this.conversationCache.has(normalized)) {
|
|
return this.conversationCache.get(normalized);
|
|
}
|
|
|
|
try {
|
|
const stored = await getConversation(normalized);
|
|
const normalizedState = normalizeConversationState({
|
|
conversation_id: normalized,
|
|
...stored,
|
|
});
|
|
this.conversationCache.set(normalized, normalizedState);
|
|
return normalizedState;
|
|
} catch (error) {
|
|
this.logger.warn("[dmNotifications] Failed to hydrate conversation", error);
|
|
const fallback = normalizeConversationState({ conversation_id: normalized });
|
|
this.conversationCache.set(normalized, fallback);
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
recordMessage({ record, preview = "", direction = "" } = {}) {
|
|
if (!record || typeof record !== "object") {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
const conversationId =
|
|
typeof record.conversation_id === "string"
|
|
? record.conversation_id.trim()
|
|
: "";
|
|
if (!conversationId) {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
return this.enqueueUpdate(conversationId, async () => {
|
|
const createdAt = sanitizeTimestamp(record.created_at);
|
|
const conversationState = await this.ensureConversationState(conversationId);
|
|
if (!conversationState) {
|
|
return null;
|
|
}
|
|
|
|
const isIncoming = direction === "incoming";
|
|
const isFocused = this.focusedConversationId === conversationId;
|
|
const shouldNotify =
|
|
isIncoming && !isFocused && createdAt > conversationState.opened_until;
|
|
const unseenDelta =
|
|
isIncoming && createdAt > conversationState.opened_until ? 1 : 0;
|
|
|
|
let updated = null;
|
|
try {
|
|
updated = await updateConversationFromMessage(record, {
|
|
preview,
|
|
unseenDelta,
|
|
downloadedUntil: createdAt,
|
|
});
|
|
} catch (error) {
|
|
this.logger.warn("[dmNotifications] Failed to update conversation", error);
|
|
}
|
|
|
|
const normalizedUpdate = normalizeConversationState({
|
|
conversation_id: conversationId,
|
|
...(updated || conversationState),
|
|
opened_until: updated?.opened_until ?? conversationState.opened_until,
|
|
});
|
|
|
|
this.conversationCache.set(conversationId, normalizedUpdate);
|
|
|
|
return {
|
|
conversation: normalizedUpdate,
|
|
shouldNotify,
|
|
isFocused,
|
|
};
|
|
});
|
|
}
|
|
|
|
acknowledgeRenderedMessages({ conversationId, renderedUntil } = {}) {
|
|
if (typeof conversationId !== "string" || !conversationId.trim()) {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
const normalized = conversationId.trim();
|
|
|
|
return this.enqueueUpdate(normalized, async () => {
|
|
const conversationState = await this.ensureConversationState(normalized);
|
|
if (!conversationState) {
|
|
return null;
|
|
}
|
|
|
|
const nextOpenedUntil = Math.max(
|
|
conversationState.opened_until || 0,
|
|
sanitizeTimestamp(renderedUntil),
|
|
);
|
|
|
|
let updated = null;
|
|
try {
|
|
updated = await updateConversationOpenState(normalized, {
|
|
openedUntil: nextOpenedUntil,
|
|
unseenCount: 0,
|
|
});
|
|
} catch (error) {
|
|
this.logger.warn("[dmNotifications] Failed to update opened_until", error);
|
|
}
|
|
|
|
const normalizedUpdate = normalizeConversationState({
|
|
conversation_id: normalized,
|
|
...conversationState,
|
|
...updated,
|
|
opened_until: nextOpenedUntil,
|
|
unseen_count: 0,
|
|
});
|
|
|
|
this.conversationCache.set(normalized, normalizedUpdate);
|
|
return normalizedUpdate;
|
|
});
|
|
}
|
|
|
|
async listConversationSummaries() {
|
|
try {
|
|
const stored = await listConversations();
|
|
const summaries = stored.map((conversation) =>
|
|
normalizeConversationState(conversation),
|
|
);
|
|
|
|
for (const summary of summaries) {
|
|
if (summary.conversation_id) {
|
|
this.conversationCache.set(summary.conversation_id, summary);
|
|
}
|
|
}
|
|
|
|
return summaries;
|
|
} catch (error) {
|
|
this.logger.warn("[dmNotifications] Failed to list conversations", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
getUnseenCount(conversationId) {
|
|
if (typeof conversationId !== "string" || !conversationId.trim()) {
|
|
return 0;
|
|
}
|
|
|
|
const cached = this.conversationCache.get(conversationId.trim());
|
|
return cached && Number.isFinite(Number(cached.unseen_count))
|
|
? Number(cached.unseen_count)
|
|
: 0;
|
|
}
|
|
}
|