Архитектура хранения непрочитанных сообщений. Способ исключения сообщений из 'непрочитанных'
UPDATE
Получил обратную связь от знакомых. Добавлю больше конкретики. Задача - хранить состояние непрочитанных сообщений. Для этого оформили таблицу MessagesHistory. Запись типа roomId(taskId) userId history[] элемент данного массива - срез непрочитанных сообщений из таблицы Messages. Условно мы заходим в чат, создаётся новый объект в массиве
{
before: - хранит messageId, присваивается messageId того сообщения, что находится снизу
after: пока null но когда придут новые сообщения и мы пролистаем ниже подобный цикл повторится или как только долистаем до последних сообщений присвоится последнее видимое сообщение
}
after предыдущего элемента массива допустим был равен null, но мы зашли на страницу и начали просматривать сообщения до before: null. Как только выходим то если сообщение входит в промежуток до before, то after что принадлежит данному промежутку присваивается messageId
Условий много, но примерно так.
UPDATE
В рамках производственной практики разрабатываю канбан с элементами saas, erp и crm. На данном моменте делаем модуль чата. В канбан доске есть задачи, у каждой задачи есть чат, чат не представлен конкретной сущностью в БД, формируется из относящихся к задаче сообщений.
При загрузке страницы, где могут отображаться чаты, в Pinia сохраняются уведомления. Но эта часть не важна.
Для хранения непрочитанных сообщений выбрали следующую структуру. (Таблица Message, Таблица MessagesHistory)
roomId совпадает с taskId history - массив элементов-объектов структуры (historyItem).
{
before: messageId | null,
after: messageId | null
}
Т.е. для каждого пользователя для каждого чата задачи создаётся запись истории, состоящая из условных "сессий" объектов типа historyItem. Когда задача создаётся, то история инициализируется для всех пользователей в проекте одним пустым элементом. Каждый такой объект - срез непрочитанных сообщений, благодаря чему можно определить их количество.
{
before: null,
after: null
}
Если в чате имеются сообщения. То пользователь при таком состоянии поля history таблицы MessagesHistory получает количество непрочитанных сообщений, равное всем сообщениям минус те, которые создал сам.
Когда пользователь откроет чат, то у записи таблицы с соответствующим roomId, userId в history[0].before останется null, что будет указывать на то, что не прочитан промежуток сообщений с самого первого, условно firstMessageId. При заходе в чат создаётся новый элемент массива history, messageId задачи внизу области прокрутки ставится в before нового объекта, after: null. Для after предыдущего элемента массива устанавливается messageId самого верхнего элемента, запрос на обновление отправляется не сразу, при onUnmounted и некоторых других случаях. Когда в элементе массива after становится равен before подобный элемент массива удаляется.
Последние 100 сообщений кэшируются в redis и присылаются сокетом, из сокета также получаем новые сообщения - messages, когда ктото пишет в том же чате и мы находимся в нём. Данные получаемые с запроса - oldMessages. Вместе они собираются в массив который рендерится в компоненты Message. Но это огромная нагрузка, ладно сообщений сотни, но тысячи уже будут заставлять приложение тормозить. А я ещё не говорил что в них могут быть изображения. Для записи сообщений используется библиотека TipTap (https://tiptap.dev/docs/editor/getting-started/install/vue3) Удобная вещь.
На этом этапе встало несколько вопросов:
- Как отслеживать верхний элемент. Либо через IntersectionObserver, его я уже использую для отслеживания вхождения наблюдателя (невидимого элемента) в область прокрутки, чтобы инициировать подгрузку других сообщений. Или MutationObserver через него ещё реализовать не пробовал.
Но создание для этого, как я понял, надо хранить для каждого сообщения условный наблюдатель, или weakMap htmlelements, что тоже затратно по ресурсам.
Я собирался использовать альтернативу - виртуальные списки, они способны предоставлять элементы что хранятся в определённой позиции.
Пробовал https://vueuse.org/core/useVirtualList/ Но он не позволяет своими средствами точно вычислять свою высоту, т.к. позволяет задавать элементам только равную высоту/ширину, а сообщения все разные по высоте. Я пытал GPT как мог, но не смог найти способа производить ресайз элементов динамически после первичного рендера, чтобы высота обёртки была равной сумме высот элементов плюс расстояние меж ними.
Думаю попробовать https://github.com/Akryum/vue-virtual-scroller/blob/master/packages/vue-virtual-scroller/README.md
Он должен иметь методы для определения высот своих элементов.
На край, я уже понимаю, как написать необходимые компоненты хуки и т.д. для определения элементов располагающихся сверху/снизу области видимости.
Вкратце: Будет allMessage - хранит подгруженные сообщения. Компонент - виртуальный лист, прокручивает 30-40 элементов из allMessage, область видимости при этом отображает 15-20. Когда заканчивается верх/низ, перерисовывается половину берёт сверху если листается наверх, остаток из allMessage после идущих следом, вниз - соответственно. Наблюдатель для подгрузки добавляется когда доходим до самой верхней границы allMessage.
Что это даёт? Можно хранить weakMap всего 30-40 наблюдаемых элементов, чтобы фиксировать их пересечение области видимости при размонтировании.
Основной плюс архитектуры - достаточно отправлять messageId. Представим ситуацию: имеется элемент в поле history который
{
before: messageId_A,
after: messageId_B
}
и мы хотим обновить after: messageId_C. Как нам найти этот элемент? Просто messageId_C будет входить в данный срез.
Если дочитали, прошу, поделитесь опытом, опыта разработки не так много, книги по архитектуре читать дело уважаемое, но я много сил потрачу на это. Что можете сказать про архитектуру хранения непрочитанных сообщений? Как мне лучше реализовать отслеживание последних прочитанных сообщений.
Я осознаю ту кучу логики которую предстоит описать для корректной работы такой реализации, с этим проблем нет все случаи учту. По типу: пользователь зашёл в чат, создался элемент с before: id задачи что было последним в чате в самом низу на момент входа, пролистали вверх и пришло несколько сообщений, как нам определить их прочитанными и исключить из среза, началом которого стал ранее обговоренный before? если отлистаем вниз? Думаю смогу определить.
Благодарю.
Ответы (1 шт):
Решение нашлось.
Через v-for Без особой нагрузки можно отрисовывать достаточно большое количество элементов. Думаю, тысячу точно. Даже с учётом, что сообщения могут иметь изображения и другие "тяжёлые" элементы.
Добавляем атрибут :data-message-id="item.data.id" чтобы па нему найти элемент и узнать id сообщения
Это метод нахождения messageId для after.
rect - получаем данные о родителе обёртке topY - положение по viweport по y leftx - положение по viweport по x
el - элемент в указанной точке. Найденный элемент может быть вложенным в искомый элемент. Следовательно нужно подняться до родительского элемента.
Есть несколько узких мест у данного решения:
- Клик может пройти мимо элемента, например: в пространство меж я ищу способы обработки данного случая
- Для адаптации под мобильные устройства можно прописать computed свойства, чтоб более точно задавать "магические числа" у topY и leftX
- Буду признателен, если найдёте ещё уязвимости.
Стоит рассмотреть варианты с применением виртуального листа и IntersectionObserver






