import { jalaliToDate } from '@/lib/jalali'; export type ActivityThreadEntry = { at: Date | null; dateLabel: string; timeLabel: string; author: string; body: string; /** شناسهٔ فایل‌های چسبیده به این یادداشت (عکس/صوت/فایل) */ attachmentIds?: string[]; }; const STAMP = /^\[(.+?) ساعت (\S+?)(?: — (.+))?\]$/; const ATTACHMENTS = /^\[\[attachments:([^\]]+)\]\]$/i; const COMMENT_MEDIA_WINDOW_MS = 3 * 60 * 1000; /** یادداشت فعالیت را به نظرهای جدا با مهر تاریخ/ساعت/نویسنده تبدیل می‌کند. */ export function parseActivityThread(raw: string | null | undefined): ActivityThreadEntry[] | null { const text = (raw ?? '').replace(/\r\n/g, '\n').trim(); if (!text) return null; const entries: ActivityThreadEntry[] = []; const preamble: string[] = []; let current: ActivityThreadEntry | null = null; let sawStamp = false; for (const line of text.split('\n')) { const trimmed = line.trim(); const stamp = trimmed.match(STAMP); if (stamp) { sawStamp = true; if (current) entries.push(trimEntry(current)); current = { at: stampToDate(stamp[1] ?? '', stamp[2] ?? ''), dateLabel: (stamp[1] ?? '').trim(), timeLabel: (stamp[2] ?? '').trim(), author: (stamp[3] ?? '').trim(), body: '', attachmentIds: [], }; continue; } const attach = trimmed.match(ATTACHMENTS); if (attach && current) { current.attachmentIds = attach[1]! .split(',') .map((id) => id.trim()) .filter(Boolean); continue; } if (current) current.body += `${current.body ? '\n' : ''}${line}`; else preamble.push(line); } if (current) entries.push(trimEntry(current)); if (!sawStamp) return null; const head = preamble.join('\n').trim(); if (head) { entries.unshift({ at: null, dateLabel: '', timeLabel: '', author: '', body: head, attachmentIds: [], }); } return entries; } /** برگرداندن متن ذخیره‌شونده از نظرهای جدا. */ export function serializeActivityThread(entries: ActivityThreadEntry[]): string { return entries .map((entry) => { const ids = (entry.attachmentIds ?? []).filter(Boolean); const attachLine = ids.length ? `\n[[attachments:${ids.join(',')}]]` : ''; if (!entry.dateLabel) return `${entry.body}${attachLine}`.trim(); const stamp = `[${entry.dateLabel} ساعت ${entry.timeLabel}${entry.author ? ` — ${entry.author}` : ''}]`; return entry.body ? `${stamp}\n${entry.body}${attachLine}` : `${stamp}${attachLine}`; }) .filter((chunk) => chunk.trim()) .join('\n\n'); } /** فایل‌ها را به یادداشت‌ها می‌چسباند: اول با شناسهٔ صریح، بعد با نزدیکی زمان. */ export function bindThreadMedia( entries: ActivityThreadEntry[], images: T[], windowMs = COMMENT_MEDIA_WINDOW_MS, ): { blocks: Array; loose: T[] } { const blocks = entries.map((entry) => ({ ...entry, media: [] as T[] })); const byId = new Map(images.map((image) => [image.id, image])); const used = new Set(); blocks.forEach((block) => { for (const id of block.attachmentIds ?? []) { const image = byId.get(id); if (!image || used.has(id)) continue; block.media.push(image); used.add(id); } }); for (const image of images) { if (used.has(image.id)) continue; const at = new Date(image.createdAt).getTime(); if (Number.isNaN(at)) continue; let best = -1; let bestDelta = Number.POSITIVE_INFINITY; blocks.forEach((block, index) => { if (!block.at) return; const delta = block.at.getTime() - at; if (delta >= -60_000 && delta < bestDelta) { best = index; bestDelta = delta; } }); if (best >= 0 && bestDelta <= windowMs) { blocks[best]!.media.push(image); used.add(image.id); } } const firstStamp = blocks.find((block) => block.at)?.at?.getTime() ?? Number.POSITIVE_INFINITY; const preamble = blocks.find((block) => !block.at); const loose: T[] = []; for (const image of images) { if (used.has(image.id)) continue; const at = new Date(image.createdAt).getTime(); if (preamble && !Number.isNaN(at) && at < firstStamp) preamble.media.push(image); else loose.push(image); } return { blocks, loose }; } function trimEntry(entry: ActivityThreadEntry): ActivityThreadEntry { return { ...entry, body: entry.body.trim(), attachmentIds: [...new Set((entry.attachmentIds ?? []).filter(Boolean))], }; } function latinDigits(value: string): string { const fa = '۰۱۲۳۴۵۶۷۸۹'; const ar = '٠١٢٣٤٥٦٧٨٩'; return value.replace(/[۰-۹٠-٩]/g, (ch) => { const faIndex = fa.indexOf(ch); return String(faIndex >= 0 ? faIndex : ar.indexOf(ch)); }); } function stampToDate(dateLabel: string, timeLabel: string): Date | null { const date = latinDigits(dateLabel).match(/(\d{4})\/(\d{1,2})\/(\d{1,2})/); if (!date) return null; const at = jalaliToDate(Number(date[1]), Number(date[2]), Number(date[3])); if (Number.isNaN(at.getTime())) return null; const time = latinDigits(timeLabel).match(/(\d{1,2}):(\d{2})/); if (time) at.setHours(Number(time[1]), Number(time[2]), 0, 0); return at; }