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

#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();
}