- src/call/: сигналинг (call.c, call_proto.h, ETCP_RT_ID_CALL 0x36) + релей медиа + headless аудио-сокет (call_headless.c, call_audio_bind) + Opus/jitter/тоны - chat: события CHAT_EVT_CALL_* (34–40), CHAT_EVT_DM_MESSAGES (41), DM-трамплины - chatgui: call window, miniaudio I/O, audio_frame_ring, test_audio_jitter - chatgui-android: CallScreen/IncomingCallDialog, аудио-роутинг, JNI, рингтон - tests: test_call, test_call_headless
345 lines
15 KiB
C++
345 lines
15 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"
|
|
}
|
|
|
|
/* 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)
|
|
{
|
|
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 ¤t, 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::loadChannels() {
|
|
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(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"
|