Перейти к содержанию

Комментарии: бизнес-логика

Обзор

Комментарии обеспечивают коммуникацию участников внутри задач. Поддерживаются треды (вложенные ответы), реакции (эмоции), пересылка комментариев между задачами, а также форматирование текста и прикрепление файлов. Поведение комментариев настраивается на уровне подкатегории.

Типы комментариев (TypeID)

Полный справочник: docs/reference/database/comments.md

Пользовательские типы

TypeID Описание Создаётся
3 Пользовательский комментарий Вручную пользователем
6 Комментарий из подзадачи Автоматически при публикации в подзадаче
10 Вложение файла (без текста) Пользователем через drag&drop файла
14 Видео-комментарий (deprecated) Закомментирован в коде, не используется в боевой БД
17 Аудио-комментарий (deprecated) Закомментирован в коде, не используется в боевой БД
20 Внесение трудозатрат Пользователем через форму трудозатрат

Системные типы

TypeID Описание Событие
1 Переход по маршруту Смена шага/статуса задачи
2 Подпись (резолюция) Согласование/подписание
5 Служебный Различные системные действия
7 Изменение ДП или файлов Изменение допараметра или файла
8 Создание задачи Создание новой задачи
9 Изменение текста задачи Редактирование описания
11 Смена категории Перемещение задачи
12 Смена срока Изменение дедлайна
13 Добавлен исполнитель Назначение исполнителя
15 Добавлен подписчик Добавление подписчика
16 Задача просрочена Автоматически при просрочке
18 Смена заказчика Изменение заказчика задачи
19 Удалён файл Удаление файла

Системные комментарии формируют текст динамически через dbo.CommentAutoContent(). Подробнее: docs/domains/comments/database.md.


Механизм EventID и автогенерируемого текста

Контекст

Комментарии могут содержать реальный текст (Comments.Content) или автогенерируемый текст (из шаблонов EventTypeTemplates). Выбор определяется полем Comments.EventID.

Логика определения текста (SP ShowCommentsFeed)

Файл: миграция 1760103589, строки 3106–3111

Content =
    case
        when c.EventID is null
        then isnull(xlted.Content, c.Content)   -- ← реальный текст пользователя
        else ac.Content                          -- ← автогенерируемый текст из CommentAutoContent()
    end,

Правило: - EventID is null → отображается Comments.Content (или перевод из UserCommentsTranslated, если есть). - EventID is not null → отображается результат функции dbo.CommentAutoContent(CommentID, LangID).

Важно: При EventID is not null поле Comments.Content полностью игнорируется SP. Даже если там есть текст — он не отобразится.

Когда устанавливается EventID

Файл: core/TCClassLib/Comments.cs:DefineEventType (строки 436–448)

Логика:

if (fileIds.Count > 0 && string.IsNullOrEmpty(commentText))
{
    return 24;  // AfterFileUploaded — комментарий только с файлами
}
else
{
    return null;  // пользовательский комментарий с текстом (или текст + файлы)
}

Таблица EventID:

EventID EventTypeName Когда устанавливается Автотекст (примеры)
null (нет) Пользователь написал текст (независимо от файлов) — (берётся Comments.Content)
24 AfterFileUploaded Прикреплены файлы БЕЗ текста "Вложен файл: report.xlsx"
25 AfterFileDeleted Удалён файл "Удалён файл: report.xlsx"
26 AfterFileChanged Изменён файл "Изменён файл: report.xlsx → report_v2.xlsx"
... (другие события) Системные действия (создание задачи, смена статуса, и т.д.) Шаблоны из EventTypeTemplates

Полный список EventID: см. docs/reference/database/comments.md.

Генерация автотекста (функция CommentAutoContent)

Файл: миграция 1767116554, функция dbo.CommentAutoContent(@CommentID int, @LangID int = 1049, @UID int = 0)

Алгоритм: 1. Получить EventID комментария. 2. Найти шаблон в EventTypeTemplates для данного EventID + язык. 3. Если шаблон не найден → вернуть пустую строку. 4. Подставить параметры в шаблон (например, имена файлов для EventID=24). 5. Кешировать результат в AutoCommentCache.

Пример для EventID=24 (AfterFileUploaded, строки 256–281):

-- Собрать имена файлов из FileStorageFileToCommentLinks
declare @FileNames nvarchar(max) = (
    select N', ' + cfile.[Name]
    from   dbo.FileStorageFileToCommentLinks fcl
           join dbo.FileStorageFileInfoLatestVersion cfile on cfile.FileId = fcl.FileId
    where  fcl.CommentId = @CommentID
    for xml path(''), type
).value('.', 'nvarchar(max)');

if @FileNames > N''
    -- Подставить в шаблон: "Вложен файл: $FILE_NAMES$"
    insert into @TemplateSubstitutios select '$FILE_NAMES$', @FileNames, 1;
else
    -- Fallback на Comments.Content только если файлов НЕТ
    set @ret = @Content;

Ключевое: файлы привязаны через FileStorageFileToCommentLinks и не удаляются при редактировании текста. Если @FileNames непустой → функция вернёт шаблон с подстановкой имён файлов, полностью игнорируя Comments.Content.

Редактирование комментариев с EventID

