Чат по TU
Списки повідомлень, прив’язаних до однієї TU.
Пункти які проговорили перед реалізацією:
- Доступ до чатів - оператор, тімлід, топ, КМ
- Привязка по ТЮ, не по клієнткам
- Окремий розділ - Реалізація як у фаворітах - є списки профілів
- Логіка прочитаних - як в тікетах
- Нові повідомлення рахуються тоді, як роль була назначена на ТЮ
- Відображати весь список ТЮ для всіх доступів, потім добавимо фільтр (по тімлідам) якщо потрібно буде
- Логіка тегів. Глобальні теги. All. У кожної ролі є список тегів, які впливають на логіку unviewed. Наприклад, “content, presents”.
- Повідомлення відправляється обовязково з тегом, тег у вікні набору тексту
- Логіка відповісти на повідомлення як в тг. При відповіді тег дублюється.
- Поле ввода, можна вставляти в скриншоті. Файли як в тікетах.
- В текст можна вставляти ссилки
- Відображати хто відправник повідомлення.
- Підгрузка повідомлень по курсору
Питання:
- назва розділу
- правила, за якими для ролей буде налаштовано обмежений список обов’язкових до перегляду тегів
Суть
Чат створюється один на пару Family + TU. Повідомлення зберігаються так, щоб завантажувати історію курсором. За змістом повідомлення повторює коментар тікета.
Поля
Чат
- id — внутрішній ID чату; використовується в повідомленнях як
tuChatIdта для socket-кімнати; - family — Family TU, наприклад ‘golden’
- tuId — анкета TU.
- createdAt, updatedAt — час створення й останньої зміни чату.
Повідомлення
- id — ID повідомлення;
- tuChatId — ID чату, до якого належить повідомлення; генерується автоматично;
- user —
{ userId, role }працівника та його роль на момент відправлення; генерується автоматично; - text — текст повідомлення;
- tag — обов’язкове поле; тег.
- answerTuChatMessageId — опційний ID повідомлення, на яке створюється відповідь; тег батьківського повідомлення не успадковується автоматично;
- attachments — масив вкладень. Кожне вкладення містить
type,originalType,size,url, опційніpreviewUrlіmediumUrl, а такожcreatedAt; - reaction — масив працівників, які переглянули повідомлення:
{ userId, role, createdAt }; - createdAt — час відправлення; генерується автоматично;
- updatedAt — час редагування; генерується автоматично;
- isDeleted — поле, яке показує, що повідомлення видалене.
У HTTP- і socket-відповідях повідомлення додатково містить збагачену інформацію автора, id для вкладень і реакцій, а також canEdit і canDelete для поточного працівника.
Логіки для повідомлень
- Створення - доступне всім ролям, що мають доступ до чату.
- Редагування - автор може змінювати лише текст повідомлення; видалене повідомлення редагувати не можна.
- Видалення - доступне лише автору протягом 1 дня. Повідомлення не видаляється з переписки, а отримує
isDeleted: true. - Вкладення передаються разом зі створенням повідомлення в полі
files; окремого маршруту для додавання, редагування чи видалення вкладень немає. - Контролер перевіряє, що під час створення передано
textабо хоча б один файл.
Реалізовано
Доступ і дані
- Доступ мають
client_manager,operator,supervisorіtopManager. - Відповідь повідомлення містить автора, вкладення, реакції перегляду, ознаки
isDeleted,canEditіcanDelete. - Завантаження списку повідомлень ініціатором автоматично переглядає повідомлення, і відправляється socket-подія
tuChat:message:viewed.
Теги
Теги будуть отримуватись http запитом. Теги підбираються для ініціатора запиту.
Cокет підключення
Socket доставляє лише зміни в реальному часі. API та база лишаються джерелом правди для історії, доступу і непрочитаних.
Архітектура: TuChatService (REST-мутації) публікує доменну подію в Event Bus (in-process EventEmitter, з можливістю заміни на Redis pub/sub без зміни сервісу, якщо з’явиться кілька інстансів застосунку) → окремий delivery-шар підписаний на шину, резолвить кімнату й робить io.to(room).emit(...). Сервіс не звертається до socket.io напряму - хто доставляє подію й куди, вирішує delivery-шар, а не бізнес-логіка.
Кімната: одна кімната на чат - tuChat:{tuChatId}, а не персональна кімната користувача. Клієнт явно робить join при відкритті чату конкретної TU і leave при закритті/переході в інший чат.
Авторизація: при join перевіряється, що роль користувача має доступ до чату; додаткова перевірка assignment-ів для конкретної пари Family+TU наразі не реалізована.
Події:
tuChat:message:created— повнийTuChatMessageResponsetuChat:message:updated— повнийTuChatMessageResponsetuChat:message:deleted— повнийTuChatMessageResponsetuChat:message:viewed— лише дельта реакції:{ messageIds, userId, role, createdAt }, не весь message. MessageIds - тому що юзери підгружають повідомлення по курсору, і viewed часто буде пачками
Гарантії доставки: WS ненадійний за визначенням, тож:
- кожна подія самодостатня - клієнт просто мержить у стан, без додаткового запиту;
- при (пере)підключенні клієнт завжди дотягує пропущене звичайним
getMessagesз курсором - WS ніколи не єдине джерело даних; - дедуплікація на клієнті за
id+updatedAt(та сама подія може прийти повторно після реконнекту).
Масштабування: поки один інстанс застосунку - вистачає in-process EventEmitter. З появою кількох інстансів подія з Event Bus публікується в Redis (чи інший pub/sub), delivery-шар лишається тим самим кодом.
Приклад реалізації
- Працівник бачить назву розділу, бачить к-сть unviewed повідомлень (логіка unviewed)
- Працівник відкриває сторінку зі списками TU, бачить к-сть unviewed біля кожної TU
- Працівник клікає на чат по конкретну TU. Повертається чат та списки повідомлень за курсором. Наприклад, 20 повідомлень відсортовані по даті створення.
- ?
Сокет підключення до чату - Логіка перегляду повідомлень: реалізована під час
getMessages, а не окремим запитом фронтенду. - Виклик
логік для повідомлень
Логіка unviewed
Інформація:
В повідомленнях будуть проставлятись tag. Для працівників певних ролей будуть проставлятись списки tags, які вони обов’язково мають переглянути (якщо tags не виставлений, то працівник має переглянути всі повідомлення (за тей проміжок часу, коли у нього була TU)
Завдання:
Відображати цифрою к-сть unviewed біля:
(замальовувати червоним кольором (?))
- назви розділу, к-сть по всім TU;
- кожної TU, коли відкрили розділ
Приклад реалізації: Фронтенду буде повертатись списки unviewed messages, щоб він в режимі реального часу при перегляді повідомлень мутував фронт.
Як буде визначатись, що message unviewed:
- повідомлення потрібного tag для цього працівника
- повідомлення не переглянуте працівником
- createAt message входить в діапазон коли TU була на працівнику
Зв’язки
- TU — визначає належність чату та доступ.
План для фронтенду
1. Індикатор у меню
Під час завантаження меню розділу чату запитати getUnviewedMessageIds для поточної family.
- Використати кількість
messageIdsяк лічильник непрочитаних для пункту меню, як у тікетах. - Зберігати множину
messageIds, а не лише число: це дозволить коректно прибирати конкретні повідомлення після перегляду та socket-події viewed.
2. Список TU у розділі
Після кліку на розділ завантажити getUnviewedByTu для поточної family.
- Відобразити повернуті TU та кількість
messageIdsбіля кожної з них. - Бекенд повертає також TU без непрочитаних повідомлень, якщо вони призначені поточному працівнику.
- Під час роботи зі списком синхронізувати лічильники з глобальним станом непрочитаних повідомлень.
3. Відкриття чату TU
Після кліку на TU виконати getMessages з family, tuId, а для наступних сторінок передавати повернений cursor.
- Об’єднувати сторінки за
message.id; не дублювати повідомлення під час повторного запиту або socket-події. - До завантаження історії або паралельно з ним надіслати socket-подію
tuChat:joinз{ family, tuId }. - У callback
tuChat:joinзберегтиdata.tuChatId; він потрібен дляtuChat:leave. - Під час закриття чату чи переходу на іншу TU надіслати
tuChat:leaveзtuChatId, відписатися від обробників подій.
4. Дії у відкритому чаті
- Перед створенням повідомлення запитати
getTagsByRole, взяти список для поточної ролі та показати його в селекторі тегів. - Для створення використати
createMessage; для текстового редагування -updateMessage; для soft delete -deleteMessage. - Відображати кнопки дій лише за
canEditіcanDeleteзTuChatMessageResponse; остаточна авторизація залишається на бекенді. - Відповідь кожної мутації одразу мержити в стан. Socket-подія може дублювати цю саму зміну, тому дедуплікувати за
idта актуальністюupdatedAt.
Socket-події
З’єднання використовує socket.io path /stack/sock/socket.io. Кімната чату має вигляд tuChat:{tuChatId}. Події created, updated, deleted і viewed отримують усі працівники, що приєдналися до цієї кімнати.
Події клієнт → сервер:
tuChat:join:{ family, tuId }. Callback успіху:{ success: true, data: { tuChatId } }; callback помилки:{ success: false, message }.tuChat:leave:tuChatId.
Події сервер → клієнт:
tuChat:message:created: повнийTuChatMessageResponse. Додати повідомлення в стан чату.tuChat:message:updated: повнийTuChatMessageResponse. Замінити повідомлення заid.tuChat:message:deleted: повнийTuChatMessageResponseзisDeleted: true. Замінити повідомлення заid, не видаляти його зі списку.tuChat:message:viewed:{ messageIds, userId, role, createdAt }. Оновити реакції цих повідомлень для іншого працівника. Для поточного працівника також прибрати всіmessageIdsз локальних unviewed-станів: глобального лічильника меню та списку TU.
Відновлення стану
Після пробудження ПК після сну, відкриття кришки ноутбука і тд.
Socket не є джерелом правди. Після реконекту треба повторно виконати tuChat:join для відкритого чату, завантажити актуальну сторінку через getMessages та оновити індикатори через getUnviewedMessageIds і getUnviewedByTu.