Files
utun2/tools/chatgui/src/channellist.cpp
T
evgeny 09756159b2 Add Android boot startup, lazy settings, emoji and DM calls
Add battery optimization controls and configurable boot startup. Keep message sounds in the application and render general settings lazily to avoid ANRs. Add animated emoji controls and live BGP availability for direct-message calls on Android and desktop.
2026-10-02 17:18:24 +03:00

396 lines
17 KiB
C++

#include "channellist.h"
#include "channeldelegate.h"
#include "../db/db_manager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QPainter>
#include <QPixmap>
#include <QMouseEvent>
extern "C" {
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include "../transport/gui_bridge.h"
#include "dm/dm_core.h"
}
/* QListView that prevents deselection, handles invite plus-button clicks */
class ChannelListView : public QListView {
Q_OBJECT
public:
using QListView::QListView;
signals:
void inviteClicked(const QString& channelId);
void inviteByLinkClicked(const QString& channelId);
void settingsClicked(const QString& channelId);
protected:
void mousePressEvent(QMouseEvent* e) override {
QModelIndex idx = indexAt(e->pos());
if (!idx.isValid()) return;
if (idx.data(ChannelIsSectionRole).toBool()) return;
QRect gearRect = ChannelDelegate::settingsButtonRect(visualRect(idx));
if (gearRect.contains(e->pos())) {
emit settingsClicked(idx.data(ChannelChannelIdRole).toString());
return;
}
QRect invRect = ChannelDelegate::inviteByLinkButtonRect(visualRect(idx));
if (invRect.contains(e->pos())) {
emit inviteByLinkClicked(idx.data(ChannelChannelIdRole).toString());
return;
}
QRect plusRect = ChannelDelegate::inviteButtonRect(visualRect(idx));
if (plusRect.contains(e->pos())) {
emit inviteClicked(idx.data(ChannelChannelIdRole).toString());
return;
}
QListView::mousePressEvent(e);
}
void mouseMoveEvent(QMouseEvent* e) override {
int prevInv = m_hoveredInviteRow;
int prevInvBy = m_hoveredInviteByLinkRow;
int prevSettings = m_hoveredSettingsRow;
m_hoveredInviteRow = -1;
m_hoveredInviteByLinkRow = -1;
m_hoveredSettingsRow = -1;
QModelIndex idx = indexAt(e->pos());
if (idx.isValid() && !idx.data(ChannelIsSectionRole).toBool()) {
QRect gearRect = ChannelDelegate::settingsButtonRect(visualRect(idx));
QRect invRect = ChannelDelegate::inviteByLinkButtonRect(visualRect(idx));
QRect plusRect = ChannelDelegate::inviteButtonRect(visualRect(idx));
if (gearRect.contains(e->pos()))
m_hoveredSettingsRow = idx.row();
else if (invRect.contains(e->pos()))
m_hoveredInviteByLinkRow = idx.row();
else if (plusRect.contains(e->pos()))
m_hoveredInviteRow = idx.row();
}
if (m_hoveredInviteRow != prevInv)
model()->setData(model()->index(prevInv, 0), 0, ChannelInviteBtnHoveredRole);
if (m_hoveredInviteRow >= 0)
model()->setData(model()->index(m_hoveredInviteRow, 0), 1, ChannelInviteBtnHoveredRole);
if (m_hoveredInviteByLinkRow != prevInvBy)
model()->setData(model()->index(prevInvBy, 0), 0, ChannelInviteByLinkBtnHoveredRole);
if (m_hoveredInviteByLinkRow >= 0)
model()->setData(model()->index(m_hoveredInviteByLinkRow, 0), 1, ChannelInviteByLinkBtnHoveredRole);
if (m_hoveredSettingsRow != prevSettings)
model()->setData(model()->index(prevSettings, 0), 0, ChannelSettingsBtnHoveredRole);
if (m_hoveredSettingsRow >= 0)
model()->setData(model()->index(m_hoveredSettingsRow, 0), 1, ChannelSettingsBtnHoveredRole);
setCursor((m_hoveredInviteRow >= 0 || m_hoveredInviteByLinkRow >= 0 || m_hoveredSettingsRow >= 0)
? Qt::PointingHandCursor : Qt::ArrowCursor);
QListView::mouseMoveEvent(e);
}
QItemSelectionModel::SelectionFlags selectionCommand(const QModelIndex& idx, const QEvent* e) const override {
if (idx.isValid() && idx.data(ChannelIsSectionRole).toBool())
return QItemSelectionModel::NoUpdate;
if (idx.isValid())
return QItemSelectionModel::ClearAndSelect;
return QItemSelectionModel::NoUpdate;
}
void selectionChanged(const QItemSelection& sel, const QItemSelection& desel) override {
QListView::selectionChanged(sel, desel);
if (selectionModel() && !selectionModel()->hasSelection() && currentIndex().isValid()
&& currentIndex().row() < model()->rowCount())
selectionModel()->select(currentIndex(), QItemSelectionModel::ClearAndSelect);
}
private:
int m_hoveredInviteRow = -1;
int m_hoveredInviteByLinkRow = -1;
int m_hoveredSettingsRow = -1;
};
static QIcon makeChannelIcon(const QColor &color, const QString &letter) {
QPixmap pix(32, 32);
pix.fill(Qt::transparent);
QPainter p(&pix);
p.setRenderHint(QPainter::Antialiasing);
p.setBrush(color);
p.setPen(Qt::NoPen);
p.drawRoundedRect(0, 0, 32, 32, 8, 8);
p.setPen(Qt::white);
QFont f = p.font();
f.setBold(true);
f.setPointSize(14);
p.setFont(f);
p.drawText(QRect(0, 0, 32, 32), Qt::AlignCenter, letter);
p.end();
return QIcon(pix);
}
ChannelList::ChannelList(DbManager* db, QWidget *parent)
: QWidget(parent)
, m_listView(new ChannelListView(this))
, m_model(new QStandardItemModel(this))
, m_db(db)
{
m_dmPresenceTimer = new QTimer(this);
m_dmPresenceTimer->setSingleShot(true);
m_dmPresenceTimer->setInterval(50);
connect(m_dmPresenceTimer, &QTimer::timeout, this, [this]() {
auto* inst = gui_bridge_get_inst();
if (!inst || !m_db) { clearDmPresence(); return; }
for (const auto& dm : m_db->getDmConversations()) {
auto* req = (struct dm_messages_req*)u_calloc(1, sizeof(struct dm_messages_req));
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "PM presence: allocation failed"); return; }
req->inst = inst;
snprintf(req->conv_id, sizeof(req->conv_id), "%s", dm.convId.toUtf8().constData());
gui_bridge_post_uasync_fn(dm_call_route_trampoline, req);
}
});
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
auto *header = new QLabel("Channels", this);
header->setAlignment(Qt::AlignCenter);
header->setStyleSheet("font-weight: bold; padding: 8px; background: palette(window);");
layout->addWidget(header);
m_listView->setModel(m_model);
m_listView->setItemDelegate(new ChannelDelegate(this));
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_listView->setSelectionMode(QAbstractItemView::SingleSelection);
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_listView->setMouseTracking(true);
layout->addWidget(m_listView);
auto *footer = new QWidget(this);
auto *fl = new QHBoxLayout(footer);
fl->setContentsMargins(6, 4, 6, 4);
fl->setSpacing(6);
auto *gearBtn = new QPushButton(QString::fromUtf8("\xE2\x9A\x99"), footer);
gearBtn->setFixedSize(28, 28);
gearBtn->setToolTip(QString::fromUtf8("Настройки"));
gearBtn->setStyleSheet(
"QPushButton { border: none; font-size: 18px; background: transparent; color: #666; }"
"QPushButton:hover { color: #3390EC; background: rgba(51,144,236,20); border-radius: 14px; }");
fl->addWidget(gearBtn);
connect(gearBtn, &QPushButton::clicked, this, [this] { emit settingsRequested(); });
auto *monitorBtn = new QPushButton(QString::fromUtf8("\xF0\x9F\x94\x8D"), footer); /* 🔍 */
monitorBtn->setFixedSize(28, 28);
monitorBtn->setToolTip(QString::fromUtf8("Connection Monitor"));
monitorBtn->setStyleSheet(
"QPushButton { border: none; font-size: 16px; background: transparent; color: #666; }"
"QPushButton:hover { color: #3390EC; background: rgba(51,144,236,20); border-radius: 14px; }");
fl->addWidget(monitorBtn);
connect(monitorBtn, &QPushButton::clicked, this, [this] { emit connMonitorRequested(); });
fl->addStretch();
auto *createBtn = new QPushButton("GRP", footer);
createBtn->setFixedSize(36, 28);
createBtn->setToolTip(QString::fromUtf8("Создать группу"));
createBtn->setStyleSheet(
"QPushButton { border: none; border-radius: 6px; font-size: 11px; font-weight: bold;"
" color: white; background: #27AE60; }"
"QPushButton:hover { background: #219A52; }");
connect(createBtn, &QPushButton::clicked, this, [this] { emit createGroupRequested(); });
fl->addWidget(createBtn);
auto *plusBtn = new QPushButton("+", footer);
plusBtn->setFixedSize(28, 28);
plusBtn->setToolTip(QString::fromUtf8("Присоединиться"));
plusBtn->setStyleSheet(
"QPushButton { border: none; border-radius: 14px; font-size: 18px; font-weight: bold;"
" color: white; background: #3390EC; }"
"QPushButton:hover { background: #2B7EC8; }");
connect(plusBtn, &QPushButton::clicked, this, [this] { emit joinRequested(); });
fl->addWidget(plusBtn);
layout->addWidget(footer);
connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged,
this, [this](const QModelIndex &current, const QModelIndex &) {
if (current.isValid()) {
QString cid = current.data(ChannelChannelIdRole).toString();
emit channelSelected(cid);
}
});
connect(static_cast<ChannelListView*>(m_listView), &ChannelListView::inviteClicked,
this, &ChannelList::inviteRequested);
connect(static_cast<ChannelListView*>(m_listView), &ChannelListView::inviteByLinkClicked,
this, &ChannelList::inviteByLinkRequested);
connect(static_cast<ChannelListView*>(m_listView), &ChannelListView::settingsClicked,
this, &ChannelList::channelSettingsRequested);
}
void ChannelList::refreshDmPresence() {
if (!m_dmPresenceTimer->isActive()) m_dmPresenceTimer->start();
}
void ChannelList::clearDmPresence() {
m_dmPresenceTimer->stop();
m_dmOnline.clear();
for (int i = 0; i < m_model->rowCount(); i++) {
auto* item = m_model->item(i);
if (item->data(ChannelChannelIdRole).toString().startsWith(QStringLiteral("dm:")))
item->setData(false, ChannelDmOnlineRole);
}
}
void ChannelList::onDmCallRoute(const uint8_t* data, int len) {
if (!data || len < 17 || len != 17 + data[0]) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "PM presence snapshot: invalid size=%d", len); return;
}
QString conv = QString::fromUtf8((const char*)data + 1, data[0]);
quint64 peer = 0, group = 0;
memcpy(&peer, data + 1 + data[0], 8); memcpy(&group, data + 9 + data[0], 8);
bool online = peer && group;
if (m_dmOnline.value(conv) != online)
DEBUG_INFO(DEBUG_CATEGORY_CALL, "PM presence: conv=%s online=%d", conv.toUtf8().constData(), online);
m_dmOnline.insert(conv, online);
for (int i = 0; i < m_model->rowCount(); i++) {
auto* item = m_model->item(i);
if (item->data(ChannelChannelIdRole).toString() == QStringLiteral("dm:") + conv)
item->setData(online, ChannelDmOnlineRole);
}
}
void ChannelList::loadChannels() {
refreshDmPresence();
QString selectedCid;
if (m_listView->currentIndex().isValid())
selectedCid = m_listView->currentIndex().data(ChannelChannelIdRole).toString();
auto* sel = m_listView->selectionModel();
sel->blockSignals(true);
m_model->clear();
if (m_db && m_db->isOpen()) {
int sortMode = m_db->getUiStateInt("channel_sort", 0);
auto channels = m_db->getChannels(sortMode == 1 ? ChannelSortMode::Recent : ChannelSortMode::Alphabetical);
static const QColor colors[] = {
QColor(74,144,226), QColor(126,211,33), QColor(245,166,35),
QColor(208,2,27), QColor(144,19,254), QColor(0,184,148)
};
int ci = 0;
for (const auto& ch : channels) {
auto letter = ch.name.mid(0, 1).toUpper();
if (letter.isEmpty()) letter = "#";
auto *item = new QStandardItem();
item->setData(ch.name, ChannelNameRole);
item->setData(ch.channelId, ChannelChannelIdRole);
item->setData(m_db->getChannelLastMessage(ch.channelId), ChannelLastMessageRole);
item->setData(ch.unreadCount, ChannelUnreadCountRole);
item->setData(ch.lastMsgAt, ChannelLastMsgAtRole);
item->setData(makeChannelIcon(colors[ci % 6], letter), ChannelIconRole);
m_model->appendRow(item);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "ChannelList::loadChannels row=%d cid='%s' name='%s' unread=%d lastMsgAt=%lld",
m_model->rowCount()-1, qPrintable(ch.channelId), qPrintable(ch.name), ch.unreadCount,
(long long)ch.lastMsgAt);
ci++;
}
/* ── секция «Личные» (DM-беседы) ── */
auto dms = m_db->getDmConversations();
if (!dms.isEmpty()) {
auto *section = new QStandardItem();
section->setData(QStringLiteral("Личные"), ChannelNameRole);
section->setData(true, ChannelIsSectionRole);
section->setFlags(Qt::NoItemFlags);
m_model->appendRow(section);
static const QColor dmColor(0x8E, 0x44, 0xAD);
for (const auto& dm : dms) {
QString name = dm.peerName;
if (name.isEmpty()) name = QString("0x%1").arg(dm.peerNodeId, 4, 16, QChar('0')).toUpper();
auto letter = name.mid(0, 1).toUpper();
if (letter.isEmpty()) letter = "@";
auto *item = new QStandardItem();
item->setData(name, ChannelNameRole);
item->setData(QStringLiteral("dm:") + dm.convId, ChannelChannelIdRole);
item->setData(m_dmOnline.value(dm.convId), ChannelDmOnlineRole);
item->setData(QString(), ChannelLastMessageRole);
item->setData(0, ChannelUnreadCountRole);
item->setData(dm.lastTs, ChannelLastMsgAtRole);
item->setData(makeChannelIcon(dmColor, letter), ChannelIconRole);
m_model->appendRow(item);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "ChannelList::loadChannels dm row=%d conv='%s' peer='%s'",
m_model->rowCount()-1, qPrintable(dm.convId), qPrintable(name));
}
}
}
sel->blockSignals(false);
if (!selectedCid.isEmpty()) {
selectChannel(selectedCid);
} else if (m_model->rowCount() > 0) {
QModelIndex idx = m_model->index(0, 0);
sel->select(idx, QItemSelectionModel::ClearAndSelect);
sel->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
}
}
void ChannelList::selectChannel(const QString& channelId) {
for (int row = 0; row < m_model->rowCount(); row++) {
if (m_model->item(row)->data(ChannelChannelIdRole).toString() == channelId) {
QModelIndex idx = m_model->index(row, 0);
m_listView->selectionModel()->select(idx, QItemSelectionModel::ClearAndSelect);
m_listView->selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
return;
}
}
}
void ChannelList::setChannelPeersOnline(const QString& channelId, int count) {
int found = 0;
for (int row = 0; row < m_model->rowCount(); row++) {
QStandardItem* item = m_model->item(row);
if (item) {
QString itemCid = item->data(ChannelChannelIdRole).toString();
int match = (itemCid == channelId) ? 1 : 0;
if (match) {
item->setData(count, ChannelPeersOnlineRole);
item->setData(m_autoConnectActive ? 1 : 0, ChannelAutoConnectActiveRole);
found = 1;
break;
}
}
}
if (!found)
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, " NOT FOUND in model");
}
void ChannelList::setAutoConnectActive(bool active) {
m_autoConnectActive = active;
for (int row = 0; row < m_model->rowCount(); row++) {
QStandardItem* item = m_model->item(row);
if (item) item->setData(active ? 1 : 0, ChannelAutoConnectActiveRole);
}
}
void ChannelList::setChannelUnreadCount(const QString& channelId, int count) {
for (int row = 0; row < m_model->rowCount(); row++) {
QStandardItem* item = m_model->item(row);
if (item && item->data(ChannelChannelIdRole).toString() == channelId) {
int old = item->data(ChannelUnreadCountRole).toInt();
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "ChannelList::setChannelUnreadCount ch=%s %d->%d",
qPrintable(channelId), old, count);
item->setData(count, ChannelUnreadCountRole);
emit m_model->dataChanged(m_model->index(row, 0), m_model->index(row, 0));
return;
}
}
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "ChannelList::setChannelUnreadCount ch=%s not found", qPrintable(channelId));
}
int ChannelList::getChannelUnreadCount(const QString& channelId) const {
for (int row = 0; row < m_model->rowCount(); row++) {
QStandardItem* item = m_model->item(row);
if (item && item->data(ChannelChannelIdRole).toString() == channelId)
return item->data(ChannelUnreadCountRole).toInt();
}
return 0;
}
#include "channellist.moc"