import { IChatAttachment, IChatMessage } from './models/IChatMessage'; import { displayFileName } from './chatFileNames'; /** * Часть комментария SharePoint, из которой строится сообщение чата. * * Объявлена своим типом, а не взята как ICommentInfo из @pnp/sp/comments: * этот модуль намеренно НЕ знает про PnPjs (он чистый и покрыт тестами), а * ICommentInfo структурно подходит под этот интерфейс — сервис передаёт его * без приведения типов. */ export interface ICommentSource { id: string; text: string; createdDate: string; author?: { name?: string; email?: string }; } /** * Служебная пометка о прикреплённых файлах в КОНЦЕ текста комментария: * Проверьте, пожалуйста, акт * [rp-files]20260824-153012-1__акт.pdf|20260824-153012-2__скан.jpg * * Почему в тексте, а не отдельным хранилищем: у комментария SharePoint нет * своих полей — только текст. Класть связь «сообщение → файлы» больше некуда, * не заводя отдельный список и не превращая каждую загрузку чата в лишний * запрос к библиотеке. Имена файлов складские (с префиксом), но не полные * адреса — адрес библиотеки резолвится на лету, поэтому её можно переместить, * и ссылки в старой переписке не сломаются. * * Разделитель имён — «|»: этот символ SharePoint в именах файлов запрещает * (см. FORBIDDEN_CHARS), значит в имени он встретиться не может. */ const FILE_MARKER = '[rp-files]'; /** Пометка всегда последняя и всегда в одну строку — отсюда привязка к концу. */ const FILE_MARKER_PATTERN = /\s*\[rp-files\]([^\r\n]*)$/; /** * Собрать текст комментария из сообщения и складских имён файлов. * Сообщение может быть пустым — тогда комментарий состоит из одной пометки * (так выглядит отправка «только файл»). */ export function buildCommentText(text: string, storedFileNames: string[]): string { const trimmed = text.trim(); if (storedFileNames.length === 0) return trimmed; const marker = `${FILE_MARKER}${storedFileNames.join('|')}`; return trimmed ? `${trimmed}\n${marker}` : marker; } function parseStoredNames(raw: string): string[] { const found = FILE_MARKER_PATTERN.exec(raw); if (!found) return []; return found[1] .split('|') .map((name) => name.trim()) .filter((name) => !!name); } /** Есть ли в переписке файлы — по этому признаку сервис решает, нужен ли адрес библиотеки. */ export function hasAttachments(comments: ICommentSource[]): boolean { return comments.some((c) => !!c.text && FILE_MARKER_PATTERN.test(c.text)); } /** * Дата комментария приходит строкой ISO. Если часового пояса в ней нет, * new Date() трактует строку как МЕСТНОЕ время, а SharePoint отдаёт UTC — * сообщения уехали бы на 5 часов (Астана — UTC+5). Поэтому при отсутствии * пояса дописываем Z явно. */ function parseCommentDate(raw: string): Date { const hasTimeZone = /(Z|[+-]\d{2}:\d{2})$/.test(raw); return new Date(hasTimeZone ? raw : `${raw}Z`); } function toAttachments(storedNames: string[], folderUrl: string): IChatAttachment[] { return storedNames.map((storedName) => ({ fileName: displayFileName(storedName), url: folderUrl ? `${folderUrl}/${storedName}` : '' })); } function toMessage( comment: ICommentSource, currentUserEmail: string, folderUrl: string ): IChatMessage { const authorEmail = comment.author?.email || ''; const authorName = comment.author?.name || authorEmail || 'Неизвестный автор'; const storedNames = parseStoredNames(comment.text); return { id: comment.id, text: comment.text.replace(FILE_MARKER_PATTERN, '').trim(), authorName, authorEmail, createdAt: parseCommentDate(comment.createdDate), isOwn: !!authorEmail && !!currentUserEmail && authorEmail.toLowerCase() === currentUserEmail.toLowerCase(), attachments: toAttachments(storedNames, folderUrl) }; } /** * Комментарии SharePoint → сообщения чата, от старых к новым (как в любой * переписке: свежее внизу). * * Порядок ответа API не документирован и мы на него не опираемся — сортируем * по дате сами. Комментарии без текста и без файлов отбрасываем: показывать * пустой «пузырь» нечего, а в списке они появляются, если кто-то отправил * комментарий не из портала (например, из стандартной формы SharePoint). * * folderUrl — папка заявки в библиотеке «Файлы чата»; пустая строка допустима, * когда в переписке файлов нет (тогда адрес библиотеки и не запрашивался). */ export function toChatMessages( comments: ICommentSource[], currentUserEmail: string, folderUrl: string = '' ): IChatMessage[] { return comments .filter((c) => !!c.text && !!c.text.trim()) .map((c) => toMessage(c, currentUserEmail, folderUrl)) .filter((m) => !!m.text || m.attachments.length > 0) .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); }