Проблема: При редактировании комментария с EventID = 24 (только файлы) пользователь может добавить текст. Если не сбросить EventID → null, введённый текст запишется в Comments.Content, но не отобразится — SP продолжит брать автотекст.

Решение (backend):

Файл: core/TCClassLib/Comments.cs:EditCommentFromInterface (строки 3627–3641)

// Обнулить EventID при добавлении текста
var newEventId = !string.IsNullOrEmpty(newInputText) ? (int?)null : commentInfo.EventId;

CommentsEntityService.UpdateByPredicate(x => x.Id == commentId, x => new CommentEntity
{
    Content = newCommentText,
    EventId = newEventId,  // ← обнуляется, если текст добавлен
    HasBeenEdited = true,
    ...
});

// Очистить кеш автотекста
delete from dbo.AutoCommentCache where commentid = @commentId;

Почему это критично: - Без сброса EventID: SP берёт CommentAutoContent() → игнорирует Content. - С EventID = null: SP берёт isnull(xlted.Content, c.Content) → отображается введённый текст.

Связанные данные: Та же логика case when EventID is null применяется к: - ForwardedCommentContent (пересланный комментарий, строка 3160 SP). - InReplyComment_Content (комментарий, на который ответили, строка 3181 SP). - Tooltip избранного (Comments.cs:843–852).

Кеширование автотекста

Таблица: AutoCommentCache

Поле Тип Описание
CommentID int (PK) ID комментария
Content nvarchar(max) Результат CommentAutoContent()

Когда заполняется: При первом чтении комментария через SP ShowCommentsFeed (строка 3099).

Когда очищается: - При редактировании комментария → EditCommentFromInterface (строки 3630–3632). - При изменении шаблона в EventTypeTemplates → массовая очистка (не автоматическая).

