206 lines
12 KiB
C++
206 lines
12 KiB
C++
#include <QCoreApplication>
|
|
#include "devgroupdialog.h"
|
|
#include "../transport/gui_bridge.h"
|
|
#include "debug_ui.h"
|
|
#include <QDialog>
|
|
#include <QVBoxLayout>
|
|
#include <QLabel>
|
|
#include <QPushButton>
|
|
#include <QLineEdit>
|
|
#include <QInputDialog>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QTimer>
|
|
#include <QPointer>
|
|
#include <QHash>
|
|
#include <QMessageBox>
|
|
#include <QSet>
|
|
#include <QDialogButtonBox>
|
|
#include <QRegularExpressionValidator>
|
|
extern "C" {
|
|
#include "../../../src/chat/chat_devgroup.h"
|
|
#include "../../../lib/mem.h"
|
|
}
|
|
class DevgroupDialog;
|
|
static QPointer<DevgroupDialog> active;
|
|
static QHash<quint64, QPointer<QPushButton>> leaveRequests;
|
|
static quint64 nextRequest = 0;
|
|
static QPointer<QWidget> invitationWindow;
|
|
static quint64 statusRequest = 0, lastAutomaticSnapshot = 0;
|
|
static QHash<quint64, QString> acceptRequests;
|
|
static QHash<QString, QPointer<QInputDialog>> pinDialogs;
|
|
static QSet<QString> dismissedInvites, awaitingPins, attemptedPins;
|
|
static void refreshInvitations();
|
|
static void automaticInvitations(const QJsonObject& snapshot);
|
|
static void devgroupResult(const uint8_t* data, int len);
|
|
class DevgroupDialog : public QDialog {
|
|
public:
|
|
static QString tr(const char* text) { return QCoreApplication::translate("DevgroupDialog", text); }
|
|
void refresh() { send(CHAT_DEVGROUP_STATUS); }
|
|
DevgroupDialog(QWidget* parent, const QString& channel, quint64 peer) : QDialog(parent), channel(channel), peer(peer) {
|
|
setAttribute(Qt::WA_DeleteOnClose); setModal(true); setWindowTitle(tr("Add device")); resize(420, 140);
|
|
auto* layout = new QVBoxLayout(this);
|
|
status = new QLabel(tr("Waiting for PIN"), this); status->setWordWrap(true);
|
|
status->setTextInteractionFlags(Qt::TextSelectableByMouse); layout->addWidget(status);
|
|
auto* close = new QDialogButtonBox(QDialogButtonBox::Close, this); layout->addWidget(close);
|
|
connect(close, &QDialogButtonBox::rejected, this, &QDialog::close);
|
|
auto* timer = new QTimer(this); timer->setInterval(1000);
|
|
connect(timer, &QTimer::timeout, this, [this] {
|
|
if (!gui_bridge_is_uasync_ready()) { pending.clear(); status->setText(tr("Core unavailable")); }
|
|
else if (!pending.values().contains(CHAT_DEVGROUP_STATUS)) refresh();
|
|
});
|
|
timer->start(); send(CHAT_DEVGROUP_INVITE);
|
|
}
|
|
void result(const uint8_t* data, int len) {
|
|
if (len < 9) return;
|
|
quint64 id = 0; for (int i = 0; i < 8; ++i) id |= quint64(data[i]) << (8*i);
|
|
if (!pending.contains(id)) return;
|
|
const int action = pending.take(id);
|
|
if (!data[8]) { status->setText(tr("Operation failed. Check connection and invitation status.")); return; }
|
|
const auto json = QJsonDocument::fromJson(QByteArray(reinterpret_cast<const char*>(data+9), len-9)).object();
|
|
if (action == CHAT_DEVGROUP_INVITE) {
|
|
outgoingId = json["id"].toString(); pinText = tr("PIN: %1 — enter it on the other device within 2 minutes.").arg(json["pin"].toString());
|
|
status->setText(pinText); return;
|
|
}
|
|
const QStringList states = {{}, tr("Waiting for PIN"), tr("Checking PIN"), tr("Transferring keys"), tr("Joined"),
|
|
tr("Rejected"), tr("Expired"), tr("Result unknown")};
|
|
for (const auto value : json["invitations"].toArray()) {
|
|
const auto invitation = value.toObject(); const int state = invitation["state"].toInt();
|
|
if (invitation["id"].toString() == outgoingId) status->setText((state < 4 ? pinText + "\n" : QString()) + states.value(state));
|
|
}
|
|
}
|
|
private:
|
|
void send(chat_devgroup_action action) {
|
|
auto* req = static_cast<chat_devgroup_request*>(u_calloc(1, sizeof(chat_devgroup_request)));
|
|
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup UI: allocation failed"); return; }
|
|
req->inst = gui_bridge_get_inst(); req->request_id = ++nextRequest; req->group = channel.toULongLong(); req->peer = peer;
|
|
req->action = action; const quint64 id = req->request_id;
|
|
if (gui_bridge_post_uasync_owned_fn(chat_devgroup_trampoline, req) < 0) {
|
|
u_free(req); status->setText(tr("Core unavailable")); DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: core unavailable"); return;
|
|
}
|
|
pending.insert(id, action);
|
|
}
|
|
QString channel, outgoingId, pinText; quint64 peer;
|
|
QLabel* status;
|
|
QHash<quint64, int> pending;
|
|
};
|
|
static void devgroupResult(const uint8_t* data, int len) {
|
|
if (!len) {
|
|
refreshInvitations();
|
|
if (active) active->refresh();
|
|
} else {
|
|
if (len >= 9) {
|
|
quint64 id = 0; for (int i = 0; i < 8; ++i) id |= quint64(data[i]) << (8*i);
|
|
if (leaveRequests.contains(id)) {
|
|
const auto button = leaveRequests.take(id);
|
|
if (!data[8]) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: disconnect refused");
|
|
if (button) { button->setEnabled(true); QMessageBox::warning(button->window(),
|
|
QCoreApplication::translate("ChatGui", "Disconnect from my devices"),
|
|
DevgroupDialog::tr("Operation failed. Check connection and invitation status.")); }
|
|
} else DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup UI: own device disconnected");
|
|
return;
|
|
}
|
|
const bool automatic = id == statusRequest || acceptRequests.contains(id);
|
|
if (id == statusRequest) statusRequest = 0;
|
|
if (acceptRequests.contains(id)) awaitingPins.remove(acceptRequests.take(id));
|
|
if (automatic) {
|
|
if (data[8] && id >= lastAutomaticSnapshot) {
|
|
lastAutomaticSnapshot = id;
|
|
automaticInvitations(QJsonDocument::fromJson(QByteArray(reinterpret_cast<const char*>(data+9), len-9)).object());
|
|
}
|
|
else if (!data[8]) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: automatic PIN/status request refused");
|
|
}
|
|
}
|
|
if (active) active->result(data, len);
|
|
}
|
|
}
|
|
static void refreshInvitations() {
|
|
if (!invitationWindow || statusRequest || !gui_bridge_is_uasync_ready()) return;
|
|
auto* req = static_cast<chat_devgroup_request*>(u_calloc(1, sizeof(chat_devgroup_request)));
|
|
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup UI: status allocation failed"); return; }
|
|
req->inst = gui_bridge_get_inst(); req->action = CHAT_DEVGROUP_STATUS; req->request_id = ++nextRequest;
|
|
const quint64 id = req->request_id;
|
|
if (gui_bridge_post_uasync_owned_fn(chat_devgroup_trampoline, req) < 0) {
|
|
u_free(req); DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: status enqueue failed"); return;
|
|
}
|
|
statusRequest = id;
|
|
}
|
|
static void automaticInvitations(const QJsonObject& snapshot) {
|
|
if (!invitationWindow) return;
|
|
QSet<QString> waiting;
|
|
for (const auto value : snapshot["invitations"].toArray()) {
|
|
const auto entry = value.toObject(); const QString id = entry["id"].toString();
|
|
if (entry["outgoing"].toInt() || entry["state"].toInt() != 1) continue;
|
|
waiting.insert(id);
|
|
if (pinDialogs.value(id) || dismissedInvites.contains(id) || awaitingPins.contains(id)) continue;
|
|
auto* dialog = new QInputDialog(invitationWindow); pinDialogs.insert(id, dialog);
|
|
dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setObjectName("devgroupIncomingPin");
|
|
dialog->setWindowTitle(DevgroupDialog::tr("Add device"));
|
|
dialog->setLabelText(entry["peer"].toString() + "\n" + DevgroupDialog::tr(attemptedPins.contains(id) ?
|
|
"PIN was not accepted. Try again." : "PIN shown on the inviting device"));
|
|
dialog->setInputMode(QInputDialog::TextInput); dialog->setWindowModality(Qt::ApplicationModal);
|
|
if (auto* edit = dialog->findChild<QLineEdit*>()) {
|
|
edit->setMaxLength(4); edit->setValidator(new QRegularExpressionValidator(QRegularExpression("[0-9]{0,4}"), edit));
|
|
}
|
|
QObject::connect(dialog, &QInputDialog::textValueChanged, dialog, [dialog](const QString& text) {
|
|
if (auto* box = dialog->findChild<QDialogButtonBox*>()) box->button(QDialogButtonBox::Ok)->setEnabled(text.size() == 4);
|
|
});
|
|
QObject::connect(dialog, &QDialog::finished, dialog, [dialog, id](int result) {
|
|
pinDialogs.remove(id);
|
|
if (result != QDialog::Accepted) { dismissedInvites.insert(id); return; }
|
|
auto* req = static_cast<chat_devgroup_request*>(u_calloc(1, sizeof(chat_devgroup_request)));
|
|
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup UI: PIN request allocation failed"); return; }
|
|
const auto pin = dialog->textValue().toLatin1(), invite = id.toLatin1();
|
|
if (pin.size() != 4 || invite.size() != 32) { u_free(req); DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: invalid PIN request"); return; }
|
|
req->inst = gui_bridge_get_inst(); req->action = CHAT_DEVGROUP_ACCEPT; req->request_id = ++nextRequest;
|
|
memcpy(req->text, invite.constData(), 32); memcpy(req->pin, pin.constData(), 4);
|
|
const quint64 request = req->request_id;
|
|
if (gui_bridge_post_uasync_owned_fn(chat_devgroup_trampoline, req) < 0) {
|
|
memset(req, 0, sizeof(*req)); u_free(req); DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: PIN enqueue failed"); return;
|
|
}
|
|
awaitingPins.insert(id); attemptedPins.insert(id); acceptRequests.insert(request, id);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup UI: PIN submitted invite=%s", qPrintable(id));
|
|
});
|
|
dialog->open();
|
|
if (auto* box = dialog->findChild<QDialogButtonBox*>()) box->button(QDialogButtonBox::Ok)->setEnabled(false);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup UI: incoming PIN dialog opened invite=%s", qPrintable(id));
|
|
}
|
|
for (const auto& id : pinDialogs.keys()) if (!waiting.contains(id)) {
|
|
auto dialog = pinDialogs.take(id); if (dialog) { dialog->disconnect(); dialog->close(); }
|
|
}
|
|
}
|
|
void initDevgroupUi(QWidget* window) {
|
|
if (invitationWindow) return;
|
|
statusRequest = 0; lastAutomaticSnapshot = 0; acceptRequests.clear(); dismissedInvites.clear(); awaitingPins.clear(); attemptedPins.clear();
|
|
invitationWindow = window; gui_bridge_set_devgroup_result_cb(devgroupResult);
|
|
auto* timer = new QTimer(window); timer->setInterval(1000);
|
|
QObject::connect(timer, &QTimer::timeout, window, [] {
|
|
if (!gui_bridge_is_uasync_ready()) {
|
|
statusRequest = 0; lastAutomaticSnapshot = 0; acceptRequests.clear(); awaitingPins.clear(); attemptedPins.clear(); dismissedInvites.clear();
|
|
automaticInvitations(QJsonObject{{"invitations", QJsonArray{}}}); return;
|
|
}
|
|
refreshInvitations();
|
|
});
|
|
timer->start(); refreshInvitations();
|
|
}
|
|
void inviteMyDevice(QWidget* parent, const QString& channel, quint64 peer) {
|
|
if (active) { active->close(); active = nullptr; }
|
|
gui_bridge_set_devgroup_result_cb(devgroupResult);
|
|
active = new DevgroupDialog(parent, channel, peer); active->show();
|
|
}
|
|
|
|
void disconnectMyDevice(QPushButton* button) {
|
|
auto* req = static_cast<chat_devgroup_request*>(u_calloc(1, sizeof(chat_devgroup_request)));
|
|
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup UI: disconnect allocation failed"); return; }
|
|
req->inst = gui_bridge_get_inst(); req->action = CHAT_DEVGROUP_LEAVE; req->request_id = ++nextRequest;
|
|
const quint64 id = req->request_id; gui_bridge_set_devgroup_result_cb(devgroupResult);
|
|
if (gui_bridge_post_uasync_owned_fn(chat_devgroup_trampoline, req) < 0) {
|
|
u_free(req); DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: disconnect enqueue failed");
|
|
QMessageBox::warning(button->window(), QCoreApplication::translate("ChatGui", "Disconnect from my devices"),
|
|
DevgroupDialog::tr("Core unavailable")); return;
|
|
}
|
|
leaveRequests.insert(id, button); button->setEnabled(false);
|
|
}
|