304 lines
13 KiB
C++
304 lines
13 KiB
C++
#include <QCoreApplication>
|
|
#include "invitedialog.h"
|
|
#include "qrcode_utils.h"
|
|
#include "../db/db_manager.h"
|
|
#include "../transport/gui_bridge.h"
|
|
#include "debug_ui.h"
|
|
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QLineEdit>
|
|
#include <QPushButton>
|
|
#include <QLabel>
|
|
#include <QComboBox>
|
|
#include <QClipboard>
|
|
#include <QApplication>
|
|
#include <QSignalBlocker>
|
|
#include <QPointer>
|
|
#include <QtEndian>
|
|
#include <cstring>
|
|
|
|
extern "C" {
|
|
#include "../../../src/chat/invite_build.h"
|
|
#include "../../../src/chat/chat_join.h"
|
|
#include "../../../src/chat/chat_local.h"
|
|
#include "../../../lib/mem.h"
|
|
}
|
|
|
|
static QPointer<ShareInviteDialog> s_shareInviteDialog;
|
|
|
|
ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QWidget* parent)
|
|
: QDialog(parent)
|
|
, m_channelId(channelId)
|
|
, m_db(db)
|
|
{
|
|
setWindowTitle(QCoreApplication::translate("ChatGui", "Invite link"));
|
|
setMinimumSize(420, 560);
|
|
setModal(true);
|
|
s_shareInviteDialog = this;
|
|
|
|
auto *layout = new QVBoxLayout(this);
|
|
layout->setSpacing(12);
|
|
auto* expiry = new QLabel(QCoreApplication::translate("ChatGui",
|
|
"The link expires %1 minutes after creation.").arg(JOIN_KEY_TTL_SECONDS / 60), this);
|
|
expiry->setWordWrap(true);
|
|
layout->addWidget(expiry);
|
|
|
|
auto *nodeLabel = new QLabel(QCoreApplication::translate("ChatGui", "Connect via"), this);
|
|
nodeLabel->setStyleSheet("font-size: 12px; color: palette(window-text);");
|
|
|
|
m_nodeCombo = new QComboBox(this);
|
|
m_nodeCombo->setStyleSheet(
|
|
"QComboBox { border: 1px solid palette(mid); border-radius: 4px;"
|
|
" padding: 6px 10px; font-size: 13px; background: palette(base); }");
|
|
|
|
auto *nodeRow = new QHBoxLayout();
|
|
nodeRow->setSpacing(8);
|
|
nodeRow->addWidget(nodeLabel);
|
|
nodeRow->addWidget(m_nodeCombo, 1);
|
|
layout->addLayout(nodeRow);
|
|
|
|
m_linkEdit = new QLineEdit(this);
|
|
m_linkEdit->setReadOnly(true);
|
|
m_linkEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Generating link..."));
|
|
m_linkEdit->setStyleSheet("padding: 6px; font-family: monospace;");
|
|
|
|
auto *copyBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Copy"), this);
|
|
connect(copyBtn, &QPushButton::clicked, this, [this]() {
|
|
QClipboard* cb = QApplication::clipboard();
|
|
cb->setText(m_linkEdit->text(), QClipboard::Clipboard);
|
|
if (cb->supportsSelection())
|
|
cb->setText(m_linkEdit->text(), QClipboard::Selection);
|
|
m_linkEdit->setFocus();
|
|
});
|
|
|
|
auto *linkRow = new QHBoxLayout();
|
|
linkRow->setSpacing(8);
|
|
linkRow->addWidget(m_linkEdit, 1);
|
|
linkRow->addWidget(copyBtn);
|
|
layout->addLayout(linkRow);
|
|
|
|
m_localRow = new QWidget(this);
|
|
auto* localLayout = new QVBoxLayout(m_localRow);
|
|
localLayout->setContentsMargins(0, 0, 0, 0);
|
|
localLayout->addWidget(new QLabel(QCoreApplication::translate("ChatGui", "Devices on the local network:"), m_localRow));
|
|
auto* deviceRow = new QHBoxLayout();
|
|
m_devices = new QComboBox(m_localRow);
|
|
m_devices->setObjectName(QStringLiteral("localDevices"));
|
|
m_sendLocal = new QPushButton(QCoreApplication::translate("ChatGui", "Send to device"), m_localRow);
|
|
m_sendLocal->setObjectName(QStringLiteral("sendLocalInvite"));
|
|
m_sendLocal->setEnabled(false);
|
|
deviceRow->addWidget(m_devices, 1); deviceRow->addWidget(m_sendLocal);
|
|
localLayout->addLayout(deviceRow);
|
|
layout->addWidget(m_localRow); m_localRow->hide();
|
|
m_delivery = new QLabel(this);
|
|
m_delivery->setWordWrap(true); layout->addWidget(m_delivery);
|
|
connect(m_sendLocal, &QPushButton::clicked, this, &ShareInviteDialog::sendLocalInvite);
|
|
connect(m_devices, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this] { updateLocalSend(); });
|
|
gui_bridge_set_local_devices_cb([](const uint8_t* data, int len) {
|
|
if (s_shareInviteDialog) s_shareInviteDialog->onLocalDevices(data, len);
|
|
});
|
|
gui_bridge_set_local_delivery_cb([](const uint8_t* data, int len) {
|
|
if (s_shareInviteDialog) s_shareInviteDialog->onLocalDelivery(data, len);
|
|
});
|
|
if (gui_bridge_is_uasync_ready() && gui_bridge_get_inst())
|
|
gui_bridge_post_uasync_fn(chat_local_peers_trampoline, gui_bridge_get_inst());
|
|
|
|
m_qrLabel = new QLabel(this);
|
|
m_qrLabel->setAlignment(Qt::AlignCenter);
|
|
m_qrLabel->setMinimumSize(180, 180);
|
|
layout->addWidget(m_qrLabel, 0, Qt::AlignCenter);
|
|
|
|
auto *closeBtn = new QPushButton("OK", this);
|
|
connect(closeBtn, &QPushButton::clicked, this, &QDialog::accept);
|
|
layout->addWidget(closeBtn, 0, Qt::AlignRight);
|
|
|
|
connect(m_nodeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
|
this, &ShareInviteDialog::onNodeSelected);
|
|
|
|
/* колбэки результатов (по образцу JoinDialog) */
|
|
gui_bridge_set_invite_link_ready_cb([](const char* chId, int chIdLen, uint64_t requestId, const char* link) {
|
|
ShareInviteDialog* dlg = s_shareInviteDialog;
|
|
if (dlg) dlg->onLinkReady(chId, chIdLen, requestId, link);
|
|
});
|
|
gui_bridge_set_invite_candidates_cb([](const char* chId, int chIdLen, const uint8_t* data, int len) {
|
|
ShareInviteDialog* dlg = s_shareInviteDialog;
|
|
if (dlg) dlg->onCandidates(chId, chIdLen, data, len);
|
|
});
|
|
|
|
/* запросить кандидатов; линк строится после их прихода */
|
|
struct chat_invite_cand_req* creq = (struct chat_invite_cand_req*)u_calloc(1, sizeof(*creq));
|
|
if (!creq) return;
|
|
creq->inst = gui_bridge_get_inst();
|
|
strncpy(creq->ch_id, m_channelId.toUtf8().constData(), sizeof(creq->ch_id) - 1);
|
|
gui_bridge_post_uasync_fn(chat_invite_candidates_trampoline, creq);
|
|
}
|
|
|
|
ShareInviteDialog::~ShareInviteDialog() {
|
|
s_shareInviteDialog = nullptr;
|
|
gui_bridge_set_local_devices_cb(nullptr);
|
|
gui_bridge_set_local_delivery_cb(nullptr);
|
|
gui_bridge_set_invite_link_ready_cb(nullptr);
|
|
gui_bridge_set_invite_candidates_cb(nullptr);
|
|
}
|
|
|
|
void ShareInviteDialog::onNodeSelected(int index) {
|
|
if (index < 0) return;
|
|
qulonglong nodeId = m_nodeCombo->itemData(index).toULongLong();
|
|
requestLink((uint64_t)nodeId);
|
|
}
|
|
|
|
void ShareInviteDialog::requestLink(uint64_t targetNodeId) {
|
|
struct chat_invite_build_req* req = (struct chat_invite_build_req*)u_malloc(sizeof(*req));
|
|
if (!req) {
|
|
GUI_ERROR("invite: requestLink — alloc failed");
|
|
return;
|
|
}
|
|
memset(req, 0, sizeof(*req));
|
|
req->inst = gui_bridge_get_inst();
|
|
QByteArray chBytes = m_channelId.toUtf8();
|
|
strncpy(req->ch_id, chBytes.constData(), sizeof(req->ch_id) - 1);
|
|
static uint64_t nextRequestId = 0;
|
|
req->request_id = m_requestId = ++nextRequestId;
|
|
req->target_node_id = targetNodeId;
|
|
m_linkEdit->setText(QString());
|
|
updateLocalSend();
|
|
m_linkEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Generating link..."));
|
|
m_qrLabel->clear();
|
|
gui_bridge_post_uasync_fn(chat_invite_build_link_trampoline, req);
|
|
}
|
|
|
|
void ShareInviteDialog::setLink(const QString& link) {
|
|
m_linkEdit->setText(link);
|
|
updateLocalSend();
|
|
QImage qrImage = generateQRCode(link);
|
|
if (!qrImage.isNull())
|
|
m_qrLabel->setPixmap(QPixmap::fromImage(qrImage).scaled(
|
|
180, 180, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
|
adjustSize();
|
|
}
|
|
|
|
void ShareInviteDialog::onCandidates(const char* chId, int chIdLen, const uint8_t* data, int len) {
|
|
QString cid = QString::fromUtf8(chId, chIdLen);
|
|
if (cid != m_channelId) return;
|
|
if (!data || len < 10) return;
|
|
|
|
uint64_t autoId = 0;
|
|
memcpy(&autoId, data, 8);
|
|
uint16_t count = 0;
|
|
memcpy(&count, data + 8, 2);
|
|
|
|
if (autoId == 0) {
|
|
m_linkEdit->setText(QString());
|
|
m_linkEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Failed to identify the node (see log)"));
|
|
return;
|
|
}
|
|
|
|
auto nodeLabel = [this](uint64_t nid) {
|
|
QString name = m_db ? m_db->getDisplayName(nid) : QString();
|
|
if (name.isEmpty())
|
|
name = QString("0x%1").arg(nid, 16, 16, QChar('0'));
|
|
return name;
|
|
};
|
|
|
|
/* блокируем сигналы, чтобы не перезапрашивать линк при добавлении пунктов */
|
|
QSignalBlocker blocker(m_nodeCombo);
|
|
m_nodeCombo->clear();
|
|
|
|
/* пункт 0 — узел, найденный авто-выбором */
|
|
m_nodeCombo->addItem(nodeLabel(autoId) + QCoreApplication::translate("ChatGui", " (auto)"),
|
|
QVariant::fromValue((qulonglong)autoId));
|
|
|
|
/* остальные узлы (пропуская авто), свой помечаем (self) */
|
|
int off = 10;
|
|
for (int i = 0; i < (int)count && off + 8 <= len; i++) {
|
|
uint64_t nid = 0;
|
|
memcpy(&nid, data + off, 8);
|
|
off += 8;
|
|
if (nid == autoId) continue;
|
|
QString label = nodeLabel(nid);
|
|
if (m_db && nid == m_db->myNodeId())
|
|
label += QCoreApplication::translate("ChatGui", " (self)");
|
|
m_nodeCombo->addItem(label, QVariant::fromValue((qulonglong)nid));
|
|
}
|
|
|
|
/* строим линк для авто-узла (сигнал заблокирован) */
|
|
requestLink(autoId);
|
|
}
|
|
|
|
void ShareInviteDialog::onLinkReady(const char* chId, int chIdLen, uint64_t requestId, const char* link) {
|
|
if (requestId != m_requestId) return;
|
|
QString cid = QString::fromUtf8(chId, chIdLen);
|
|
if (cid != m_channelId) return;
|
|
if (!link || link[0] == '\0') {
|
|
m_linkEdit->setText(QString());
|
|
updateLocalSend();
|
|
m_linkEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "Failed to build the link (see log)"));
|
|
m_qrLabel->clear();
|
|
return;
|
|
}
|
|
setLink(QString::fromUtf8(link));
|
|
}
|
|
|
|
void ShareInviteDialog::updateLocalSend() {
|
|
m_sendLocal->setEnabled(!m_localRequest && m_devices->currentIndex() >= 0 && !m_linkEdit->text().isEmpty());
|
|
}
|
|
|
|
void ShareInviteDialog::onLocalDevices(const uint8_t* data, int len) {
|
|
if (len < 2) { GUI_ERROR("discovery: invalid device list bytes=%d", len); return; }
|
|
struct Device { quint64 id; QString name; };
|
|
QList<Device> devices;
|
|
int count = qFromBigEndian<quint16>(data), offset = 2;
|
|
for (int i = 0; i < count; i++) {
|
|
if (offset + 9 > len || offset + 9 + data[offset + 8] > len) {
|
|
GUI_ERROR("discovery: truncated device list bytes=%d offset=%d", len, offset); return;
|
|
}
|
|
int nameLen = data[offset + 8];
|
|
devices.append({qFromBigEndian<quint64>(data + offset), QString::fromUtf8((const char*)data + offset + 9, nameLen)});
|
|
offset += 9 + nameLen;
|
|
}
|
|
if (offset != len) { GUI_ERROR("discovery: trailing device list bytes=%d", len - offset); return; }
|
|
auto selected = m_devices->currentData();
|
|
QSignalBlocker blocker(m_devices);
|
|
m_devices->clear();
|
|
for (const auto& device : devices) {
|
|
const QString suffix = QString::number(device.id, 16).right(6);
|
|
m_devices->addItem(device.name + QStringLiteral(" [%1]").arg(suffix), QVariant::fromValue(device.id));
|
|
}
|
|
int index = m_devices->findData(selected);
|
|
if (index >= 0) m_devices->setCurrentIndex(index);
|
|
m_localRow->setVisible(!devices.isEmpty()); updateLocalSend();
|
|
}
|
|
|
|
void ShareInviteDialog::sendLocalInvite() {
|
|
QByteArray link = m_linkEdit->text().toUtf8();
|
|
if (m_localRequest || m_devices->currentIndex() < 0 || link.isEmpty()) return;
|
|
if (!gui_bridge_is_uasync_ready() || !gui_bridge_get_inst()) {
|
|
GUI_ERROR("discovery: core stopped before local send");
|
|
m_delivery->setText(QCoreApplication::translate("ChatGui", "Could not deliver invitation")); return;
|
|
}
|
|
if (link.size() > LOCAL_TRANSPORT_DATA_MAX) {
|
|
GUI_ERROR("discovery: invitation too long bytes=%d", (int)link.size());
|
|
m_delivery->setText(QCoreApplication::translate("ChatGui", "Could not deliver invitation")); return;
|
|
}
|
|
auto* req = (struct chat_local_send_request*)u_calloc(1, sizeof(struct chat_local_send_request));
|
|
if (!req) { GUI_ERROR("discovery: send request allocation failed"); return; }
|
|
static uint64_t nextRequest = 0;
|
|
req->inst = gui_bridge_get_inst(); req->peer_id = m_devices->currentData().toULongLong();
|
|
req->request_id = m_localRequest = ++nextRequest;
|
|
memcpy(req->link, link.constData(), (size_t)link.size());
|
|
m_delivery->setText(QCoreApplication::translate("ChatGui", "Sending invitation…")); updateLocalSend();
|
|
gui_bridge_post_uasync_fn(chat_local_send_trampoline, req);
|
|
}
|
|
|
|
void ShareInviteDialog::onLocalDelivery(const uint8_t* data, int len) {
|
|
if (len != 17 || data[16] > LOCAL_DELIVERY_FORBIDDEN) { GUI_ERROR("discovery: invalid delivery response bytes=%d", len); return; }
|
|
if (!m_localRequest || qFromBigEndian<quint64>(data) != m_localRequest) return;
|
|
m_localRequest = 0;
|
|
if (data[16] == LOCAL_DELIVERY_FORBIDDEN) m_delivery->setText(QCoreApplication::translate("ChatGui",
|
|
"Receiving invitations is forbidden. Enable it in the recipient device's settings."));
|
|
else m_delivery->setText(QCoreApplication::translate("ChatGui",
|
|
data[16] == LOCAL_DELIVERY_RECEIVED ? "Invitation delivered" : "Could not deliver invitation"));
|
|
updateLocalSend();
|
|
}
|