Зачем нужен: CommentAutoContent() — тяжёлая функция (подстановка параметров, работа с XML, JOIN'ы). Кеш ускоряет повторное чтение ленты.

Примеры EventTypeTemplates (шаблоны)

Таблица: EventTypeTemplates

EventTypeID LangID Template
24 1049 Вложен файл: $FILE_NAMES$
25 1049 Удалён файл: $FILE_NAMES$
8 1049 Создана задача
1 1049 Переход: $STATE_FROM$ → $STATE_TO$

Параметры подстановки: - $FILE_NAMES$ — имена файлов (через запятую). - $STATE_FROM$, $STATE_TO$ — названия статусов. - $USER_NAME$ — имя пользователя (исполнитель, подписчик, и т.д.). - $EP_NAME$, $EP_VALUE$ — название и значение ДП.

Где настраивается: БД-миграции (нет UI для редактирования шаблонов).

Диагностика проблем с автотекстом

Симптом 1: Пустой текст у системного комментария

SQL:

-- Проверка наличия шаблона
select
        t.ID,
        t.EventTypeID,
        et.TypeName,
        t.LangID,
        t.Template
from EventTypeTemplates t
        join EventTypes et on et.ID = t.EventTypeID
where et.ID = @eventId
        and t.LangID = 1049;  -- русский

-- Если строка отсутствует → шаблон не настроен → CommentAutoContent вернёт пустую строку

Симптом 2: Текст введён, но не отображается

SQL:

-- Проверка EventID
select
        c.CommentID,
        c.EventID,       -- ← должно быть NULL после редактирования
        c.Content,       -- ← новый текст
        c.HasBeenEdited  -- ← true
from Comments c
where c.CommentID = @commentId;

-- Если EventID is not null → SP берёт автотекст вместо Content

Симптом 3: Автотекст не обновился после изменения шаблона

SQL:

-- Очистить кеш для пересоздания
delete from AutoCommentCache where CommentID = @commentId;

-- Или массовая очистка для всех комментариев данного EventID
delete from AutoCommentCache
where CommentID in (
    select c.CommentID
    from Comments c
    where c.EventID = @eventId
);


Адресаты комментариев (кто видит что)

Ключевой принцип

Видимость комментария определяется записями в CommentRecipients. Если для пользователя нет записи — он не видит комментарий в ленте.

Типы адресатов

Флаг Значение
IsRealRecipient = true Явный адресат (выбран пользователем при отправке)
IsRealRecipient = false Неявный адресат (подписчик задачи)
IsCopyRecipient = true В копии
HasToAnswer = true Требует ответа (вопрос)

Алгоритм расчёта адресатов

Файл: TCClassLib/Comments.cs:1199-1810 (метод AddSilentCommentCore)

1. Начальные адресаты из API:
   ├── recipientsIds (явные адресаты)
   ├── recipientGroupIds (группы)
   ├── copyRecipientsIds (копия)
   └── copyRecipientGroupIds (группы в копии)

2. Исключить системного робота (settings.SystemRobotId)

3. Если >1 адресат И отправитель не должен быть в списке:
   └── Удалить отправителя из адресатов

4. Smart Events: BeforeSendNotification
   └── Для каждого подписчика: если SmartCancelException → исключить из уведомлений

5. Проверка типа комментария:
   ├── СИСТЕМНЫЙ + HideSystemComments = true → ОЧИСТИТЬ ВСЕХ адресатов
   └── СИСТЕМНЫЙ + HideSystemComments = false → Исключить завершивших исполнителей (multi-finish)

6. ForwardCommentsToAllHelpers:
   └── Если включено И хотя бы один адресат — исполнитель → добавить ВСЕХ исполнителей

7. Smart Events контексты:
   ├── NoCommentRecipientsForNonUserComments → очистить для системных
   └── NoCommentRecipientsForFiles → очистить для файловых

8. Подписка на тип комментария (UserLentaCommentTypesCache):
   └── Оставить только тех, кто подписан на данный TypeID

9. Настройки подкатегории:
   ├── EnableComments = false → ОЧИСТИТЬ ВСЕХ
   └── OneFMainVisibilityMode = ShowOnlyTasks / ShowNothing → ОЧИСТИТЬ ВСЕХ

10. Для каждого адресата определить:
    ├── IsUnread (через NotificationSettings + AutoReadComments + тип)
    ├── IsRealRecipient / IsCopyRecipient
    └── HasToAnswer

11. → INSERT INTO CommentRecipients

Изменение адресатов при редактировании

При редактировании комментария можно изменить список адресатов. Система пересчитывает записи в CommentRecipients и корректирует push-уведомления:

  • Добавленные адресаты получают push о комментарии и начинают видеть его в ленте.
  • Исключённые адресаты получают сигнал удаления push (уведомление исчезает из центра уведомлений на устройстве), и комментарий перестаёт быть видимым для них в ленте.
  • При сужении «Всем» до конкретного пользователя push остаётся только у указанного.

Подробнее о поведении push-уведомлений при изменении адресатов — см. notifications/business.md → Редактирование адресатов комментария.

Настройки подкатегории, влияющие на видимость

Настройка Тип Влияние
HideSystemComments bool Если true — все адресаты системных комментариев очищаются. Комментарий записывается, но никто его не видит в ленте
ForwardCommentsToAllHelpers bool Если true — при адресации любому исполнителю, автоматически добавляются все исполнители задачи
EnableComments bool Если false — комментарии отключены, адресаты не создаются
OneFMainVisibilityMode enum ShowOnlyTasks/ShowNothing — адресаты не создаются
IsDeleteUserCommentsForbidden bool Запрещает удаление пользовательских комментариев

Видимость при чтении ленты

Процедура ShowCommentsFeed

При чтении ленты (GET /api/comments/lenta) процедура ShowCommentsFeed отбирает комментарии по: 1. CommentRecipients.UserId = @CurrentUserId — только записи для текущего пользователя 2. Comments.IsDeleted = 0 — не удалённые 3. Фильтры по дате, типу, задаче, тредам

Когда пользователь может НЕ видеть отправленный комментарий

Причина Как диагностировать
Нет записи в CommentRecipients для этого UserId SELECT * FROM CommentRecipients WHERE CommentID = @id AND UserID = @uid
HideSystemComments = true для системного типа Проверить SubcatSettings и TypeID комментария
Подписка на тип отключена в настройках пользователя Проверить UserLentaCommentTypes
Smart Event заблокировал (BeforeSendNotification) Проверить наличие smart-событий в подкатегории
EnableComments = false в подкатегории Проверить настройки подкатегории
OneFMainVisibilityMode = ShowOnlyTasks/ShowNothing Проверить настройки подкатегории
Комментарий удалён (IsDeleted = 1) SELECT IsDeleted FROM Comments WHERE CommentID = @id
Пользователь не подписчик задачи SELECT * FROM MailSubscribersUsers WHERE TaskID = @tid AND UserID = @uid

Автопрочтение (AutoRead)

Комментарий может быть автоматически помечен как прочитанный (IsUnread = false) при: 1. User.AutoReadComments = true — персональная настройка пользователя 2. Пользователь в списке notToNotifyUserIds (отфильтрован Smart Event) 3. Для событий календаря — если CalendarEnableNotify = false 4. Персональные настройки уведомлений (NotificationSettings)

Закреплённые комментарии

В чате задачи участники с соответствующим правом могут закреплять сообщения для общего внимания. Закреплённые сообщения отображаются в отдельной панели над лентой.

Признак закрепления хранится в поле Comments.IsPinned. Тредовые закрепы (ThreadCommentId != NULL) не попадают в панель общего чата — они видны только внутри обсуждения.

При закреплении или откреплении создаётся системный комментарий (TypeID=3, EventID=199/200) с текстом «@User закрепил(а) сообщение» / «открепил(а) сообщение». Push/email-уведомления для этого события подавляются — запись появляется только в ленте.

Все участники чата получают real-time обновление через SignalR-событие PinnedCommentChanged с флагом isPinned.

Треды (обсуждения)

Тред = корневой комментарий + дочерние ответы. Статусы: открыт / закрыт. В закрытом треде публикация запрещена (кроме транскрибации).

Хранение в БД

Поле Comments.ThreadCommentId (INT, FK → Comments.CommentID):

Тип комментария ThreadCommentId
Обычный (вне треда) NULL
Корневой комментарий треда = CommentID (self-reference)
Ответ в треде = CommentID корневого комментария

API endpoints

Метод Endpoint Назначение
POST /api/comments/threads/add Создать тред (StartThreadRequestDto: TaskId, ThreadCaption, CommentText)
POST /api/comments/addThreadCommentId) Ответ в треде
POST /api/comments/{taskId}/threads Список тредов задачи
PUT /api/comments/threads/{threadCommentId}/close Закрыть тред
PUT /api/comments/threads/{threadCommentId}/activate Переоткрыть тред
PUT /api/comments/threads/{threadCommentId}/edit Редактировать заголовок
DELETE /api/comments/threads/{threadCommentId} Удалить тред

