2026-04-17 15:43:49 +02:00
|
|
|
#include "MessageListModel.h"
|
|
|
|
|
|
2026-07-29 22:10:19 +02:00
|
|
|
#include "Identity.h"
|
|
|
|
|
#include "TimeFormat.h"
|
|
|
|
|
|
2026-07-17 09:18:54 +02:00
|
|
|
#include <QDate>
|
|
|
|
|
#include <QLocale>
|
2026-07-15 08:16:41 +02:00
|
|
|
#include <algorithm>
|
|
|
|
|
#include <utility>
|
|
|
|
|
|
2026-04-17 15:43:49 +02:00
|
|
|
MessageListModel::MessageListModel(QObject* parent)
|
|
|
|
|
: QAbstractListModel(parent)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int MessageListModel::rowCount(const QModelIndex& parent) const
|
|
|
|
|
{
|
|
|
|
|
if (parent.isValid()) return 0;
|
|
|
|
|
return m_items.size();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariant MessageListModel::data(const QModelIndex& index, int role) const
|
|
|
|
|
{
|
|
|
|
|
if (!index.isValid() || index.row() >= m_items.size())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
const auto& item = m_items.at(index.row());
|
2026-07-17 09:18:54 +02:00
|
|
|
// The older neighbour renders directly above this row (newest-first model,
|
|
|
|
|
// BottomToTop list), so index+1 is the chronologically previous message.
|
|
|
|
|
const int older = index.row() + 1;
|
2026-04-17 15:43:49 +02:00
|
|
|
switch (role) {
|
|
|
|
|
case SenderRole: return item.sender;
|
|
|
|
|
case ContentRole: return item.content;
|
|
|
|
|
case TimestampRole: return item.timestamp;
|
|
|
|
|
case IsMeRole: return item.isMe;
|
2026-07-17 09:18:54 +02:00
|
|
|
case SameSenderAsPreviousRole: {
|
|
|
|
|
if (older >= m_items.size()) return false;
|
|
|
|
|
const auto& prev = m_items.at(older);
|
|
|
|
|
return item.sender == prev.sender && item.isMe == prev.isMe
|
|
|
|
|
&& item.timestamp.date() == prev.timestamp.date();
|
|
|
|
|
}
|
|
|
|
|
case ShowDaySeparatorRole:
|
|
|
|
|
return older >= m_items.size() || item.timestamp.date() != m_items.at(older).timestamp.date();
|
|
|
|
|
case DayLabelRole: return dayLabel(item.timestamp);
|
2026-07-29 22:10:19 +02:00
|
|
|
case TimeDisplayRole: return TimeFormat::shortTime(item.timestamp);
|
|
|
|
|
case AvatarInitialsRole: return Identity::initials(item.sender);
|
|
|
|
|
case AvatarRampRole: return Identity::avatarRamp(item.sender);
|
2026-04-17 15:43:49 +02:00
|
|
|
default: return {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QHash<int, QByteArray> MessageListModel::roleNames() const
|
|
|
|
|
{
|
|
|
|
|
return {
|
2026-07-17 09:18:54 +02:00
|
|
|
{ SenderRole, "sender" },
|
|
|
|
|
{ ContentRole, "content" },
|
|
|
|
|
{ TimestampRole, "timestamp" },
|
|
|
|
|
{ IsMeRole, "isMe" },
|
|
|
|
|
{ SameSenderAsPreviousRole, "sameSenderAsPrevious" },
|
|
|
|
|
{ ShowDaySeparatorRole, "showDaySeparator" },
|
2026-07-29 22:10:19 +02:00
|
|
|
{ DayLabelRole, "dayLabel" },
|
|
|
|
|
{ TimeDisplayRole, "timeDisplay" },
|
|
|
|
|
{ AvatarInitialsRole, "avatarInitials" },
|
|
|
|
|
{ AvatarRampRole, "avatarRamp" }
|
2026-04-17 15:43:49 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MessageListModel::addMessage(const QString& sender, const QString& content,
|
|
|
|
|
const QDateTime& timestamp, bool isMe)
|
|
|
|
|
{
|
2026-07-15 08:16:41 +02:00
|
|
|
// Drop an exact duplicate of the newest message. A reconnect resync reloads
|
2026-06-25 21:51:25 +02:00
|
|
|
// the active thread via get_messages while live events still push, so a
|
|
|
|
|
// message persisted just before the reload and delivered just after would
|
2026-07-15 08:16:41 +02:00
|
|
|
// otherwise appear twice; distinct messages never share content + timestamp
|
|
|
|
|
// + sender. The model is newest-first, so the newest row is the front.
|
2026-06-25 21:51:25 +02:00
|
|
|
if (!m_items.isEmpty()) {
|
2026-07-15 08:16:41 +02:00
|
|
|
const MessageItem& newest = m_items.first();
|
|
|
|
|
if (newest.isMe == isMe && newest.timestamp == timestamp && newest.content == content)
|
2026-06-25 21:51:25 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 08:16:41 +02:00
|
|
|
beginInsertRows(QModelIndex(), 0, 0);
|
|
|
|
|
m_items.prepend({ sender, content, timestamp, isMe });
|
2026-04-17 15:43:49 +02:00
|
|
|
endInsertRows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MessageListModel::addMessages(QVector<MessageItem> items)
|
|
|
|
|
{
|
|
|
|
|
const int n = items.size();
|
|
|
|
|
if (n == 0) return;
|
|
|
|
|
|
2026-07-15 08:16:41 +02:00
|
|
|
// get_messages returns the thread oldest-first; the model is newest-first
|
|
|
|
|
// (row 0 is the newest message, which the BottomToTop list pins to the
|
|
|
|
|
// visual bottom), so reverse the batch before appending it as older history.
|
|
|
|
|
std::reverse(items.begin(), items.end());
|
2026-04-17 15:43:49 +02:00
|
|
|
const int firstRow = m_items.size();
|
|
|
|
|
beginInsertRows(QModelIndex(), firstRow, firstRow + n - 1);
|
|
|
|
|
m_items += std::move(items);
|
|
|
|
|
endInsertRows();
|
2026-07-17 09:18:54 +02:00
|
|
|
|
|
|
|
|
// The row that was oldest now has an older neighbour beneath it, so its
|
|
|
|
|
// day-separator and grouping flags may have flipped.
|
|
|
|
|
if (firstRow > 0)
|
|
|
|
|
emit dataChanged(index(firstRow - 1), index(firstRow - 1),
|
|
|
|
|
{ SameSenderAsPreviousRole, ShowDaySeparatorRole });
|
2026-04-17 15:43:49 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-15 08:16:41 +02:00
|
|
|
void MessageListModel::setMessages(QVector<MessageItem> items)
|
|
|
|
|
{
|
|
|
|
|
// Replace the whole thread in a single reset. get_messages returns the
|
|
|
|
|
// thread oldest-first and the model is newest-first, so reverse as
|
|
|
|
|
// addMessages does. Doing this as one reset (rather than clear() then
|
|
|
|
|
// addMessages()) means the list never passes through an empty state, so
|
|
|
|
|
// switching conversations does not flash the view empty.
|
|
|
|
|
std::reverse(items.begin(), items.end());
|
|
|
|
|
beginResetModel();
|
|
|
|
|
m_items = std::move(items);
|
|
|
|
|
endResetModel();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 15:43:49 +02:00
|
|
|
void MessageListModel::clear()
|
|
|
|
|
{
|
|
|
|
|
if (m_items.isEmpty()) return;
|
|
|
|
|
beginResetModel();
|
|
|
|
|
m_items.clear();
|
|
|
|
|
endResetModel();
|
|
|
|
|
}
|
2026-07-17 09:18:54 +02:00
|
|
|
|
|
|
|
|
QString MessageListModel::dayLabel(const QDateTime& timestamp) const
|
|
|
|
|
{
|
|
|
|
|
if (!timestamp.isValid())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
const QDate today = QDate::currentDate();
|
|
|
|
|
const QDate date = timestamp.date();
|
|
|
|
|
if (date == today)
|
|
|
|
|
return tr("Today");
|
|
|
|
|
if (date == today.addDays(-1))
|
|
|
|
|
return tr("Yesterday");
|
|
|
|
|
return QLocale::system().toString(date, QLocale::ShortFormat);
|
|
|
|
|
}
|