feat: create stream and add members to stream
This commit is contained in:
@@ -38,6 +38,10 @@ qt_add_executable(llink
|
||||
src/particlelistmodel.cpp
|
||||
src/textinputdialog.h
|
||||
src/textinputdialog.cpp
|
||||
src/createstreamdialog.h
|
||||
src/createstreamdialog.cpp
|
||||
src/memberpickerdialog.h
|
||||
src/memberpickerdialog.cpp
|
||||
)
|
||||
|
||||
qt_add_resources(llink
|
||||
|
||||
+66
-1
@@ -5,10 +5,13 @@
|
||||
#include "MainWindow.h"
|
||||
#include "authdialog.h"
|
||||
#include "authmanager.h"
|
||||
#include "createstreamdialog.h"
|
||||
#include "memberpickerdialog.h"
|
||||
#include "settingsdialog.h"
|
||||
#include "textinputdialog.h"
|
||||
#include "ui_mainwindow.h"
|
||||
#include <QApplication>
|
||||
#include <QJsonDocument>
|
||||
#include <QHeaderView>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QLabel>
|
||||
@@ -23,7 +26,7 @@
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent), m_trayIcon(nullptr), m_authManager(new AuthManager(this)), m_settingsDialog(nullptr),
|
||||
m_authDialog(nullptr), m_store(nullptr), m_networkStreamModel(nullptr), m_particleListModel(nullptr),
|
||||
m_selectedParticleRow(-1), m_isDragging(false), m_textInputDialog(nullptr)
|
||||
m_selectedParticleRow(-1), m_isDragging(false), m_textInputDialog(nullptr), m_createStreamDialog(nullptr)
|
||||
{
|
||||
m_authDialog = new AuthDialog(this);
|
||||
m_textInputDialog = new TextInputDialog(this);
|
||||
@@ -110,6 +113,57 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
// DocumentSend toolbar button opens text input dialog
|
||||
connect(ui->toolButton_3, &QToolButton::clicked, this, &MainWindow::showTextInputDialog);
|
||||
|
||||
// "New stream" button
|
||||
m_createStreamDialog = new CreateStreamDialog(this);
|
||||
connect(ui->pushButton, &QPushButton::clicked, this, [this]() {
|
||||
if (m_selectedNetworkName.isEmpty())
|
||||
return;
|
||||
const Network *network = m_store->networkById(m_selectedNetworkName);
|
||||
if (network)
|
||||
m_createStreamDialog->setNetworkMembers(network->members);
|
||||
m_createStreamDialog->show();
|
||||
m_createStreamDialog->raise();
|
||||
m_createStreamDialog->activateWindow();
|
||||
});
|
||||
connect(m_createStreamDialog, &CreateStreamDialog::streamRequested, this,
|
||||
[this](const QString &name, const QString &visibility, const QStringList &members) {
|
||||
m_store->createStream(m_selectedNetworkName, name, "", visibility, members);
|
||||
});
|
||||
|
||||
// "Add members" toolbar button (ListAdd icon in right sidebar)
|
||||
connect(ui->toolButton_7, &QToolButton::clicked, this, [this]() {
|
||||
if (m_selectedStreamId.isEmpty() || m_selectedNetworkName.isEmpty())
|
||||
return;
|
||||
const Network *network = m_store->networkById(m_selectedNetworkName);
|
||||
const Stream *stream = m_store->streamById(m_selectedStreamId);
|
||||
if (!network || !stream)
|
||||
return;
|
||||
|
||||
MemberPickerDialog picker("Add Members", this);
|
||||
picker.setMembers(network->members, stream->memberEmails);
|
||||
|
||||
if (picker.exec() == QDialog::Accepted)
|
||||
{
|
||||
QStringList emails = picker.selectedEmails();
|
||||
if (!emails.isEmpty())
|
||||
{
|
||||
auto *op = m_store->addStreamMembers(m_selectedStreamId, emails);
|
||||
connect(op, &Operation::success, this, [this](const QJsonDocument &) {
|
||||
auto *fetchOp = m_store->fetchStream(m_selectedStreamId);
|
||||
connect(fetchOp, &Operation::success, this, [this](const QJsonDocument &) {
|
||||
ui->membersList->clear();
|
||||
const Stream *s = m_store->streamById(m_selectedStreamId);
|
||||
if (s)
|
||||
{
|
||||
for (const QString &email : s->memberEmails)
|
||||
ui->membersList->addItem(email.split("@")[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
m_authManager->tryRestoreSession();
|
||||
|
||||
setupTrayIcon();
|
||||
@@ -286,6 +340,12 @@ void MainWindow::onParticleSelectionChanged(const QItemSelection &selected, cons
|
||||
{
|
||||
ui->label->setText("");
|
||||
}
|
||||
|
||||
// Mark seen on selection
|
||||
if (!particle->seen)
|
||||
{
|
||||
m_store->markParticleSeen(particle->id);
|
||||
}
|
||||
}
|
||||
|
||||
updateStatusBar();
|
||||
@@ -407,6 +467,11 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event)
|
||||
showTextInputDialog();
|
||||
return true;
|
||||
}
|
||||
if (keyEvent->key() == Qt::Key_A && keyEvent->modifiers() == Qt::NoModifier && !m_selectedParticleId.isEmpty())
|
||||
{
|
||||
m_store->ackParticle(m_selectedParticleId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QMainWindow::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ class QLabel;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class AuthDialog;
|
||||
class CreateStreamDialog;
|
||||
class MemberPickerDialog;
|
||||
class SettingsDialog;
|
||||
class TextInputDialog;
|
||||
|
||||
@@ -62,6 +64,7 @@ private:
|
||||
|
||||
SettingsDialog *m_settingsDialog;
|
||||
TextInputDialog *m_textInputDialog;
|
||||
CreateStreamDialog *m_createStreamDialog;
|
||||
|
||||
// Data and models
|
||||
Store *m_store;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "createstreamdialog.h"
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
CreateStreamDialog::CreateStreamDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle("New Stream");
|
||||
setMinimumWidth(320);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
|
||||
layout->addWidget(new QLabel("Stream name:"));
|
||||
m_nameEdit = new QLineEdit(this);
|
||||
m_nameEdit->setPlaceholderText("e.g. design-review");
|
||||
layout->addWidget(m_nameEdit);
|
||||
|
||||
layout->addWidget(new QLabel("Visibility:"));
|
||||
m_visibilityCombo = new QComboBox(this);
|
||||
m_visibilityCombo->addItem("Everyone in network", "network_all");
|
||||
m_visibilityCombo->addItem("Specific members", "custom");
|
||||
layout->addWidget(m_visibilityCombo);
|
||||
|
||||
m_membersLabel = new QLabel("Select members:");
|
||||
m_membersList = new QListWidget(this);
|
||||
m_membersLabel->setVisible(false);
|
||||
m_membersList->setVisible(false);
|
||||
layout->addWidget(m_membersLabel);
|
||||
layout->addWidget(m_membersList);
|
||||
|
||||
connect(m_visibilityCombo, &QComboBox::currentIndexChanged, this, [this](int) {
|
||||
bool specific = (m_visibilityCombo->currentData().toString() == "custom");
|
||||
m_membersLabel->setVisible(specific);
|
||||
m_membersList->setVisible(specific);
|
||||
});
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, [this]() {
|
||||
QString name = m_nameEdit->text().trimmed();
|
||||
if (name.isEmpty())
|
||||
return;
|
||||
|
||||
QString visibility = m_visibilityCombo->currentData().toString();
|
||||
QStringList members;
|
||||
if (visibility == "custom")
|
||||
{
|
||||
for (int i = 0; i < m_membersList->count(); ++i)
|
||||
{
|
||||
auto *item = m_membersList->item(i);
|
||||
if (item->checkState() == Qt::Checked)
|
||||
members.append(item->data(Qt::UserRole).toString());
|
||||
}
|
||||
}
|
||||
|
||||
emit streamRequested(name, visibility, members);
|
||||
m_nameEdit->clear();
|
||||
m_visibilityCombo->setCurrentIndex(0);
|
||||
accept();
|
||||
});
|
||||
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
void CreateStreamDialog::setNetworkMembers(const std::vector<Human> &members)
|
||||
{
|
||||
m_membersList->clear();
|
||||
for (const auto &human : members)
|
||||
{
|
||||
auto *item = new QListWidgetItem(human.email, m_membersList);
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(Qt::Unchecked);
|
||||
item->setData(Qt::UserRole, human.email);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef CREATESTREAMDIALOG_H
|
||||
#define CREATESTREAMDIALOG_H
|
||||
|
||||
#include "models.h"
|
||||
#include <QDialog>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QLineEdit;
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class CreateStreamDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CreateStreamDialog(QWidget *parent = nullptr);
|
||||
|
||||
/// Call before showing to populate the member checklist.
|
||||
void setNetworkMembers(const std::vector<Human> &members);
|
||||
|
||||
signals:
|
||||
void streamRequested(const QString &name, const QString &visibility, const QStringList &memberEmails);
|
||||
|
||||
private:
|
||||
QLineEdit *m_nameEdit;
|
||||
QComboBox *m_visibilityCombo;
|
||||
QLabel *m_membersLabel;
|
||||
QListWidget *m_membersList;
|
||||
};
|
||||
|
||||
#endif // CREATESTREAMDIALOG_H
|
||||
@@ -0,0 +1,55 @@
|
||||
#include "memberpickerdialog.h"
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QSet>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
MemberPickerDialog::MemberPickerDialog(const QString &title, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle(title);
|
||||
setMinimumWidth(300);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
|
||||
m_list = new QListWidget(this);
|
||||
layout->addWidget(m_list);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
void MemberPickerDialog::setMembers(const std::vector<Human> &members, const std::vector<QString> &exclude)
|
||||
{
|
||||
m_list->clear();
|
||||
|
||||
QSet<QString> excludeSet;
|
||||
for (const auto &e : exclude)
|
||||
excludeSet.insert(e);
|
||||
|
||||
for (const auto &human : members)
|
||||
{
|
||||
if (excludeSet.contains(human.email))
|
||||
continue;
|
||||
auto *item = new QListWidgetItem(human.email, m_list);
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(Qt::Unchecked);
|
||||
item->setData(Qt::UserRole, human.email);
|
||||
}
|
||||
}
|
||||
|
||||
QStringList MemberPickerDialog::selectedEmails() const
|
||||
{
|
||||
QStringList result;
|
||||
for (int i = 0; i < m_list->count(); ++i)
|
||||
{
|
||||
auto *item = m_list->item(i);
|
||||
if (item->checkState() == Qt::Checked)
|
||||
result.append(item->data(Qt::UserRole).toString());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef MEMBERPICKERDIALOG_H
|
||||
#define MEMBERPICKERDIALOG_H
|
||||
|
||||
#include "models.h"
|
||||
#include <QDialog>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QListWidget;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
/// A simple dialog that shows a checklist of humans and returns the selected emails.
|
||||
class MemberPickerDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MemberPickerDialog(const QString &title, QWidget *parent = nullptr);
|
||||
|
||||
/// Populate the checklist. Members whose emails appear in `exclude` are hidden.
|
||||
void setMembers(const std::vector<Human> &members, const std::vector<QString> &exclude = {});
|
||||
|
||||
/// Returns the emails of all checked items.
|
||||
QStringList selectedEmails() const;
|
||||
|
||||
private:
|
||||
QListWidget *m_list;
|
||||
};
|
||||
|
||||
#endif // MEMBERPICKERDIALOG_H
|
||||
@@ -12,6 +12,26 @@ NetworkStreamModel::NetworkStreamModel(Store *store, QObject *parent) : QAbstrac
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
});
|
||||
|
||||
// Granular update: repaint a single stream row when its properties change (e.g. unseen count)
|
||||
connect(m_store, &Store::streamUpdated, this, [this](const QString &streamId) {
|
||||
const auto &networks = m_store->networks();
|
||||
for (int ni = 0; ni < networks.size(); ++ni)
|
||||
{
|
||||
const auto &streams = networks.at(ni).streams;
|
||||
for (int si = 0; si < static_cast<int>(streams.size()); ++si)
|
||||
{
|
||||
if (streams[si].id == streamId)
|
||||
{
|
||||
// Build indices for both columns of this stream row
|
||||
QModelIndex topLeft = index(si, 0, index(ni, 0));
|
||||
QModelIndex bottomRight = index(si, 1, index(ni, 0));
|
||||
emit dataChanged(topLeft, bottomRight, {Qt::DisplayRole, Qt::ForegroundRole});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QModelIndex NetworkStreamModel::index(int row, int column, const QModelIndex &parent) const
|
||||
|
||||
@@ -15,6 +15,22 @@ ParticleListModel::ParticleListModel(Store *store, QObject *parent)
|
||||
endResetModel();
|
||||
}
|
||||
});
|
||||
|
||||
// Granular update: repaint a single row when a particle property changes
|
||||
connect(m_store, &Store::particleUpdated, this, [this](const QString &streamId, const QString &particleId) {
|
||||
if (streamId != m_currentStreamId)
|
||||
return;
|
||||
const QList<Particle> &particles = m_store->particlesForStream(m_currentStreamId);
|
||||
for (int i = 0; i < particles.size(); ++i)
|
||||
{
|
||||
if (particles.at(i).id == particleId)
|
||||
{
|
||||
QModelIndex idx = index(i, 0);
|
||||
emit dataChanged(idx, idx, {Qt::DisplayRole, Qt::ForegroundRole});
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
int ParticleListModel::rowCount(const QModelIndex &parent) const
|
||||
|
||||
+70
-150
@@ -10,35 +10,14 @@
|
||||
// Operation
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
Operation::Operation(QObject *parent)
|
||||
Operation::Operation(QNetworkReply *reply, QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
connect(this, &Operation::finished, this, &QObject::deleteLater);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Store — construction
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
|
||||
Store::Store(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Operation helpers
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
Operation *Store::makeOperation()
|
||||
{
|
||||
auto *op = new Operation(this);
|
||||
connect(op, &Operation::finished, op, &QObject::deleteLater);
|
||||
return op;
|
||||
}
|
||||
|
||||
void Store::wireReply(QNetworkReply *reply, Operation *op,
|
||||
std::function<void(const QJsonDocument &)> successHandler)
|
||||
{
|
||||
connect(reply, &QNetworkReply::finished, this, [reply, op, successHandler = std::move(successHandler)]() {
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
QByteArray body = reply->readAll();
|
||||
@@ -48,23 +27,26 @@ void Store::wireReply(QNetworkReply *reply, Operation *op,
|
||||
{
|
||||
message = reply->errorString();
|
||||
}
|
||||
emit op->failed(message);
|
||||
emit op->finished();
|
||||
qWarning() << message;
|
||||
emit failed(message);
|
||||
emit finished();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray body = reply->readAll();
|
||||
QJsonDocument doc = body.isEmpty() ? QJsonDocument() : QJsonDocument::fromJson(body);
|
||||
successHandler(doc);
|
||||
emit success(doc);
|
||||
emit finished();
|
||||
});
|
||||
}
|
||||
|
||||
void Store::wireReply(QNetworkReply *reply, Operation *op)
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Store — construction
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
Store::Store(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
wireReply(reply, op, [op](const QJsonDocument &) {
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
@@ -381,23 +363,18 @@ const Particle *Store::particleById(const QString &id) const
|
||||
|
||||
Operation *Store::loadStartupData()
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().get("/startup");
|
||||
|
||||
wireReply(reply, op, [this, op](const QJsonDocument &doc) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this](const QJsonDocument &doc) {
|
||||
if (!doc.isObject())
|
||||
{
|
||||
emit op->failed("Invalid startup data");
|
||||
emit op->finished();
|
||||
qWarning() << "Invalid startup data";
|
||||
return;
|
||||
}
|
||||
populateFromStartupData(doc.object());
|
||||
emit startupDataLoaded();
|
||||
emit networksChanged();
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
@@ -407,51 +384,41 @@ Operation *Store::loadStartupData()
|
||||
|
||||
Operation *Store::createNetwork(const QString &name)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
body["name"] = name;
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().post("/networks", QJsonDocument(body));
|
||||
|
||||
wireReply(reply, op, [this, op](const QJsonDocument &doc) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this](const QJsonDocument &doc) {
|
||||
if (doc.isObject())
|
||||
{
|
||||
upsertNetwork(parseNetwork(doc.object()));
|
||||
emit networksChanged();
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::fetchNetworks()
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().get("/networks");
|
||||
|
||||
wireReply(reply, op, [this, op](const QJsonDocument &doc) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this](const QJsonDocument &doc) {
|
||||
const QJsonArray arr = doc.array();
|
||||
for (const QJsonValue &v : arr)
|
||||
{
|
||||
upsertNetwork(parseNetwork(v.toObject()));
|
||||
}
|
||||
emit networksChanged();
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::fetchNetwork(const QString &networkId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().get(QString("/networks/%1").arg(networkId));
|
||||
|
||||
wireReply(reply, op, [this, op, networkId](const QJsonDocument &doc) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this, networkId](const QJsonDocument &doc) {
|
||||
QJsonObject obj = doc.object();
|
||||
Network network = parseNetwork(obj);
|
||||
|
||||
@@ -464,17 +431,12 @@ Operation *Store::fetchNetwork(const QString &networkId)
|
||||
}
|
||||
upsertNetwork(network);
|
||||
emit networksChanged();
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::addNetworkMembers(const QString &networkId, const QStringList &emails)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
QJsonArray arr;
|
||||
for (const QString &e : emails)
|
||||
@@ -485,30 +447,24 @@ Operation *Store::addNetworkMembers(const QString &networkId, const QStringList
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/networks/%1/members").arg(networkId), QJsonDocument(body));
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
return new Operation(reply, this);
|
||||
}
|
||||
|
||||
Operation *Store::removeNetworkMember(const QString &networkId, const QString &email)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().del(
|
||||
QString("/networks/%1/members/%2").arg(networkId, email));
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
return new Operation(reply, this);
|
||||
}
|
||||
|
||||
Operation *Store::setStreamCapacity(const QString &networkId, int capacity)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
body["capacity"] = capacity;
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().put(
|
||||
QString("/networks/%1/capacity").arg(networkId), QJsonDocument(body));
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
return new Operation(reply, this);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
@@ -519,8 +475,6 @@ Operation *Store::createStream(const QString &networkId, const QString &name,
|
||||
const QString &description, const QString &visibility,
|
||||
const QStringList &members)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
body["name"] = name;
|
||||
body["description"] = description;
|
||||
@@ -534,27 +488,23 @@ Operation *Store::createStream(const QString &networkId, const QString &name,
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/networks/%1/streams").arg(networkId), QJsonDocument(body));
|
||||
|
||||
wireReply(reply, op, [this, op, networkId](const QJsonDocument &doc) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this, networkId](const QJsonDocument &doc) {
|
||||
if (doc.isObject())
|
||||
{
|
||||
Stream stream = parseStream(doc.object());
|
||||
upsertStream(networkId, stream);
|
||||
emit streamsChanged(networkId);
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::fetchStream(const QString &streamId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().get(QString("/streams/%1").arg(streamId));
|
||||
|
||||
wireReply(reply, op, [this, op, streamId](const QJsonDocument &doc) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this, streamId](const QJsonDocument &doc) {
|
||||
QJsonObject obj = doc.object();
|
||||
Stream stream = parseStream(obj);
|
||||
|
||||
@@ -574,18 +524,12 @@ Operation *Store::fetchStream(const QString &streamId)
|
||||
}
|
||||
m_particles[streamId] = particleList;
|
||||
emit particlesChanged(streamId);
|
||||
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::updateStream(const QString &streamId, const QString &name, const QString &description)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
if (!name.isEmpty())
|
||||
{
|
||||
@@ -598,8 +542,8 @@ Operation *Store::updateStream(const QString &streamId, const QString &name, con
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().patch(
|
||||
QString("/streams/%1").arg(streamId), QJsonDocument(body));
|
||||
|
||||
wireReply(reply, op, [this, op, streamId](const QJsonDocument &doc) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this, streamId](const QJsonDocument &doc) {
|
||||
if (doc.isObject())
|
||||
{
|
||||
Stream stream = parseStream(doc.object());
|
||||
@@ -610,20 +554,16 @@ Operation *Store::updateStream(const QString &streamId, const QString &name, con
|
||||
emit streamsChanged(networkId);
|
||||
}
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::openStream(const QString &streamId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/streams/%1/open").arg(streamId), QJsonDocument());
|
||||
|
||||
wireReply(reply, op, [this, op, streamId](const QJsonDocument &) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this, streamId](const QJsonDocument &) {
|
||||
QString networkId = m_streamToNetwork.value(streamId);
|
||||
if (m_networkIndex.contains(networkId))
|
||||
{
|
||||
@@ -638,20 +578,16 @@ Operation *Store::openStream(const QString &streamId)
|
||||
}
|
||||
emit streamsChanged(networkId);
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::closeStream(const QString &streamId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/streams/%1/close").arg(streamId), QJsonDocument());
|
||||
|
||||
wireReply(reply, op, [this, op, streamId](const QJsonDocument &) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this, streamId](const QJsonDocument &) {
|
||||
QString networkId = m_streamToNetwork.value(streamId);
|
||||
if (m_networkIndex.contains(networkId))
|
||||
{
|
||||
@@ -666,17 +602,12 @@ Operation *Store::closeStream(const QString &streamId)
|
||||
}
|
||||
emit streamsChanged(networkId);
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::addStreamMembers(const QString &streamId, const QStringList &emails)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
QJsonArray arr;
|
||||
for (const QString &e : emails)
|
||||
@@ -687,14 +618,11 @@ Operation *Store::addStreamMembers(const QString &streamId, const QStringList &e
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/streams/%1/members").arg(streamId), QJsonDocument(body));
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
return new Operation(reply, this);
|
||||
}
|
||||
|
||||
Operation *Store::removeStreamMembers(const QString &streamId, const QStringList &emails)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
QJsonArray arr;
|
||||
for (const QString &e : emails)
|
||||
@@ -703,8 +631,7 @@ Operation *Store::removeStreamMembers(const QString &streamId, const QStringList
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().del(
|
||||
QString("/streams/%1/members").arg(streamId), QJsonDocument(body));
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
return new Operation(reply, this);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
@@ -714,8 +641,6 @@ Operation *Store::removeStreamMembers(const QString &streamId, const QStringList
|
||||
Operation *Store::createParticle(const QString &streamId, const QString &type,
|
||||
const QMap<QString, QVariant> &data)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
body["type"] = type;
|
||||
QJsonObject dataObj;
|
||||
@@ -725,27 +650,23 @@ Operation *Store::createParticle(const QString &streamId, const QString &type,
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/streams/%1/particles").arg(streamId), QJsonDocument(body));
|
||||
|
||||
wireReply(reply, op, [this, op, streamId](const QJsonDocument &doc) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this, streamId](const QJsonDocument &doc) {
|
||||
if (doc.isObject())
|
||||
{
|
||||
Particle particle = parseParticle(doc.object());
|
||||
upsertParticle(streamId, particle);
|
||||
emit particlesChanged(streamId);
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::fetchParticle(const QString &particleId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().get(QString("/particles/%1").arg(particleId));
|
||||
|
||||
wireReply(reply, op, [this, op, particleId](const QJsonDocument &doc) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this, particleId](const QJsonDocument &doc) {
|
||||
if (doc.isObject())
|
||||
{
|
||||
Particle particle = parseParticle(doc.object());
|
||||
@@ -756,17 +677,12 @@ Operation *Store::fetchParticle(const QString &particleId)
|
||||
emit particlesChanged(streamId);
|
||||
}
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::updateParticle(const QString &particleId, const QMap<QString, QVariant> &data)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
QJsonObject dataObj;
|
||||
for (auto it = data.constBegin(); it != data.constEnd(); ++it)
|
||||
@@ -775,8 +691,8 @@ Operation *Store::updateParticle(const QString &particleId, const QMap<QString,
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().patch(
|
||||
QString("/particles/%1").arg(particleId), QJsonDocument(body));
|
||||
|
||||
wireReply(reply, op, [this, op, particleId](const QJsonDocument &doc) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this, particleId](const QJsonDocument &doc) {
|
||||
if (doc.isObject())
|
||||
{
|
||||
Particle particle = parseParticle(doc.object());
|
||||
@@ -787,27 +703,20 @@ Operation *Store::updateParticle(const QString &particleId, const QMap<QString,
|
||||
emit particlesChanged(streamId);
|
||||
}
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::deleteParticle(const QString &particleId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().del(QString("/particles/%1").arg(particleId));
|
||||
|
||||
wireReply(reply, op, [this, op, particleId](const QJsonDocument &) {
|
||||
auto *op = new Operation(reply, this);
|
||||
connect(op, &Operation::success, this, [this, particleId](const QJsonDocument &) {
|
||||
QString streamId = findStreamForParticle(particleId);
|
||||
removeParticleFromState(particleId);
|
||||
if (!streamId.isEmpty())
|
||||
emit particlesChanged(streamId);
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
@@ -818,20 +727,36 @@ Operation *Store::markParticleSeen(const QString &particleId)
|
||||
{
|
||||
for (int i = 0; i < it.value().size(); ++i)
|
||||
{
|
||||
if (it.value()[i].id == particleId)
|
||||
if (it.value()[i].id == particleId && !it.value()[i].seen)
|
||||
{
|
||||
it.value()[i].seen = true;
|
||||
emit particlesChanged(it.key());
|
||||
QString streamId = it.key();
|
||||
|
||||
// Decrement the parent stream's unseen count
|
||||
QString networkId = m_streamToNetwork.value(streamId);
|
||||
if (m_networkIndex.contains(networkId))
|
||||
{
|
||||
auto &streams = m_networks[m_networkIndex[networkId]].streams;
|
||||
for (auto &s : streams)
|
||||
{
|
||||
if (s.id == streamId && s.unseenCount > 0)
|
||||
{
|
||||
s.unseenCount--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit particleUpdated(streamId, particleId);
|
||||
emit streamUpdated(streamId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/particles/%1/seen").arg(particleId), QJsonDocument());
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
return new Operation(reply, this);
|
||||
}
|
||||
|
||||
Operation *Store::markParticlesSeen(const QStringList &particleIds)
|
||||
@@ -856,8 +781,6 @@ Operation *Store::markParticlesSeen(const QStringList &particleIds)
|
||||
for (const QString &streamId : affectedStreams)
|
||||
emit particlesChanged(streamId);
|
||||
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
QJsonArray ids;
|
||||
for (const QString &id : particleIds)
|
||||
@@ -865,15 +788,12 @@ Operation *Store::markParticlesSeen(const QStringList &particleIds)
|
||||
body["particle_ids"] = ids;
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().post("/particles/seen", QJsonDocument(body));
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
return new Operation(reply, this);
|
||||
}
|
||||
|
||||
Operation *Store::ackParticle(const QString &particleId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/particles/%1/ack").arg(particleId), QJsonDocument());
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
return new Operation(reply, this);
|
||||
}
|
||||
|
||||
+8
-17
@@ -6,21 +6,22 @@
|
||||
#include <QMap>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QJsonDocument;
|
||||
class QNetworkReply;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
/// Represents an in-flight API operation.
|
||||
/// Callers connect to success/failed for one-off feedback.
|
||||
/// Wraps a QNetworkReply: owns cleanup, parses JSON, and emits signals.
|
||||
/// Automatically deletes itself after finished() fires.
|
||||
class Operation : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Operation(QObject *parent = nullptr);
|
||||
explicit Operation(QNetworkReply *reply, QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void success();
|
||||
void success(const QJsonDocument &response);
|
||||
void failed(const QString &message);
|
||||
void finished();
|
||||
};
|
||||
@@ -91,6 +92,10 @@ signals:
|
||||
void streamsChanged(const QString &networkId);
|
||||
void particlesChanged(const QString &streamId);
|
||||
|
||||
// Granular signals for in-place property updates (no structural change)
|
||||
void particleUpdated(const QString &streamId, const QString &particleId);
|
||||
void streamUpdated(const QString &streamId);
|
||||
|
||||
void startupDataLoaded();
|
||||
void errorOccurred(const QString &message);
|
||||
|
||||
@@ -120,20 +125,6 @@ private:
|
||||
/// Returns the streamId that contains the given particle, or empty QString.
|
||||
QString findStreamForParticle(const QString &particleId) const;
|
||||
|
||||
// ── Operation helpers ──
|
||||
|
||||
/// Creates an Operation parented to this Store, wired to self-destruct after finished().
|
||||
Operation *makeOperation();
|
||||
|
||||
/// Connects reply->finished to a handler. On error, emits op->failed + op->finished.
|
||||
/// On success, calls successHandler with the parsed QJsonDocument.
|
||||
/// successHandler is responsible for emitting op->success() and op->finished().
|
||||
void wireReply(QNetworkReply *reply, Operation *op,
|
||||
std::function<void(const QJsonDocument &)> successHandler);
|
||||
|
||||
/// Simplified wireReply for operations that don't need to parse a response body
|
||||
/// (e.g. 204 No Content). Emits op->success() and op->finished() automatically.
|
||||
void wireReply(QNetworkReply *reply, Operation *op);
|
||||
};
|
||||
|
||||
#endif // STORE_H
|
||||
|
||||
Reference in New Issue
Block a user