SmartAction

StandardAction 183 = CreateThread — доступен из SmartActions и Lua.

Хранимые процедуры

ShowTaskThreadsFeed — список тредов задачи с метаданными (автор, дата, количество ответов, статус).

Автозакрытие

CloseStaleThreadsJob — закрывает треды через 14 дней неактивности. Закрытый тред можно переоткрыть через /activate.

Права

Действие Кто может
Создать тред Любой участник задачи
Закрыть Автор, Owner, Admin, Moderator
Удалить Автор (свой), Owner/Admin/Moderator (любой)

Вопрос-ответ

Трёхуровневая модель

Поле Таблица Уровень Назначение
NeedsAnswer Comments Глобальный Комментарий — вопрос
IsAnswered Comments Глобальный На вопрос дан ответ
HasToAnswer CommentRecipients Per-recipient Конкретный получатель должен ответить
NeedsAnswer CommentRecipients Per-recipient Денормализованная копия из Comments (v2.262)
IsAnswered CommentRecipients Per-recipient Денормализованная копия из Comments (v2.262)

Жизненный цикл

  1. Пометка как вопрос (MarkAsQuestion): Comments.NeedsAnswer = true, Comments.IsAnswered = false, CommentRecipients.HasToAnswer = true (для IsRealRecipient и не-IsCopyRecipient)
  2. Ответ (MarkAsAnswered): Comments.IsAnswered = true, CommentRecipients.HasToAnswer = false (у всех)
  3. Не-мой-вопрос (MarkAsNotMyQuestion): CommentRecipients.HasToAnswer = false только у одного получателя; если ни у кого не осталось HasToAnswer = trueComments.IsAnswered = true
  4. Ответ на вопрос текстом (reply): автор нового комментария автоматически помечает вопрос как отвеченный

Денормализация (v2.262)

NeedsAnswer и IsAnswered скопированы из Comments в CommentRecipients и CommentRecipientsArchive для ускорения запросов.

Инвариант: SetIsAnswered и MarkAsQuestion обновляют и Comments, и CommentRecipients атомарно. CommentRecipientsArchive не обновляется — расхождение ожидаемо для строк, заархивированных до изменения статуса.

Архивация и вопрос-ответ

sp_tc_ArchiveCommentRecipientsJob переносит реципиентов из CommentRecipients в CommentRecipientsArchive.

Критическое правило: комментарий с неотвеченным вопросом (NeedsAnswer = 1 AND IsAnswered = 0) не должен архивироваться, пока хотя бы у одного реципиента вопрос открыт. Условие архивации должно проверять все строки CommentID, а не отдельные.

Баг (обнаружен 2026-02-11, задача ): процедура использовала select distinct с per-row условием — одна строка автора (у которого всегда NeedsAnswer = 0) протаскивала в архив весь CommentID. Результат: 154 неотвеченных вопроса на деве со всеми реципиентами в архиве.

CommentRecipientsArchive не имеет колонки HasToAnswer — при чтении из архива подставляется cast(0 as bit), т.е. архивные записи не участвуют в подсчёте «кому нужно ответить».

Автодетекция вопроса на фронтенде

Как работает

При каждом изменении текста комментария PostCommentViewService.setCommentText() автоматически выставляет или снимает чекбокс «Нужен ответ» (NeedsAnswer).

Условия срабатывания: - пользователь не переключал чекбокс вручную в текущей сессии набора (firstQuestionByHandToggle = false) - категория задачи разрешает вопросы (commentQuestionsAllow = true) - выбран хотя бы один получатель ИЛИ открыт приватный чат

Если все условия выполнены — вызывается isQuestionText(text).

Алгоритм isQuestionText(text)

Перед проверкой из текста вырезаются HTML-теги, code blocks и inline code. Затем применяются 4 правила (достаточно одного):

# Правило Примеры
1 Символ ? в тексте «Это работает?», «Когда будет готово?»
2 Вопросительные слова в начале строки (RU + EN) «Когда будет готово», «Как это работает», «Could you check», «Is there a way»
3 Императивные просьбы (RU) «Подскажите», «Прошу уточнить», «Сообщите»
4 Императивные просьбы (EN) «Please let me know», «Kindly confirm»

Реализация: libs/utils/src/lib/question-detector.ts, экспортируется из libs/utils/src/index.ts.

Точка вызова в коде

apps/spa/src/app/common/components/post-comment/post-comment-view.service.ts, метод setCommentText():

const excludeUrls = excludeAllUrlsFromText(text || '');
const hasQuestion = autoSetCommentAsQuestion && isQuestionText(excludeUrls);

Сначала из текста удаляются URL (excludeAllUrlsFromText), затем результат передаётся в isQuestionText.

