|
|
|
|
@ -1,5 +1,6 @@
|
|
|
|
|
#include <QCoreApplication> |
|
|
|
|
#include "messagelist.h" |
|
|
|
|
#include "../transport/stream_bridge.h" |
|
|
|
|
#include "debug_ui.h" |
|
|
|
|
#include "chatview.h" |
|
|
|
|
#include "messagedelegate.h" |
|
|
|
|
@ -426,6 +427,30 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
|
|
|
|
|
connect(m_radioBtn, &QPushButton::toggled, this, &MessageList::onRadioToggled); |
|
|
|
|
hl->addWidget(m_radioBtn); |
|
|
|
|
|
|
|
|
|
m_streamBtn = new QPushButton(header); m_streamBtn->setObjectName("streamButton"); |
|
|
|
|
QPixmap streamIcon(24, 24); streamIcon.fill(Qt::transparent); |
|
|
|
|
{ QPainter painter(&streamIcon); painter.setRenderHint(QPainter::Antialiasing); painter.setPen(QPen(QColor("#e53935"), 2)); |
|
|
|
|
painter.drawRoundedRect(QRectF(2, 6, 20, 14), 2, 2); painter.drawLine(8, 1, 12, 5); painter.drawLine(16, 1, 12, 5); |
|
|
|
|
painter.setBrush(QColor("#e53935")); painter.drawEllipse(QPointF(12, 13), 3, 3); } |
|
|
|
|
m_streamBtn->setIcon(QIcon(streamIcon)); m_streamBtn->setIconSize(QSize(24, 24)); m_streamBtn->hide(); |
|
|
|
|
m_streamBtn->setAccessibleName(QCoreApplication::translate("ChatGui", "Live streams")); |
|
|
|
|
m_streamBtn->setCursor(Qt::PointingHandCursor); |
|
|
|
|
connect(m_streamBtn, &QPushButton::clicked, this, [this] { |
|
|
|
|
auto watch = [this](const LiveSource& source) { |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop selected stream group=%s source=%llx epoch=%llx", |
|
|
|
|
qPrintable(m_currentChannelId), (unsigned long long)source.id, (unsigned long long)source.epoch); |
|
|
|
|
emit streamRequested(m_currentChannelId.toULongLong(), source.id, source.epoch, source.name); |
|
|
|
|
}; |
|
|
|
|
if (m_streamSources.size() == 1) { watch(m_streamSources.front()); return; } |
|
|
|
|
QMenu menu(this); |
|
|
|
|
for (const auto& source : m_streamSources) { |
|
|
|
|
auto* action = menu.addAction(QStringLiteral("%1 · %2").arg(source.name).arg(source.id)); |
|
|
|
|
connect(action, &QAction::triggered, this, [watch, source] { watch(source); }); |
|
|
|
|
} |
|
|
|
|
menu.exec(m_streamBtn->mapToGlobal(QPoint(0, m_streamBtn->height()))); |
|
|
|
|
}); |
|
|
|
|
hl->addWidget(m_streamBtn); |
|
|
|
|
|
|
|
|
|
m_groupMenuButton = new QPushButton(QStringLiteral("…"), header); |
|
|
|
|
m_groupMenuButton->setObjectName("groupMenuButton"); |
|
|
|
|
m_groupMenuButton->setFixedSize(34, 30); |
|
|
|
|
@ -749,6 +774,7 @@ void MessageList::loadChannel(const QString& channelId) {
|
|
|
|
|
m_avatarCache.clear(); |
|
|
|
|
if (m_currentChannelId != channelId) { m_feedback.clear(); m_pendingPoll.clear(); } |
|
|
|
|
m_currentChannelId = channelId; |
|
|
|
|
clearStreams(); watchStreams(); |
|
|
|
|
m_groupMenuButton->setVisible(channelId.toULongLong() != 0); |
|
|
|
|
clearDmCallRoute(); |
|
|
|
|
m_callBtn->setVisible(isDmChannel(channelId)); |
|
|
|
|
@ -1451,6 +1477,43 @@ void MessageList::onRadioTalk(quint64 groupId, quint64 srcNodeId, bool on) {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Каталог выбранного канала не удерживает media owner; DM/пустой target снимает наблюдение. */ |
|
|
|
|
void MessageList::watchStreams() { |
|
|
|
|
if (gui_stream_ready()) gui_stream_watch(isDmChannel(m_currentChannelId) ? 0 : m_currentChannelId.toULongLong()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void MessageList::clearStreams() { m_streamSources.clear(); m_streamBtn->hide(); } |
|
|
|
|
|
|
|
|
|
/* Полный снимок применяется атомарно; ответы предыдущего канала не меняют текущую шапку. */ |
|
|
|
|
void MessageList::onStreamSources(const uint8_t* data, int len) { |
|
|
|
|
if (!data || len < 11 || data[8] > 1) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop source event invalid len=%d", len); return; } |
|
|
|
|
quint64 group = stream_read_be(data, 8); |
|
|
|
|
if (isDmChannel(m_currentChannelId) || group != m_currentChannelId.toULongLong()) return; |
|
|
|
|
QJsonParseError error; |
|
|
|
|
auto document = QJsonDocument::fromJson(QByteArray(reinterpret_cast<const char*>(data + 9), len - 9), &error); |
|
|
|
|
if (!data[8] || error.error != QJsonParseError::NoError || !document.isArray()) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop source list failed group=%llx valid=%u parse=%s", |
|
|
|
|
(unsigned long long)group, data[8], qPrintable(error.errorString())); |
|
|
|
|
clearStreams(); return; |
|
|
|
|
} |
|
|
|
|
QList<LiveSource> sources; |
|
|
|
|
for (const auto& value : document.array()) { |
|
|
|
|
auto member = value.toObject(); bool idOk = false, epochOk = false; |
|
|
|
|
quint64 id = member.value("id").toString().toULongLong(&idOk); |
|
|
|
|
quint64 epoch = member.value("epoch").toString().toULongLong(&epochOk, 16); |
|
|
|
|
if (!idOk || !epochOk || !id || !epoch || !member.value("name").isString()) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop source identity malformed group=%llx", (unsigned long long)group); |
|
|
|
|
clearStreams(); return; |
|
|
|
|
} |
|
|
|
|
QString name = member.value("name").toString(); if (name.isEmpty()) name = QString::number(id); |
|
|
|
|
sources.push_back({id, epoch, name}); |
|
|
|
|
} |
|
|
|
|
m_streamSources = std::move(sources); m_streamBtn->setVisible(!m_streamSources.isEmpty()); |
|
|
|
|
m_streamBtn->setText(m_streamSources.size() > 1 ? QString::number(m_streamSources.size()) : QString()); |
|
|
|
|
m_streamBtn->setToolTip(QCoreApplication::translate("ChatGui", "Live streams: %1").arg(m_streamSources.size())); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop stream header group=%llx sources=%d", (unsigned long long)group, (int)m_streamSources.size()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void MessageList::onRadioToggled(bool on) { |
|
|
|
|
if (m_currentChannelId.isEmpty() || isDmChannel(m_currentChannelId)) return; |
|
|
|
|
|
|
|
|
|
|