You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
252 lines
9.0 KiB
252 lines
9.0 KiB
#include "messagelist.h" |
|
#include "chatview.h" |
|
#include "messagedelegate.h" |
|
#include "inputbar.h" |
|
#include "emojipanel.h" |
|
#include "emoji.h" |
|
#include "animtimer.h" |
|
#include "lottieicon.h" |
|
#include "../db/db_manager.h" |
|
#include <QVBoxLayout> |
|
#include <QPainter> |
|
#include <QPixmap> |
|
#include <QDateTime> |
|
#include <QFile> |
|
#include <QDebug> |
|
#include <vector> |
|
#include <memory> |
|
|
|
extern "C" { |
|
#include "../transport/gui_bridge.h" |
|
#include "../transport/chat_core.h" |
|
#include "../../lib/u_async.h" |
|
#include "../../lib/mem.h" |
|
} |
|
|
|
static QPixmap makeAvatar(const QColor &color, const QString &letter, int size = 36) { |
|
QPixmap pix(size, size); |
|
pix.fill(Qt::transparent); |
|
QPainter p(&pix); |
|
p.setRenderHint(QPainter::Antialiasing); |
|
p.setBrush(color); |
|
p.setPen(Qt::NoPen); |
|
p.drawEllipse(0, 0, size, size); |
|
p.setPen(Qt::white); |
|
QFont f = p.font(); |
|
f.setBold(true); |
|
f.setPointSize(size / 3); |
|
p.setFont(f); |
|
p.drawText(QRect(0, 0, size, size), Qt::AlignCenter, letter); |
|
p.end(); |
|
return pix; |
|
} |
|
|
|
static QVariant makeReaction(const QString &emoji, int count, |
|
const QList<QColor> &colors = {}) { |
|
QVariantMap m; |
|
m["emoji"] = emoji; |
|
m["count"] = count; |
|
if (!colors.isEmpty()) |
|
m["colors"] = QVariant::fromValue(colors); |
|
return m; |
|
} |
|
|
|
static void setMsg(QStandardItem *item, |
|
const QString &author, const QString &text, const QString &time, |
|
const QPixmap &avatar, bool edited, |
|
bool hasQuote, const QString &qAuthor, const QString &qText, |
|
const QVariantList &reactions) { |
|
item->setData(author, MsgAuthorRole); |
|
item->setData(text, MsgTextRole); |
|
item->setData(time, MsgTimeRole); |
|
item->setData(edited, MsgEditedRole); |
|
item->setData(QVariant::fromValue(avatar), MsgAvatarRole); |
|
item->setData(hasQuote, MsgHasQuoteRole); |
|
if (hasQuote) { |
|
item->setData(qAuthor, MsgQuoteAuthorRole); |
|
item->setData(qText, MsgQuoteTextRole); |
|
} |
|
if (!reactions.isEmpty()) |
|
item->setData(reactions, MsgReactionsRole); |
|
} |
|
|
|
MessageList::MessageList(DbManager* db, QWidget *parent) |
|
: QWidget(parent) |
|
, m_view(new ChatView(this)) |
|
, m_model(new QStandardItemModel(this)) |
|
, m_inputBar(nullptr) |
|
, m_emojiPanel(nullptr) |
|
, m_db(db) |
|
{ |
|
auto *layout = new QVBoxLayout(this); |
|
layout->setContentsMargins(0, 0, 0, 0); |
|
layout->setSpacing(0); |
|
|
|
m_view->setModel(m_model); |
|
m_view->setItemDelegate(new MessageDelegate(this)); |
|
m_view->setEditTriggers(QAbstractItemView::NoEditTriggers); |
|
m_view->setSelectionMode(QAbstractItemView::NoSelection); |
|
m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
|
m_view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); |
|
m_view->setFrameShape(QFrame::NoFrame); |
|
m_view->setBackgroundImage("../resources/bg.jpg"); |
|
layout->addWidget(m_view, 1); |
|
|
|
static std::vector<std::unique_ptr<LottieIcon>> s_icons; |
|
if (s_icons.empty()) { |
|
for (const auto &cat : builtinEmojiSet()) { |
|
for (const auto &e : cat.items) { |
|
if (e.type == EmojiType::Animation && !e.animPath.isEmpty()) { |
|
QFile f(e.animPath); |
|
if (f.open(QIODevice::ReadOnly)) { |
|
auto icon = std::make_unique<LottieIcon>(f.readAll()); |
|
if (icon->isValid()) { |
|
AnimTimer::instance()->registerIcon(e.animPath, icon.get()); |
|
s_icons.push_back(std::move(icon)); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
m_emojiPanel = new EmojiPanel(this); |
|
layout->addWidget(m_emojiPanel); |
|
|
|
m_inputBar = new InputBar(m_emojiPanel, this); |
|
layout->addWidget(m_inputBar); |
|
|
|
connect(m_inputBar, &InputBar::sendMessage, this, [this](const QString &text) { |
|
if (m_currentChannelId.isEmpty()) { |
|
qWarning("MessageList: no channel selected, message not sent"); |
|
return; |
|
} |
|
|
|
QByteArray textData = text.toUtf8(); |
|
auto ts = QDateTime::currentMSecsSinceEpoch(); |
|
|
|
uint64_t datahash = 0; |
|
QByteArray chainHash; |
|
bool ok = m_db->insertMessage(m_currentChannelId, m_db->myNodeId(), |
|
"text/plain", textData, ts, true, |
|
&datahash, &chainHash); |
|
if (!ok) { |
|
qWarning("MessageList: insertMessage FAILED ch=%s ts=%lld", |
|
qPrintable(m_currentChannelId), ts); |
|
return; |
|
} |
|
qDebug("MessageList: msg stored ch=%s ts=%lld dh=0x%016llx", |
|
qPrintable(m_currentChannelId), ts, (unsigned long long)datahash); |
|
|
|
loadChannel(m_currentChannelId); |
|
|
|
struct chat_msg_submit* req = (struct chat_msg_submit*) |
|
u_malloc(sizeof(struct chat_msg_submit) + textData.size()); |
|
memset(req, 0, sizeof(*req)); |
|
snprintf(req->channel_id, sizeof(req->channel_id), "%s", |
|
m_currentChannelId.toUtf8().constData()); |
|
snprintf(req->content_type, sizeof(req->content_type), "text/plain"); |
|
req->data = (uint8_t*)(req + 1); |
|
req->data_len = (uint32_t)textData.size(); |
|
memcpy(req->data, textData.constData(), textData.size()); |
|
req->timestamp = (uint64_t)ts; |
|
gui_bridge_post_uasync_fn(chat_core_push_trampoline, req); |
|
}); |
|
|
|
connect(AnimTimer::instance(), &AnimTimer::ticked, this, [this]() { |
|
m_view->viewport()->update(); |
|
}); |
|
|
|
connect(m_view, &ChatView::hoveredIndexChanged, this, [this](const QModelIndex &idx) { |
|
bool hasAnim = false; |
|
if (idx.isValid()) { |
|
QString text = idx.data(MsgTextRole).toString(); |
|
for (auto it = g_animatedEmojiMap.begin(); it != g_animatedEmojiMap.end(); ++it) { |
|
if (text.contains(it.key())) { hasAnim = true; break; } |
|
} |
|
if (!hasAnim) { |
|
QVariantList reactions = idx.data(MsgReactionsRole).toList(); |
|
for (const QVariant &rv : reactions) { |
|
if (g_animatedEmojiMap.contains(rv.toMap()["emoji"].toString())) |
|
{ hasAnim = true; break; } |
|
} |
|
} |
|
} |
|
bool wasActive = m_animHoverActive; |
|
m_animHoverActive = hasAnim; |
|
if (!wasActive && m_animHoverActive) AnimTimer::instance()->activate(); |
|
else if (wasActive && !m_animHoverActive) AnimTimer::instance()->deactivate(); |
|
}); |
|
} |
|
|
|
void MessageList::loadChannel(const QString& channelId) { |
|
m_model->clear(); |
|
m_avatarCache.clear(); |
|
m_currentChannelId = channelId; |
|
if (!m_db || !m_db->isOpen()) return; |
|
|
|
auto msgs = m_db->getMessages(channelId, 100); |
|
|
|
struct ReactionKey { qint64 msgId; quint64 node; QString emoji; }; |
|
QHash<qint64, QHash<QString, QList<QColor>>> reactMap; |
|
|
|
for (const auto& m : msgs) { |
|
AccountRow acc = m_db->getAccount(m.authorNodeId); |
|
QString author = acc.displayName.isEmpty() |
|
? QString("node_%1").arg(m.authorNodeId) : acc.displayName; |
|
QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter; |
|
QColor color(acc.avatarColor); |
|
|
|
if (!m_avatarCache.contains(m.authorNodeId)) { |
|
m_avatarCache[m.authorNodeId] = color; |
|
} |
|
|
|
auto avatar = makeAvatar(color, letter); |
|
auto time = QDateTime::fromMSecsSinceEpoch(m.timestamp).toString("HH:mm"); |
|
|
|
QVariantList reacts; |
|
auto *item = new QStandardItem(); |
|
setMsg(item, author, QString::fromUtf8(m.data), time, avatar, false, |
|
false, QString(), QString(), reacts); |
|
m_model->appendRow(item); |
|
} |
|
|
|
if (m_model->rowCount() > 0) |
|
m_view->scrollToBottom(); |
|
m_inputBar->setFocus(); |
|
} |
|
|
|
void MessageList::refresh() { |
|
if (!m_currentChannelId.isEmpty()) |
|
loadChannel(m_currentChannelId); |
|
} |
|
|
|
void MessageList::addMessage(const QString& channelId, quint64 authorNodeId, |
|
const QByteArray& content, qint64 timestamp) { |
|
if (channelId != m_currentChannelId) return; |
|
|
|
AccountRow acc = m_db ? m_db->getAccount(authorNodeId) : AccountRow(); |
|
QString author = acc.displayName.isEmpty() |
|
? QString("node_%1").arg(authorNodeId) : acc.displayName; |
|
QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter; |
|
QColor color(acc.avatarColor); |
|
auto avatar = makeAvatar(color, letter); |
|
auto time = QDateTime::fromMSecsSinceEpoch(timestamp).toString("HH:mm"); |
|
|
|
/* dedup */ |
|
for (int r = 0; r < m_model->rowCount(); r++) { |
|
auto* it = m_model->item(r); |
|
if (it->data(MsgTimeRole).toString() == time |
|
&& it->data(MsgAuthorRole).toString() == author) return; |
|
} |
|
|
|
auto *item = new QStandardItem(); |
|
setMsg(item, author, QString::fromUtf8(content), time, avatar, false, |
|
false, QString(), QString(), QVariantList{}); |
|
m_model->appendRow(item); |
|
|
|
if (m_model->rowCount() > 50) |
|
m_model->removeRow(0); |
|
|
|
m_view->scrollToBottom(); |
|
}
|
|
|