Настройка пользователя

Настройка «Не помечать сообщение как вопрос автоматически» (профиль → Прочее, поле NotSetNeedAnswerCheckboxAutomatically в Users) полностью отключает автодетекцию — autoSetCommentAsQuestion выставляется в false, и вызов isQuestionText() не происходит.

При ручном переключении чекбокса пользователем (byHand = true) настройка не мешает.

Обзор

При создании или редактировании комментария система автоматически извлекает из текста два типа ссылок:

Тип Как распознаётся Пример Хранение
Task link (ссылка на задачу) Regex #(\d+) в тексте #12345 Таблица CommentTaskLinks
URL link (внешний URL) Regex https?://... https://example.com/page Таблица CommentLinks

В LinkType enum зарезервированы также типы User и Comment, но парсер их пока не использует.

Режимы сохранения

Клиент может передать ссылки двумя способами: - Merge (Replace = false) — добавляет/обновляет ссылки, не удаляя существующие. - Replace (Replace = true) — полная замена: ссылки, отсутствующие в новом наборе, удаляются.

Если клиент не передаёт ссылки — система парсит текст автоматически (автопарсинг).

Автопарсинг vs ручные ссылки

  1. Текст комментария проходит HTML-decode + strip HTML (чтобы regex не цеплял color="#..." и подобные атрибуты).
  2. LinkParser.ParseLinks() извлекает task-ссылки и URL.
  3. Для task-ссылок проверяется существование задачи; Title = текст задачи (до 200 символов).
  4. Для URL-ссылок Title = сам URL (до 200 символов).
  5. Дедупликация по LinkedTaskId (task) и Url (url, case-insensitive).
  6. Если передан флаг hideAutoParsedLinks = true — автоматически найденные ссылки получают IsHidden = true и не отображаются в ленте.

Флаг IsHidden

Ссылки с IsHidden = true: - Не попадают в CommentLinksCache (пакетное чтение). - Не возвращаются при пакетном чтении ленты. - Доступны через прямой запрос по CommentId (для редактирования).

Цель: скрыть автоматически распарсенные ссылки при ручном управлении (когда пользователь сам выбирает, какие ссылки показывать).

OpenGraph-превью (сниппеты)

Для URL-ссылок доступен парсинг OG-метаданных (эндпоинт POST /api/comments/links/parse-preview): - Загружается HTML страницы. - Извлекаются og:title, og:image, og:type, og:url и другие метатеги. - Картинка-превью скачивается и возвращается как Base64. - Метаданные сохраняются в JsonAttributes ссылки. - Картинка может быть сохранена как PreviewFileId (FK на FileStorageFiles).

Ограничение: для парсинга внешних URL необходим параметр Configuration.OutboundHttpAllowed = true. При проброске запроса используются User-Agent и sec-ch-* заголовки текущего пользователя (или fallback "1f" / https://1forma.ru/).

Фоновый парсинг (ParseCommentLinksJob)

Задача

Quartz-джоба для backfill-парсинга ссылок в исторических комментариях:

  1. Читает позицию последнего обработанного CommentId из CustomSettings (ключ ParseCommentsLinksJob_LastParsedCommentId).
  2. Выбирает батч комментариев, в тексте которых есть # или http, у которых ещё нет записей в CommentLinks/CommentTaskLinks.
  3. Пороговая дата: Date > 2020-01-01 — более старые комментарии не обрабатываются.
  4. Для каждого комментария вызывает CommentLinksService.ParseCommentLinksAndUpdate().
  5. Ошибки отдельных комментариев не прерывают процесс — агрегируются и логируются.
  6. При массовом парсинге подавляется отправка Rebus-сигналов инвалидации кеша (SuppressCacheCommandScope).

Связанные настройки

Настройка Где Влияние
Параметр действия «Показывать найденные ссылки» SmartAction «Написать комментарий» Управляет отображением автоматически найденных ссылок в UI
OutboundHttpAllowed Конфигурация сервера Разрешает/запрещает парсинг OG-превью

Права доступа

  • Запись/редактирование ссылок: только автор комментария + доступ к задаче.
  • Чтение ссылок: любой, кто имеет доступ к задаче.

Известные проблемы

Проблема Задача Суть
Размножение ссылок При повторном вызове merge ссылки дублировались
Timeout парсинга ParseCommentLinksJob таймаутился на больших батчах
Ошибки парсинга Ошибки при парсинге некоторых URL
Пропажа файлов Привязка файлов-превью конфликтовала с файлами комментария

Оценки исполнителей

В категориях с включённым режимом оценки исполнителей заказчик после завершения задачи может оценить работу каждого исполнителя по пятибалльной шкале.

Аспект Правило
Кто оценивает Заказчик задачи
Кого оценивает Каждый исполнитель (кроме себя, если заказчик сам является исполнителем)
Обязательность комментария Заказчик обязан прокомментировать оценку
Видимость оценок Открытые (всем) или закрытые (только с правом ViewSecretPerformerPoint)
Где отображаются Комментарии к оценке показываются в ленте комментариев задачи

Оценка проставляется из карточки задачи или из ленты задач и комментариев.

Геолокация в комментариях

При включённой геолокации в профиле пользователя и выданном разрешении на мобильном устройстве, в мобильном приложении 1F Mobile при отправке комментария передаются координаты местоположения. В веб-интерфейсе в контекстном меню такого комментария доступен пункт Посмотреть в Google картах — открывается карта с указанием места отправки и точных координат.

⚠️ Требования: разрешение на геоданные на устройстве + включённая настройка геолокации в профиле пользователя.

Опросы (SurveyJS)

В специально настроенных категориях поддерживается проведение опросов и тестирований через интеграцию с SurveyJS.

Создание опросов

Опросы создаются в визуальном редакторе (конструктор SurveyJS) в категории-каталоге опросов. Поддерживаются: - Текстовые поля, чекбоксы, радиокнопки, загрузка файлов и другие типы вопросов - Логика прохождения (разветвление, пропуск вопросов) - Таймер на прохождение - Отображение правильных/неправильных ответов - Перемешивание вопросов для тестирований - JSON-редактор для гибкой настройки

Прохождение опроса

Задачи на прохождение создаются для каждого участника. В карточке задачи доступна кнопка Пройти опрос.

⚠️ В задаче обязательно должен быть назначен ответственный исполнитель — прохождение разрешено только ему. Каждая задача предназначена для одного исполнителя.

Режимы сохранения: - По умолчанию — результат сохраняется только при полном завершении опроса и нажатии Готово - При разрешённом частичном прохождении — каждый ответ сохраняется автоматически, при повторном входе пользователь продолжает с места остановки

После завершения повторное прохождение недоступно.

Результаты

Результаты отображаются в карточке задачи в виде древовидной таблицы (вопросы сгруппированы по страницам). По умолчанию необязательные вопросы без ответа скрыты. Также доступны сводные таблицы и графики.

Прямая ссылка на результаты: /spa/survey/result/{TaskID}.

Опросы могут быть персонализированными и анонимными.

Лента сообщений в задаче

Вкладки ленты

Вкладка Что показывает
Лента (по умолчанию) Все типы событий и сообщения с учётом настроек пользователя
Сообщения Только пользовательские сообщения
Журнал Системные события (без сообщений пользователей)
Все Все события вне зависимости от настройки «Видно в ленте»

Источник сообщений

По кнопке меню над лентой выбирается источник: - Текущая задача (по умолчанию) — сообщения текущей задачи - Из подзадач — сообщения подзадач уровнем ниже - Используется — сообщения задач, где данная задача выбрана в ДП

Если выбран источник, отличный от умолчания, на кнопке отображается синяя точка.

Кнопка «Отметить всё как прочитанное» автоматически прочитывает все новые сообщения в ленте задачи.

Сортировка ленты

По умолчанию комментарии отображаются в обратном хронологическом порядке (свежие сверху). Стрелки в шапке ленты переключают порядок на прямой и обратно.

Фильтрация по типам сообщений

Настройка Все типы событий на вкладке «Уведомления» в профиле пользователя:

  • Включена — в ленте отображаются комментарии всех типов, независимо от персональных настроек.
  • Выключена — отображаются только типы, отмеченные флажком «Видно в ленте».

Связана с настройкой профиля «Фильтровать комментарии в задаче согласно настройкам ленты»: настройки фильтрации действуют на все задачи в категории; в разных категориях могут отличаться.

Фильтрация влияет только на отображение — комментарии всех типов всегда публикуются и хранятся. Изменив настройки, можно увидеть ранее скрытые комментарии.

Адресация сообщений

Каждое сообщение отправляется в рамках задачи или чата. У сообщения один автор и несколько адресатов.

Адресат Поведение
Явный (поле «Кому») Сообщение помечается непрочитанным у адресата
Копия Получает сообщение, но «непрочитанным» не помечается
Всем Отправить всем подписчикам задачи
Никому Сообщение всегда попадает в список непрочитанных, уведомление — по настройкам категории

По умолчанию в выборе адресатов отображаются только подписчики задачи. Для выбора других пользователей — ввести имя в строку поиска. Поиск работает по имени, фамилии и основной должности.

Выбор групп в поле адресатов доступен только сотрудникам компании и администраторам категории.

Сортировка результатов поиска адресатов: 1. Подписчики и частые собеседники (по рейтингу общения) 2. Сотрудники той же оргструктуры 3. Сотрудники компании (выше внешних пользователей)

Отправка без адресата невозможна для вопросов — кнопка «Как вопрос» блокируется, пока не указан адресат.

@-упоминание (визитка)

Символ @ после пробела, начала строки или открывающей скобки раскрывает список подписчиков. Выбор заменяет @... на тег @user{UserID} и добавляет пользователя в поле «Кому». После отправки тег заменяется на кликабельное имя, которое открывает краткую карточку пользователя.

Индикаторы сообщений

Набора текста

Когда собеседник набирает сообщение, над строкой ввода появляется «ФИО печатает...».

Отправки и прочтения

Индикатор Состояние
✓ (одинарный) Сообщение доставлено на сервер, никем не прочитано
✓✓ (двойной, серый) Прочитано кем-то из адресатов, но не всеми
✓✓ (двойной, синий) Прочитано всеми получателями

Индикатор не отображается для сообщений без адресата и для чужих сообщений.

Просмотр кто и когда прочитал — пункт Прочтения в контекстном меню. Показываются первые 5 пользователей, кнопка «Ещё» открывает полный список.

Черновик

При закрытии задачи недописанный черновик сохраняется. При повторном открытии текст восстанавливается.

Форматирование текста

При выделении текста в поле ввода появляется панель форматирования. Также доступен markdown:

Разметка Отображение
**текст** Жирный
__текст__ Курсив
~~текст~~ Зачёркнутый
((текст)) Подчёркнутый
#номер / №номер Активная ссылка на задачу
+#номер Установить связь с задачей
`код` Код
[текст](ссылка) Активная ссылка

Горячие клавиши форматирования: Ctrl/Cmd+B (жирный), Ctrl/Cmd+I (курсив), Ctrl/Cmd+U (подчёркивание), Ctrl/Cmd+Shift+X (зачёркивание). Повторное применение снимает форматирование.

Вставка из внешних источников (Ctrl/Cmd+V): система автоматически приводит Markdown и HTML (Word, веб-страницы) к единому виду — заголовки → жирный, маркированные списки, ссылки, переносы строк сохраняются.

Превью ссылок: при добавлении ссылки на задачу, чат, пространство — автоматически формируется блок-превью (аватарка, текст, категория, номер). При добавлении внешнего URL — превью-карточка с заголовком и описанием из OG-метаданных.

Сообщения только из эмодзи (один или несколько, в т.ч. через пробел) отображаются увеличенным шрифтом.

Действия с сообщениями

Контекстное меню сообщения

Открывается по ПКМ на сообщении (единое для текста, файла и превью):

Пункт Описание
Ответить Ответить на конкретное сообщение
Ответить всем Ответить всем адресатам
Как вопрос Пометить сообщение как вопрос (у адресата появляется счётчик)
Как отвеченный Снять вопрос (только для своих сообщений)
Как непрочитанный Вернуть сообщению статус непрочитанного
Начать обсуждение Создать тред по сообщению
В избранное Добавить сообщение в избранное (появляется в ленте на вкладке «Избранные»)
Изменить Редактировать текст и адресатов (если разрешено категорией)
Переслать Переслать в текущую задачу, другую задачу или чат
Выбрать Войти в режим массового выбора сообщений
Прочтения Кто и когда прочитал сообщение
Создать → Задачу / Подзадачу Создать задачу из текста сообщения (с опцией копирования файлов)
Ответы Кто ответил на вопрос (только для вопросов с несколькими адресатами)
Удалить Удалить сообщение (только своё; чужое — только для администраторов категории)

Также: Реакции — короткий список эмодзи вверху меню; стрелка раскрывает полный список.

Поиск в ленте

Кнопка поиска в правой части блока ленты. Поиск работает по тексту, в т.ч. по названиям обсуждений.

Вложение файлов в сообщение

Кнопка прикрепления в поле ввода предлагает варианты:

Вариант Поведение
Файлы Без обработки. Передача в исходном виде, без сжатия. Используется для изображений с мелким текстом и видео для дальнейшей обработки
Фото или видео Медиа-режим: изображения и видео автоматически сжимаются и оптимизируются. Удобно для быстрого просмотра
Создать опрос См. раздел «Опросы»

Файлы можно перетащить в поле ввода с компьютера или использовать горячие клавиши Ctrl+U (Windows) / Ctrl+Cmd+U (Mac). После добавления в поле автоматически появляется фокус — можно сразу набирать текст.

Файлы 0 байт не загружаются: «Файлы не переданы или не содержат данных». Сообщение, состоящее только из вложения без текста, нельзя редактировать.

Если в задаче запрещено вложение файлов, кнопка прикрепления и связанные возможности скрыты.

Действия с файлом в сообщении

Контекстное меню по ПКМ на файле:

Пункт Описание
Изменить Редактирование (только для текстовых вложений)
Скачать / Скачать все Скачать конкретный файл / все файлы из сообщения
Открыть в папке диска Перейти к папке Диска (только для файлов из Диска — отмечены иконкой)
Просмотр версий Окно истории версий
Открыть актуальную версию Только если у файла есть более новая версия
Скопировать ссылку Полный URL файла
Удалить Убрать файл из сообщения

Контекстное меню также доступно в режиме редактирования сообщения.

Перезапись и просмотр

При загрузке файла с именем, совпадающим с уже существующим в задаче, выполняется автоматическая перезапись — старая версия отображается в ленте с зачёркнутым названием.

Просмотр или скачивание файла из нового сообщения в ленте приравнивается к прочтению сообщения.

Видео в превью ленты: - Размер ≤ 50 МБ — автоматически воспроизводится при клике на превью прямо в ленте. - Размер > 50 МБ — клик открывает отдельное окно просмотрщика.

Пересылка сообщений

Вариант Поведение
В текущую задачу Переходит к полю написания с закреплённым исходным сообщением
Выбрать Модальное окно: поиск по номеру/тексту задач и чатов, фильтр «Только задачи» / «Только пользователи»
Из задачи в чат После выбора открывается вид чата
Из чата в задачу После выбора открывается форма задачи

Пересланное сообщение публикуется как новое (реакции и служебные данные не переносятся). При пересылке с вложением можно выбрать — пересылать полностью или только файл.

⚠️ Из зашифрованной задачи пересылка возможна только в текущую задачу.

Массовая обработка

Режим выбора активируется через пункт Выбрать в контекстном меню. Отмеченные сообщения можно переслать или удалить. Выход — кнопка «Отмена» в контекстном меню.

Реакции (эмоции)

Реакция — emoji-отметка на комментарии другого пользователя. Доступность определяется настройкой категории «Разрешить пользовательские реакции».

Установка и удаление

В контекстном меню сообщения (по ПКМ) в верхней части — короткий список реакций. Стрелка вниз раскрывает полное меню; если реакции уже использовались, в начале списка отображаются недавно использованные.

Правила: - Один пользователь — одна реакция на сообщение. Повторное нажатие на ту же реакцию убирает её. - При установке реакции на новое сообщение оно автоматически отмечается прочитанным. - При пересылке комментария реакции исходного сообщения не копируются. - Реакции не влияют на видимость комментариев.

Отображение

Реакция отображается под сообщением аватаром поставившего пользователя. Если одну и ту же реакцию оставило больше двух человек — вместо аватаров выводится число.

При наведении на реакцию появляется всплывающее окно со списком пользователей. Клик по нему открывает модальное окно «Все реакции» — там можно переключаться между разными реакциями и видеть авторов каждой; иконка профиля рядом с пользователем открывает его карточку.

Кнопка emoji в поле ввода

Если в категории включены реакции, в панели написания сообщения появляется кнопка emoji. Меню вставляет символ в текущую позицию курсора и не закрывается автоматически — можно вставить несколько эмодзи подряд. Кнопка доступна только на десктопе.

Реализация

Агрегат реакций доступен через EmojiReactionsService. Вид реакций может отличаться в зависимости от устройства.

Пересылка комментариев

  • POST /api/comments/forward — пересылка в другую задачу
  • Файлы копируются в целевую задачу
  • Создаётся новый комментарий в целевой задаче с Event.OnForwardComment

Удаление комментариев

Условие Разрешено
Пользовательский комментарий ДА (если не IsDeleteUserCommentsForbidden)
Системный комментарий НЕТ (без bypass)
Тред Автор (свой), Owner/Admin/Moderator (любой)

Типичные тикеты и диагностика

"Не вижу отправленный комментарий"

Причины (в порядке частоты): 1. Комментарий системный + HideSystemComments включено 2. Пользователь не подписчик задачи → нет записи в CommentRecipients 3. Тип комментария отключён в настройках подписки пользователя 4. Smart Event заблокировал уведомление 5. EnableComments выключено / OneFMainVisibilityMode скрывает

Диагностика:

-- 1. Проверить наличие комментария
select
        c.CommentID,
        c.TaskID,
        c.UserID,
        c.TypeID,
        c.IsDeleted,
        c.Content,
        c.Date
from Comments c
where c.CommentID = @commentId;

-- 2. Проверить адресатов
select
        r.UserID,
        u.FullName,
        r.IsUnread,
        r.IsRealRecipient,
        r.IsCopyRecipient,
        r.HasToAnswer
from CommentRecipients r
        join Users u
            on u.UserID = r.UserID
where r.CommentID = @commentId
order by r.UserID;

-- 3. Проверить подписку на задачу
select
        s.UserID,
        s.IsDeleted
from MailSubscribersUsers s
where s.TaskID = @taskId and
      s.UserID = @userId;

-- 4. Проверить настройки подкатегории
select
        sc.SubcatID,
        sc.HideSystemComments,
        sc.ForwardCommentsToAllHelpers,
        sc.EnableComments
from Subcategories sc
where sc.SubcatID = (
        select t.SubcatID
        from Tasks t
        where t.TaskID = @taskId
);

Чеклист: 1. Комментарий существует в Comments? Не удалён? 2. Есть запись в CommentRecipients для данного пользователя? 3. Если нет — проверить TypeID (системный?) + HideSystemComments 4. Если нет — проверить подписку пользователя на задачу 5. Если нет — проверить Smart Events в подкатегории 6. Если есть, но IsUnread = false — проверить AutoRead настройки 7. Если всё корректно в БД — проверить доставку SignalR (NewComment)

"Комментарий виден заказчику, но не исполнителю"

Частая причина: ForwardCommentsToAllHelpers выключено и комментарий адресован конкретному исполнителю, а не всем.

"Системные комментарии не отображаются"

Причина: HideSystemComments = true в настройках подкатегории. Системные комментарии записываются, но адресаты не создаются.

Детальная документация

  • database.md — автогенерация комментариев: CommentAutoContent, EventID → таблицы-логи, процедуры очистки, EXISTS vs функция (16KB)

SQL-справочники

  • comments.md — таблица Comments, TypeID (1-20), CommentRecipients, типовые запросы

Перекрёстные ссылки

  • docs/domains/comments/backend.md — контроллеры, сервисы, маршрут данных
  • docs/domains/comments/data-flow.md — сквозные сценарии с диагностикой
  • docs/domains/chat/business.md — типы чатов (сообщения чата = комментарии)
  • docs/reference/database/comments.md — структура таблиц