From c92531e8f0d71d7fe19a332705e24e64d2311675 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:16:18 +0100 Subject: [PATCH 01/11] improve circle editor Added Status column to show the status of the user --- .../src/gui/Circles/CreateCircleDialog.cpp | 186 +++++++++++++++++- .../src/gui/Circles/CreateCircleDialog.h | 14 ++ .../src/gui/Circles/CreateCircleDialog.ui | 5 + 3 files changed, 202 insertions(+), 3 deletions(-) diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp index 52fa8034f..54727a2f7 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp @@ -46,8 +46,13 @@ #define CREATECIRCLEDIALOG_IDINFO 3 #define RSCIRCLEID_COL_NICKNAME 0 -#define RSCIRCLEID_COL_IDTYPE 1 -#define RSCIRCLEID_COL_KEYID 2 +#define RSCIRCLEID_COL_KEYID 1 +#define RSCIRCLEID_COL_STATUS 2 +#define RSCIRCLEID_COL_IDTYPE 3 + +#define IMAGE_INVITED ":/icons/bullet_yellow_128.png" +#define IMAGE_MEMBER ":/icons/bullet_green_128.png" +#define IMAGE_UNKNOWN ":/icons/bullet_grey_128.png" /** Constructor */ CreateCircleDialog::CreateCircleDialog() @@ -355,13 +360,25 @@ void CreateCircleDialog::addMember(const QString& keyId, const QString& idtype, member->setText(RSCIRCLEID_COL_NICKNAME, nickname); member->setIcon(RSCIRCLEID_COL_NICKNAME, icon); member->setText(RSCIRCLEID_COL_KEYID, keyId); - member->setText(RSCIRCLEID_COL_IDTYPE, idtype); + //member->setText(RSCIRCLEID_COL_IDTYPE, idtype); tree->addTopLevelItem(member); updateMembership(); } +void CreateCircleDialog::addMember(const QString &keyId, const QString &idtype, + const QString &nickname, const QIcon &icon, + bool invited, bool subscrb, bool is_own_id) +{ + QTreeWidgetItem *subitem = new QTreeWidgetItem(ui.treeWidget_membership); + subitem->setText(RSCIRCLEID_COL_NICKNAME, nickname); + subitem->setIcon(RSCIRCLEID_COL_NICKNAME, icon); + subitem->setText(RSCIRCLEID_COL_KEYID, keyId); + + updateMemberStatus(subitem, invited, subscrb, is_own_id); +} + /** Maybe we can use RsGxsCircleGroup instead of RsGxsCircleDetails ??? (TODO)**/ void CreateCircleDialog::addCircle(const RsGxsCircleDetails &cirDetails) { @@ -949,14 +966,177 @@ void CreateCircleDialog::MembershipListCustomPopupMenu( QPoint ) QMenu contextMnu( this ); QTreeWidgetItem *item = ui.treeWidget_membership->currentItem(); + + RsGxsId current_gxs_id(item->text(RSCIRCLEID_COL_KEYID).toStdString()); + RsIdentityDetails idd; + bool is_my_own_id = rsIdentity->getIdDetails(current_gxs_id, idd); + if (item && !mReadOnly) contextMnu.addAction(QIcon(":/images/delete.png"), tr("Remove Member"), this, SLOT(removeMember())); + if (is_my_own_id && item->text(RSCIRCLEID_COL_STATUS) == tr("Invited")) { + contextMnu.addAction(tr("Accept Invite"), this, SLOT(acceptInvite())); + contextMnu.addAction(tr("Reject Invite"), this, SLOT(rejectInvite())); + contextMnu.addSeparator(); + } + + if (am_I_circle_admin) + { + RsGxsCircleDetails details; + if (rsGxsCircles->getCircleDetails(RsGxsCircleId(mCircleGroup.mMeta.mGroupId), details)) + { + auto it = details.mSubscriptionFlags.find(current_gxs_id); + if (it != details.mSubscriptionFlags.end()) + { + contextMnu.addSeparator(); + + if (it->second & GXS_EXTERNAL_CIRCLE_FLAGS_IN_ADMIN_LIST) + { + QAction *action = new QAction(tr("Revoke this member"), this); + connect(action, &QAction::triggered, this, &CreateCircleDialog::revokeCircleMembership); + contextMnu.addAction(action); + } + else + { + QAction *action = new QAction(tr("Grant membership"), this); + connect(action, &QAction::triggered, this, &CreateCircleDialog::grantCircleMembership); + contextMnu.addAction(action); + } + } + } + } + contextMnu.exec(QCursor::pos()); } void CreateCircleDialog::updateMembership() { ui.members_groupBox->setTitle( tr("Invited Members") + " (" + QString::number(ui.treeWidget_membership->topLevelItemCount()) + ")" ); + + RsGxsCircleDetails details; + if (!rsGxsCircles->getCircleDetails(RsGxsCircleId(mCircleGroup.mMeta.mGroupId), details)) { + return; + } + + // Iterate through the subscription flags map found in details + for(auto const& [memberId, flags] : details.mSubscriptionFlags) + { + // Find or create your tree widget item + QString memberIdStr = QString::fromStdString(memberId.toStdString()); + QTreeWidgetItem* item = nullptr; + + // Search for the existing item in the tree + for(int i = 0; i < ui.treeWidget_membership->topLevelItemCount(); ++i) { + if(ui.treeWidget_membership->topLevelItem(i)->text(RSCIRCLEID_COL_KEYID) == memberIdStr) { + item = ui.treeWidget_membership->topLevelItem(i); + break; + } + } + if(!item) continue; + + // Use the flags defined in rsgxscircles.h + bool invited = (flags & GXS_EXTERNAL_CIRCLE_FLAGS_IN_ADMIN_LIST); + bool subscrb = (flags & GXS_EXTERNAL_CIRCLE_FLAGS_SUBSCRIBED); + + // Check if this ID is one of your own + RsIdentityDetails idd; + bool is_own_id = rsIdentity->getIdDetails(memberId, idd); + //am_I_circle_admin = (mCircleGroup.mMeta.mAuthorId == ownId); + + updateMemberStatus(item, invited, subscrb, is_own_id); + } } +void CreateCircleDialog::updateMemberStatus(QTreeWidgetItem* subitem, bool invited, bool subscrb, bool is_own_id) +{ + QString statusText; + QString tooltip = tr("Status: "); + + if (invited && !subscrb) { + // Case: Admin invited them, but they haven't accepted yet + statusText = tr("Invited"); + tooltip += tr("Invited by admin"); + subitem->setIcon(RSCIRCLEID_COL_STATUS, FilesDefs::getIconFromQtResourcePath(IMAGE_INVITED)) ; + } else if (!invited && subscrb) { + // Case: User requested to join, but Admin hasn't approved yet + statusText = tr("Subscription pending"); + tooltip += tr("Subscription request pending"); + if (is_own_id) am_I_pending = true; + subitem->setIcon(RSCIRCLEID_COL_STATUS, FilesDefs::getIconFromQtResourcePath(IMAGE_INVITED)) ; + } else if (invited && subscrb) { + statusText = tr("Member"); + tooltip += tr("Full member"); + subitem->setIcon(RSCIRCLEID_COL_STATUS, FilesDefs::getIconFromQtResourcePath(IMAGE_MEMBER)) ; + } else { + statusText = tr("Unknown"); + tooltip += tr("unknown"); + subitem->setIcon(RSCIRCLEID_COL_STATUS, FilesDefs::getIconFromQtResourcePath(IMAGE_UNKNOWN)) ; + } + + subitem->setText(RSCIRCLEID_COL_STATUS, statusText); + subitem->setToolTip(RSCIRCLEID_COL_STATUS, tooltip); + + // Apply bold font for own ID + QFont font = ui.treeWidget_membership->font(); + font.setBold(is_own_id); + for(int i=0; icolumnCount(); ++i) { + subitem->setFont(i, font); + } +} + +void CreateCircleDialog::acceptInvite() +{ + QTreeWidgetItem *item = ui.treeWidget_membership->currentItem(); + if (!item) return; + + RsGxsCircleId circleId(item->text(RSCIRCLEID_COL_KEYID).toStdString()); + // Use the chosen identity from your idChooser to accept the invite + RsGxsId ownId; + if (ui.idChooser->getChosenId(ownId) == GxsIdChooser::KnowId) { + rsGxsCircles->requestCircleMembership(ownId, circleId); // true to subscribe + } +} + +void CreateCircleDialog::rejectInvite() +{ + QTreeWidgetItem *item = ui.treeWidget_membership->currentItem(); + if (!item) return; + + RsGxsCircleId circleId(item->text(RSCIRCLEID_COL_KEYID).toStdString()); + RsGxsId ownId; + if (ui.idChooser->getChosenId(ownId) == GxsIdChooser::KnowId) { + rsGxsCircles->cancelCircleMembership(ownId, circleId); + } +} + +void CreateCircleDialog::grantCircleMembership() +{ + QTreeWidgetItem *item = ui.treeWidget_membership->currentItem(); + if (!item) return; + + RsGxsCircleId circle_id(mCircleGroup.mMeta.mGroupId); + RsGxsId gxs_id_to_grant(item->text(RSCIRCLEID_COL_KEYID).toStdString()); + + if(circle_id.isNull() || gxs_id_to_grant.isNull()) return; + + RsThread::async([circle_id, gxs_id_to_grant]() { + rsGxsCircles->inviteIdsToCircle(std::set({ gxs_id_to_grant }), circle_id); + }); +} + +void CreateCircleDialog::revokeCircleMembership() +{ + QTreeWidgetItem *item = ui.treeWidget_membership->currentItem(); + if (!item) return; + + RsGxsCircleId circle_id(mCircleGroup.mMeta.mGroupId); + RsGxsId gxs_id_to_revoke(item->text(RSCIRCLEID_COL_KEYID).toStdString()); + + if(circle_id.isNull() || gxs_id_to_revoke.isNull()) return; + + RsThread::async([circle_id, gxs_id_to_revoke]() { + std::set ids; + ids.insert(gxs_id_to_revoke); + rsGxsCircles->revokeIdsFromCircle(ids, circle_id); + }); +} diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.h b/retroshare-gui/src/gui/Circles/CreateCircleDialog.h index ec078403c..7f697f6d8 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.h +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.h @@ -42,12 +42,20 @@ public: void addMember(const QString &keyId, const QString &idtype, const QString &nickname); void addMember(const RsGxsIdGroup &idGroup); void addCircle(const RsGxsCircleDetails &cirDetails); + void addMember(const QString &keyId, const QString &idtype, const QString &nickname, + const QIcon &icon, bool invited, bool subscrb, bool is_own_id); private slots: void addMember(); void removeMember(); + void acceptInvite(); + void rejectInvite(); + + void grantCircleMembership(); + void revokeCircleMembership(); + void updateCircleType(bool b); void selectedId(QTreeWidgetItem*, QTreeWidgetItem*); void selectedMember(QTreeWidgetItem*, QTreeWidgetItem*); @@ -91,6 +99,12 @@ private: RsGxsCircleGroup mCircleGroup; // for editting existing Circles. bool mClearList; + bool am_I_invited; // Tracks if the current user has an invitation to this circle + bool am_I_pending; // Tracks if the current user's subscription is pending + bool am_I_circle_admin; // Track admin status + + void updateMemberStatus(QTreeWidgetItem* item, bool invited, bool subscrb, bool is_own_id); + /** Qt Designer generated object */ Ui::CreateCircleDialog ui; }; diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.ui b/retroshare-gui/src/gui/Circles/CreateCircleDialog.ui index 62390e56a..4d1d11d0f 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.ui +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.ui @@ -280,6 +280,11 @@ IDs + + + Status + + From f0d35c1aef58091ccedb01baadb263ad0d34e8e9 Mon Sep 17 00:00:00 2001 From: jolavillette Date: Sun, 1 Mar 2026 14:03:34 +0100 Subject: [PATCH 02/11] Fix context menu and pending status logic for PR3174 --- .../src/gui/Circles/CreateCircleDialog.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp index 54727a2f7..7284527ab 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp @@ -980,6 +980,13 @@ void CreateCircleDialog::MembershipListCustomPopupMenu( QPoint ) contextMnu.addSeparator(); } + if (is_my_own_id && item->text(RSCIRCLEID_COL_STATUS) == tr("Subscription pending")) { + QAction *actionCancel = new QAction(tr("Cancel request"), this); + connect(actionCancel, &QAction::triggered, this, &CreateCircleDialog::rejectInvite); + contextMnu.addAction(actionCancel); + contextMnu.addSeparator(); + } + if (am_I_circle_admin) { RsGxsCircleDetails details; @@ -1001,6 +1008,10 @@ void CreateCircleDialog::MembershipListCustomPopupMenu( QPoint ) QAction *action = new QAction(tr("Grant membership"), this); connect(action, &QAction::triggered, this, &CreateCircleDialog::grantCircleMembership); contextMnu.addAction(action); + + QAction *actionReject = new QAction(tr("Reject request"), this); + connect(actionReject, &QAction::triggered, this, &CreateCircleDialog::revokeCircleMembership); + contextMnu.addAction(actionReject); } } } @@ -1041,7 +1052,11 @@ void CreateCircleDialog::updateMembership() // Check if this ID is one of your own RsIdentityDetails idd; bool is_own_id = rsIdentity->getIdDetails(memberId, idd); - //am_I_circle_admin = (mCircleGroup.mMeta.mAuthorId == ownId); + + RsGxsId ownId; + if (ui.idChooser->getChosenId(ownId) == GxsIdChooser::KnowId) { + am_I_circle_admin = (mCircleGroup.mMeta.mAuthorId == ownId); + } updateMemberStatus(item, invited, subscrb, is_own_id); } From aa6f8ec75f1eeabf2faa34f4dc2a7cbd4f1764b0 Mon Sep 17 00:00:00 2001 From: jolavillette Date: Tue, 3 Mar 2026 07:43:23 +0100 Subject: [PATCH 03/11] gui/circles: refresh CreateCircleDialog UI on membership updates --- .../src/gui/Circles/CreateCircleDialog.cpp | 81 ++++++++++++++++--- .../src/gui/Circles/CreateCircleDialog.h | 6 ++ 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp index 7284527ab..4d2625159 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp @@ -33,8 +33,8 @@ #include "gui/common/AvatarDefs.h" #include "gui/common/FilesDefs.h" #include "util/qtthreadsutils.h" -#include "util/misc.h" #include "gui/Circles/CreateCircleDialog.h" +#include #include "gui/gxs/GxsIdDetails.h" #include "gui/Identity/IdDialog.h" #include "gui/Identity/IdEditDialog.h" @@ -61,6 +61,11 @@ CreateCircleDialog::CreateCircleDialog() mIdentitiesLoading = false; mCircleLoading = false; mCloseRequested = false; + mEventHandlerId = 0; + + rsEvents->registerEventsHandler( + [this](std::shared_ptr event) { handleEvent(event); }, + mEventHandlerId, RsEventType::GXS_CIRCLES ); /* Invoke the Qt Designer generated object setup routine */ ui.setupUi(this); @@ -128,6 +133,8 @@ CreateCircleDialog::CreateCircleDialog() CreateCircleDialog::~CreateCircleDialog() { + if (mEventHandlerId != 0) + rsEvents->unregisterEventsHandler(mEventHandlerId); } bool CreateCircleDialog::tryClose() @@ -660,7 +667,10 @@ void CreateCircleDialog::updateCircleGUI() std::cerr << std::endl; #endif - whileBlocking(ui.circleName)->setText(QString::fromUtf8(mCircleGroup.mMeta.mGroupName.c_str())); +{ + QSignalBlocker blocker(ui.circleName); + ui.circleName->setText(QString::fromUtf8(mCircleGroup.mMeta.mGroupName.c_str())); + } bool isExternal = true; #ifdef DEBUG_CREATE_CIRCLE_DIALOG @@ -668,9 +678,18 @@ void CreateCircleDialog::updateCircleGUI() std::cerr << std::endl; #endif - whileBlocking(ui.radioButton_Public)->setChecked(false); - whileBlocking(ui.radioButton_Self)->setChecked(false); - whileBlocking(ui.radioButton_Restricted)->setChecked(false); + { + QSignalBlocker blocker1(ui.radioButton_Public); + ui.radioButton_Public->setChecked(false); + } + { + QSignalBlocker blocker2(ui.radioButton_Self); + ui.radioButton_Self->setChecked(false); + } + { + QSignalBlocker blocker3(ui.radioButton_Restricted); + ui.radioButton_Restricted->setChecked(false); + } switch(mCircleGroup.mMeta.mCircleType) { @@ -689,7 +708,10 @@ void CreateCircleDialog::updateCircleGUI() std::cerr << std::endl; #endif - whileBlocking(ui.radioButton_Public)->setChecked(true); + { + QSignalBlocker blocker(ui.radioButton_Public); + ui.radioButton_Public->setChecked(true); + } break; case GXS_CIRCLE_TYPE_EXT_SELF: @@ -703,11 +725,20 @@ void CreateCircleDialog::updateCircleGUI() #endif if (RsGxsGroupId(mCircleGroup.mMeta.mCircleId) == mCircleGroup.mMeta.mGroupId) - whileBlocking(ui.radioButton_Self)->setChecked(true); + { + QSignalBlocker blocker(ui.radioButton_Self); + ui.radioButton_Self->setChecked(true); + } else - whileBlocking(ui.radioButton_Restricted)->setChecked(true); + { + QSignalBlocker blocker(ui.radioButton_Restricted); + ui.radioButton_Restricted->setChecked(true); + } - whileBlocking(ui.circleComboBox)->loadCircles(mCircleGroup.mMeta.mCircleId); + { + QSignalBlocker blocker(ui.circleComboBox); + ui.circleComboBox->loadCircles(mCircleGroup.mMeta.mCircleId); + } break; @@ -1155,3 +1186,35 @@ void CreateCircleDialog::revokeCircleMembership() rsGxsCircles->revokeIdsFromCircle(ids, circle_id); }); } + +void CreateCircleDialog::handleEvent(std::shared_ptr event) +{ + RsQThreadUtils::postToObject( [=]() { handleEvent_main_thread(event); }, this ); +} + +void CreateCircleDialog::handleEvent_main_thread(std::shared_ptr event) +{ + if(event->mType == RsEventType::GXS_CIRCLES) + { + const RsGxsCircleEvent *pe = dynamic_cast(event.get()); + if(!pe) return; + + if(pe->mCircleId.isNull() || pe->mCircleId == RsGxsCircleId(mCircleGroup.mMeta.mGroupId)) + { + // Give it a moment as NewsFeed does to make sure the cache is populated + RsThread::async([this]() { + rstime::rs_usleep(500*1000); // 0.5s + RsQThreadUtils::postToObject( [this]() { + if(!mCircleLoading && !mIsExternalCircle && !mIsExistingCircle) + { + // Still building or whatever.. just update it directly if we're loaded + } + else if(mIsExistingCircle) + { + loadCircle(RsGxsGroupId(mCircleGroup.mMeta.mGroupId)); + } + }, this ); + }); + } + } +} diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.h b/retroshare-gui/src/gui/Circles/CreateCircleDialog.h index 7f697f6d8..98d831c6c 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.h +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.h @@ -70,6 +70,9 @@ private slots: void IdListCustomPopupMenu( QPoint point ); void MembershipListCustomPopupMenu( QPoint point); + void handleEvent(std::shared_ptr event); + + protected: virtual void keyPressEvent(QKeyEvent *e) override; virtual void accept() override; @@ -79,6 +82,7 @@ protected: private: void updateCircleGUI(); + void handleEvent_main_thread(std::shared_ptr event); void setupForPersonalCircle(); void setupForExternalCircle(); @@ -107,6 +111,8 @@ private: /** Qt Designer generated object */ Ui::CreateCircleDialog ui; + + RsEventsHandlerId_t mEventHandlerId; }; #endif From 1a8597177e261bde0f142f1f92212cc46c7389db Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:49:33 +0100 Subject: [PATCH 04/11] Fixed enable "Remove Member" only for Invited ones --- retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp index 4d2625159..0561c1595 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp @@ -1002,7 +1002,7 @@ void CreateCircleDialog::MembershipListCustomPopupMenu( QPoint ) RsIdentityDetails idd; bool is_my_own_id = rsIdentity->getIdDetails(current_gxs_id, idd); - if (item && !mReadOnly) + if (item && !mReadOnly && item->text(RSCIRCLEID_COL_STATUS) == tr("Invited")) contextMnu.addAction(QIcon(":/images/delete.png"), tr("Remove Member"), this, SLOT(removeMember())); if (is_my_own_id && item->text(RSCIRCLEID_COL_STATUS) == tr("Invited")) { From ed71618aa197706194f041bf033921723df31fe0 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:24:49 +0100 Subject: [PATCH 05/11] Rename Buton class, it gets issues with normal addbutton --- .../src/gui/Circles/CreateCircleDialog.cpp | 11 ++++++----- .../src/gui/Circles/CreateCircleDialog.ui | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp index 0561c1595..d3b95e1c2 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp @@ -90,7 +90,7 @@ CreateCircleDialog::CreateCircleDialog() ui.treeWidget_membership->setColumnWidth(RSCIRCLEID_COL_NICKNAME, 17 * fontWidth); ui.removeButton->setEnabled(false); - ui.addButton->setEnabled(false); + ui.addMemberButton->setEnabled(false); ui.radioButton_ListAll->setChecked(true); ui.radioButton_Public->setChecked(true) ; @@ -106,7 +106,7 @@ CreateCircleDialog::CreateCircleDialog() ui.circleComboBox->hide(); // connect up the buttons. - connect(ui.addButton, SIGNAL(clicked()), this, SLOT(addMember())); + connect(ui.addMemberButton, SIGNAL(clicked()), this, SLOT(addMember())); connect(ui.removeButton, SIGNAL(clicked()), this, SLOT(removeMember())); connect(ui.postButton, SIGNAL(clicked()), this, SLOT(createCircle())); @@ -202,7 +202,7 @@ void CreateCircleDialog::editExistingId(const RsGxsGroupId &circleId, const bool ui.postButton->setText(tr("Update")); - ui.addButton->setEnabled(!readonly) ; + ui.addMemberButton->setEnabled(!readonly) ; ui.removeButton->setEnabled(!readonly) ; if(readonly) @@ -210,7 +210,7 @@ void CreateCircleDialog::editExistingId(const RsGxsGroupId &circleId, const bool ui.postButton->hide() ; ui.cancelButton->setText(tr("Close")); ui.peersSelection_GB->hide() ; - ui.addButton->hide() ; + ui.addMemberButton->hide() ; ui.removeButton->hide() ; } @@ -299,7 +299,7 @@ void CreateCircleDialog::setupForExternalCircle() void CreateCircleDialog::selectedId(QTreeWidgetItem *current, QTreeWidgetItem *previous) { Q_UNUSED(previous); - ui.addButton->setEnabled(current != NULL); + ui.addMemberButton->setEnabled(current != NULL); } void CreateCircleDialog::selectedMember(QTreeWidgetItem *current, QTreeWidgetItem *previous) @@ -1097,6 +1097,7 @@ void CreateCircleDialog::updateMemberStatus(QTreeWidgetItem* subitem, bool invit { QString statusText; QString tooltip = tr("Status: "); + bool am_I_pending = false ; if (invited && !subscrb) { // Case: Admin invited them, but they haven't accepted yet diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.ui b/retroshare-gui/src/gui/Circles/CreateCircleDialog.ui index 4d1d11d0f..54e09c84a 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.ui +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.ui @@ -306,7 +306,13 @@ - + + + + 0 + 0 + + Add @@ -320,7 +326,13 @@ - + + + + 0 + 0 + + Remove From 490a18295ea6b7fd0ce7fcaf8232c4b3fcab9e8e Mon Sep 17 00:00:00 2001 From: jolavillette Date: Sat, 7 Mar 2026 12:21:11 +0100 Subject: [PATCH 06/11] gui/circles: dynamically add missing members from subscriptionFlags in circle editor UI --- .../src/gui/Circles/CreateCircleDialog.cpp | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp index 0561c1595..1bd675c34 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp @@ -1074,11 +1074,28 @@ void CreateCircleDialog::updateMembership() break; } } - if(!item) continue; - + // Use the flags defined in rsgxscircles.h bool invited = (flags & GXS_EXTERNAL_CIRCLE_FLAGS_IN_ADMIN_LIST); bool subscrb = (flags & GXS_EXTERNAL_CIRCLE_FLAGS_SUBSCRIBED); + + if(!item) { + // Find or create your tree widget item + RsIdentityDetails gxs_details; + if (rsIdentity->getIdDetails(memberId, gxs_details)) { + QString nickname = QString::fromUtf8(gxs_details.mNickname.c_str()); + QString idtype = tr("[Anonymous Id]"); + + QPixmap pixmap; + if(gxs_details.mAvatar.mSize == 0 || !GxsIdDetails::loadPixmapFromData(gxs_details.mAvatar.mData, gxs_details.mAvatar.mSize, pixmap, GxsIdDetails::SMALL)) + pixmap = GxsIdDetails::makeDefaultIcon(gxs_details.mId, GxsIdDetails::SMALL); + + // Add missing item using the overloaded addMember + addMember(memberIdStr, idtype, nickname, QIcon(pixmap), invited, subscrb, false); + } + continue; + } + // Check if this ID is one of your own RsIdentityDetails idd; From 3e182cc5c9de5aa7196c40453d936c030a8b4da7 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:59:37 +0100 Subject: [PATCH 07/11] Fixed update Membership counts * reorder columns --- retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp | 9 +++++---- retroshare-gui/src/gui/Circles/CreateCircleDialog.ui | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp index 9849c6bd8..56b00394d 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp @@ -46,8 +46,8 @@ #define CREATECIRCLEDIALOG_IDINFO 3 #define RSCIRCLEID_COL_NICKNAME 0 -#define RSCIRCLEID_COL_KEYID 1 -#define RSCIRCLEID_COL_STATUS 2 +#define RSCIRCLEID_COL_STATUS 1 +#define RSCIRCLEID_COL_KEYID 2 #define RSCIRCLEID_COL_IDTYPE 3 #define IMAGE_INVITED ":/icons/bullet_yellow_128.png" @@ -88,6 +88,7 @@ CreateCircleDialog::CreateCircleDialog() int fontWidth = QFontMetrics_horizontalAdvance(QFontMetricsF(ui.treeWidget_IdList->font()), "W"); ui.treeWidget_IdList->setColumnWidth(RSCIRCLEID_COL_NICKNAME, 17 * fontWidth); ui.treeWidget_membership->setColumnWidth(RSCIRCLEID_COL_NICKNAME, 17 * fontWidth); + ui.treeWidget_membership->setColumnWidth(RSCIRCLEID_COL_STATUS, 17 * fontWidth); ui.removeButton->setEnabled(false); ui.addMemberButton->setEnabled(false); @@ -1053,8 +1054,6 @@ void CreateCircleDialog::MembershipListCustomPopupMenu( QPoint ) void CreateCircleDialog::updateMembership() { - ui.members_groupBox->setTitle( tr("Invited Members") + " (" + QString::number(ui.treeWidget_membership->topLevelItemCount()) + ")" ); - RsGxsCircleDetails details; if (!rsGxsCircles->getCircleDetails(RsGxsCircleId(mCircleGroup.mMeta.mGroupId), details)) { return; @@ -1146,6 +1145,8 @@ void CreateCircleDialog::updateMemberStatus(QTreeWidgetItem* subitem, bool invit for(int i=0; icolumnCount(); ++i) { subitem->setFont(i, font); } + + ui.members_groupBox->setTitle( tr("Invited Members") + " (" + QString::number(ui.treeWidget_membership->topLevelItemCount()) + ")" ); } void CreateCircleDialog::acceptInvite() diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.ui b/retroshare-gui/src/gui/Circles/CreateCircleDialog.ui index 54e09c84a..26d49ee4f 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.ui +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.ui @@ -277,12 +277,12 @@ - IDs + Status - Status + IDs From 3b17e2fd6a0be6b30c2369b58100b5b0d59c526e Mon Sep 17 00:00:00 2001 From: jolavillette Date: Tue, 10 Mar 2026 05:20:16 +0100 Subject: [PATCH 08/11] gui/circles: fix unintended re-invitation of revoked members during circle update --- .../src/gui/Circles/CreateCircleDialog.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp index 56b00394d..132992efc 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp @@ -369,6 +369,7 @@ void CreateCircleDialog::addMember(const QString& keyId, const QString& idtype, member->setIcon(RSCIRCLEID_COL_NICKNAME, icon); member->setText(RSCIRCLEID_COL_KEYID, keyId); //member->setText(RSCIRCLEID_COL_IDTYPE, idtype); + member->setData(RSCIRCLEID_COL_STATUS, Qt::UserRole, true); // Manually added members are invited tree->addTopLevelItem(member); @@ -504,6 +505,7 @@ void CreateCircleDialog::createCircle() { QTreeWidgetItem *item = tree->topLevelItem(i); QString keyId = item->text(RSCIRCLEID_COL_KEYID); + bool is_invited = item->data(RSCIRCLEID_COL_STATUS, Qt::UserRole).toBool(); /* insert into circle */ if (mIsExternalCircle) @@ -516,9 +518,11 @@ void CreateCircleDialog::createCircle() continue ; } - circle.mInvitedMembers.insert(key_id_gxs) ; + if (is_invited) { + circle.mInvitedMembers.insert(key_id_gxs) ; + } #ifdef DEBUG_CREATE_CIRCLE_DIALOG - std::cerr << "CreateCircleDialog::createCircle() Inserting Member: " << keyId.toStdString(); + std::cerr << "CreateCircleDialog::createCircle() Inserting Member: " << keyId.toStdString() << " (invited: " << is_invited << ")"; std::cerr << std::endl; #endif } @@ -532,9 +536,11 @@ void CreateCircleDialog::createCircle() continue ; } - circle.mLocalFriends.insert(key_id_pgp) ; + if (is_invited) { + circle.mLocalFriends.insert(key_id_pgp) ; + } #ifdef DEBUG_CREATE_CIRCLE_DIALOG - std::cerr << "CreateCircleDialog::createCircle() Inserting Friend: " << keyId.toStdString(); + std::cerr << "CreateCircleDialog::createCircle() Inserting Friend: " << keyId.toStdString() << " (invited: " << is_invited << ")"; std::cerr << std::endl; #endif } @@ -1138,6 +1144,7 @@ void CreateCircleDialog::updateMemberStatus(QTreeWidgetItem* subitem, bool invit subitem->setText(RSCIRCLEID_COL_STATUS, statusText); subitem->setToolTip(RSCIRCLEID_COL_STATUS, tooltip); + subitem->setData(RSCIRCLEID_COL_STATUS, Qt::UserRole, invited); // Apply bold font for own ID QFont font = ui.treeWidget_membership->font(); From c00406ab5bdf2fd0d488bb70a6be95f2caecb1ca Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:45:07 +0100 Subject: [PATCH 09/11] context menu fixes --- .../src/gui/Circles/CreateCircleDialog.cpp | 130 +++++++++--------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp index 132992efc..3d5c12616 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp @@ -999,67 +999,11 @@ void CreateCircleDialog::IdListCustomPopupMenu( QPoint ) contextMnu.exec(QCursor::pos()); } -void CreateCircleDialog::MembershipListCustomPopupMenu( QPoint ) -{ - QMenu contextMnu( this ); - - QTreeWidgetItem *item = ui.treeWidget_membership->currentItem(); - - RsGxsId current_gxs_id(item->text(RSCIRCLEID_COL_KEYID).toStdString()); - RsIdentityDetails idd; - bool is_my_own_id = rsIdentity->getIdDetails(current_gxs_id, idd); - - if (item && !mReadOnly && item->text(RSCIRCLEID_COL_STATUS) == tr("Invited")) - contextMnu.addAction(QIcon(":/images/delete.png"), tr("Remove Member"), this, SLOT(removeMember())); - - if (is_my_own_id && item->text(RSCIRCLEID_COL_STATUS) == tr("Invited")) { - contextMnu.addAction(tr("Accept Invite"), this, SLOT(acceptInvite())); - contextMnu.addAction(tr("Reject Invite"), this, SLOT(rejectInvite())); - contextMnu.addSeparator(); - } - - if (is_my_own_id && item->text(RSCIRCLEID_COL_STATUS) == tr("Subscription pending")) { - QAction *actionCancel = new QAction(tr("Cancel request"), this); - connect(actionCancel, &QAction::triggered, this, &CreateCircleDialog::rejectInvite); - contextMnu.addAction(actionCancel); - contextMnu.addSeparator(); - } - - if (am_I_circle_admin) - { - RsGxsCircleDetails details; - if (rsGxsCircles->getCircleDetails(RsGxsCircleId(mCircleGroup.mMeta.mGroupId), details)) - { - auto it = details.mSubscriptionFlags.find(current_gxs_id); - if (it != details.mSubscriptionFlags.end()) - { - contextMnu.addSeparator(); - - if (it->second & GXS_EXTERNAL_CIRCLE_FLAGS_IN_ADMIN_LIST) - { - QAction *action = new QAction(tr("Revoke this member"), this); - connect(action, &QAction::triggered, this, &CreateCircleDialog::revokeCircleMembership); - contextMnu.addAction(action); - } - else - { - QAction *action = new QAction(tr("Grant membership"), this); - connect(action, &QAction::triggered, this, &CreateCircleDialog::grantCircleMembership); - contextMnu.addAction(action); - - QAction *actionReject = new QAction(tr("Reject request"), this); - connect(actionReject, &QAction::triggered, this, &CreateCircleDialog::revokeCircleMembership); - contextMnu.addAction(actionReject); - } - } - } - } - - contextMnu.exec(QCursor::pos()); -} - void CreateCircleDialog::updateMembership() { + am_I_invited = false; + am_I_pending = false; + RsGxsCircleDetails details; if (!rsGxsCircles->getCircleDetails(RsGxsCircleId(mCircleGroup.mMeta.mGroupId), details)) { return; @@ -1080,7 +1024,7 @@ void CreateCircleDialog::updateMembership() } } - // Use the flags defined in rsgxscircles.h + // Map flags using correct bitmasks bool invited = (flags & GXS_EXTERNAL_CIRCLE_FLAGS_IN_ADMIN_LIST); bool subscrb = (flags & GXS_EXTERNAL_CIRCLE_FLAGS_SUBSCRIBED); @@ -1100,11 +1044,15 @@ void CreateCircleDialog::updateMembership() } continue; } - // Check if this ID is one of your own - RsIdentityDetails idd; - bool is_own_id = rsIdentity->getIdDetails(memberId, idd); + bool is_own_id = rsIdentity->isOwnId(memberId); + + // Update global flags for the Context Menu + if (is_own_id) { + if (invited && !subscrb) am_I_invited = true; + if (!invited && subscrb) am_I_pending = true; + } RsGxsId ownId; if (ui.idChooser->getChosenId(ownId) == GxsIdChooser::KnowId) { @@ -1119,12 +1067,12 @@ void CreateCircleDialog::updateMemberStatus(QTreeWidgetItem* subitem, bool invit { QString statusText; QString tooltip = tr("Status: "); - bool am_I_pending = false ; if (invited && !subscrb) { // Case: Admin invited them, but they haven't accepted yet statusText = tr("Invited"); tooltip += tr("Invited by admin"); + if (is_own_id) am_I_invited = true; subitem->setIcon(RSCIRCLEID_COL_STATUS, FilesDefs::getIconFromQtResourcePath(IMAGE_INVITED)) ; } else if (!invited && subscrb) { // Case: User requested to join, but Admin hasn't approved yet @@ -1156,6 +1104,60 @@ void CreateCircleDialog::updateMemberStatus(QTreeWidgetItem* subitem, bool invit ui.members_groupBox->setTitle( tr("Invited Members") + " (" + QString::number(ui.treeWidget_membership->topLevelItemCount()) + ")" ); } +void CreateCircleDialog::MembershipListCustomPopupMenu( QPoint ) +{ + QMenu contextMnu( this ); + + QTreeWidgetItem *item = ui.treeWidget_membership->currentItem(); + + RsGxsId current_gxs_id(item->text(RSCIRCLEID_COL_KEYID).toStdString()); + + if (item && !mReadOnly && item->text(RSCIRCLEID_COL_STATUS) == tr("Invited")) + contextMnu.addAction(QIcon(":/images/delete.png"), tr("Remove Member"), this, SLOT(removeMember())); + + if (am_I_invited) { + contextMnu.addAction(tr("Accept Invite"), this, SLOT(acceptInvite())); + contextMnu.addSeparator(); + } + + if (am_I_pending) { + contextMnu.addAction(tr("Reject request"), this, SLOT(rejectInvite())); + contextMnu.addSeparator(); + } + + if (am_I_circle_admin) + { + RsGxsCircleDetails details; + if (rsGxsCircles->getCircleDetails(RsGxsCircleId(mCircleGroup.mMeta.mGroupId), details)) + { + auto it = details.mSubscriptionFlags.find(current_gxs_id); + if (it != details.mSubscriptionFlags.end()) + { + contextMnu.addSeparator(); + + if (it->second & GXS_EXTERNAL_CIRCLE_FLAGS_IN_ADMIN_LIST) + { + QAction *action = new QAction(tr("Revoke this member"), this); + connect(action, &QAction::triggered, this, &CreateCircleDialog::revokeCircleMembership); + contextMnu.addAction(action); + } + else + { + QAction *action = new QAction(tr("Grant membership"), this); + connect(action, &QAction::triggered, this, &CreateCircleDialog::grantCircleMembership); + contextMnu.addAction(action); + + //QAction *actionReject = new QAction(tr("Reject request"), this); + //connect(actionReject, &QAction::triggered, this, &CreateCircleDialog::revokeCircleMembership); + //contextMnu.addAction(actionReject); + } + } + } + } + + contextMnu.exec(QCursor::pos()); +} + void CreateCircleDialog::acceptInvite() { QTreeWidgetItem *item = ui.treeWidget_membership->currentItem(); From ce75fc7eff30f54dcb5564a6f6703db0d25a06c8 Mon Sep 17 00:00:00 2001 From: jolavillette Date: Sat, 14 Mar 2026 05:52:37 +0100 Subject: [PATCH 10/11] gui/circles: fix null-deref crash, wrong circle ID in accept/reject, deferred status update, and thread safety in CreateCircleDialog --- .../src/gui/Circles/CreateCircleDialog.cpp | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp index 3d5c12616..009ff2d78 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -62,6 +63,9 @@ CreateCircleDialog::CreateCircleDialog() mCircleLoading = false; mCloseRequested = false; mEventHandlerId = 0; + am_I_invited = false; + am_I_pending = false; + am_I_circle_admin = false; rsEvents->registerEventsHandler( [this](std::shared_ptr event) { handleEvent(event); }, @@ -373,7 +377,7 @@ void CreateCircleDialog::addMember(const QString& keyId, const QString& idtype, tree->addTopLevelItem(member); - updateMembership(); + ui.members_groupBox->setTitle( tr("Invited Members") + " (" + QString::number(ui.treeWidget_membership->topLevelItemCount()) + ")" ); } void CreateCircleDialog::addMember(const QString &keyId, const QString &idtype, @@ -957,6 +961,10 @@ void CreateCircleDialog::fillIdentitiesList(const std::vector& id_ } } + + // Update membership statuses once, after all members have been added to the tree + if (mIsExistingCircle) + updateMembership(); } void CreateCircleDialog::idTypeChanged() @@ -1003,6 +1011,7 @@ void CreateCircleDialog::updateMembership() { am_I_invited = false; am_I_pending = false; + am_I_circle_admin = false; RsGxsCircleDetails details; if (!rsGxsCircles->getCircleDetails(RsGxsCircleId(mCircleGroup.mMeta.mGroupId), details)) { @@ -1109,10 +1118,11 @@ void CreateCircleDialog::MembershipListCustomPopupMenu( QPoint ) QMenu contextMnu( this ); QTreeWidgetItem *item = ui.treeWidget_membership->currentItem(); + if (!item) return; RsGxsId current_gxs_id(item->text(RSCIRCLEID_COL_KEYID).toStdString()); - if (item && !mReadOnly && item->text(RSCIRCLEID_COL_STATUS) == tr("Invited")) + if (!mReadOnly && item->text(RSCIRCLEID_COL_STATUS) == tr("Invited")) contextMnu.addAction(QIcon(":/images/delete.png"), tr("Remove Member"), this, SLOT(removeMember())); if (am_I_invited) { @@ -1163,7 +1173,7 @@ void CreateCircleDialog::acceptInvite() QTreeWidgetItem *item = ui.treeWidget_membership->currentItem(); if (!item) return; - RsGxsCircleId circleId(item->text(RSCIRCLEID_COL_KEYID).toStdString()); + RsGxsCircleId circleId(mCircleGroup.mMeta.mGroupId); // Use the chosen identity from your idChooser to accept the invite RsGxsId ownId; if (ui.idChooser->getChosenId(ownId) == GxsIdChooser::KnowId) { @@ -1176,7 +1186,7 @@ void CreateCircleDialog::rejectInvite() QTreeWidgetItem *item = ui.treeWidget_membership->currentItem(); if (!item) return; - RsGxsCircleId circleId(item->text(RSCIRCLEID_COL_KEYID).toStdString()); + RsGxsCircleId circleId(mCircleGroup.mMeta.mGroupId); RsGxsId ownId; if (ui.idChooser->getChosenId(ownId) == GxsIdChooser::KnowId) { rsGxsCircles->cancelCircleMembership(ownId, circleId); @@ -1229,19 +1239,12 @@ void CreateCircleDialog::handleEvent_main_thread(std::shared_ptr if(pe->mCircleId.isNull() || pe->mCircleId == RsGxsCircleId(mCircleGroup.mMeta.mGroupId)) { - // Give it a moment as NewsFeed does to make sure the cache is populated - RsThread::async([this]() { - rstime::rs_usleep(500*1000); // 0.5s - RsQThreadUtils::postToObject( [this]() { - if(!mCircleLoading && !mIsExternalCircle && !mIsExistingCircle) - { - // Still building or whatever.. just update it directly if we're loaded - } - else if(mIsExistingCircle) - { - loadCircle(RsGxsGroupId(mCircleGroup.mMeta.mGroupId)); - } - }, this ); + // Give it a moment to make sure the cache is populated + QTimer::singleShot(500, this, [this]() { + if(mIsExistingCircle && !mCircleLoading) + { + loadCircle(RsGxsGroupId(mCircleGroup.mMeta.mGroupId)); + } }); } } From cb7ae0ecede4d455926cd7ad4f4796e39bea48c8 Mon Sep 17 00:00:00 2001 From: jolavillette Date: Mon, 30 Mar 2026 10:53:12 +0200 Subject: [PATCH 11/11] fix(circles): rewrite context menu to compute state at click time instead of relying on stale global flags --- .../src/gui/Circles/CreateCircleDialog.cpp | 131 ++++++++++++------ 1 file changed, 86 insertions(+), 45 deletions(-) diff --git a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp index 009ff2d78..0bd707fa7 100644 --- a/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp +++ b/retroshare-gui/src/gui/Circles/CreateCircleDialog.cpp @@ -1017,6 +1017,12 @@ void CreateCircleDialog::updateMembership() if (!rsGxsCircles->getCircleDetails(RsGxsCircleId(mCircleGroup.mMeta.mGroupId), details)) { return; } + + // Determine admin status once, outside the loop — it doesn't depend on the iterated member + RsGxsId ownId; + if (ui.idChooser->getChosenId(ownId) == GxsIdChooser::KnowId) { + am_I_circle_admin = (mCircleGroup.mMeta.mAuthorId == ownId); + } // Iterate through the subscription flags map found in details for(auto const& [memberId, flags] : details.mSubscriptionFlags) @@ -1057,16 +1063,11 @@ void CreateCircleDialog::updateMembership() // Check if this ID is one of your own bool is_own_id = rsIdentity->isOwnId(memberId); - // Update global flags for the Context Menu + // Update global flags for UI display purposes if (is_own_id) { if (invited && !subscrb) am_I_invited = true; if (!invited && subscrb) am_I_pending = true; } - - RsGxsId ownId; - if (ui.idChooser->getChosenId(ownId) == GxsIdChooser::KnowId) { - am_I_circle_admin = (mCircleGroup.mMeta.mAuthorId == ownId); - } updateMemberStatus(item, invited, subscrb, is_own_id); } @@ -1120,52 +1121,92 @@ void CreateCircleDialog::MembershipListCustomPopupMenu( QPoint ) QTreeWidgetItem *item = ui.treeWidget_membership->currentItem(); if (!item) return; - RsGxsId current_gxs_id(item->text(RSCIRCLEID_COL_KEYID).toStdString()); + RsGxsId clicked_gxs_id(item->text(RSCIRCLEID_COL_KEYID).toStdString()); + if (clicked_gxs_id.isNull()) return; - if (!mReadOnly && item->text(RSCIRCLEID_COL_STATUS) == tr("Invited")) - contextMnu.addAction(QIcon(":/images/delete.png"), tr("Remove Member"), this, SLOT(removeMember())); + // --- Compute everything locally at click time --- - if (am_I_invited) { - contextMnu.addAction(tr("Accept Invite"), this, SLOT(acceptInvite())); - contextMnu.addSeparator(); + // 1. Is the clicked item one of my own identities? + bool is_own_id = rsIdentity->isOwnId(clicked_gxs_id); + + // 2. Am I the circle admin? + bool is_admin = false; + RsGxsId chosenId; + if (ui.idChooser->getChosenId(chosenId) == GxsIdChooser::KnowId) { + is_admin = (mCircleGroup.mMeta.mAuthorId == chosenId); } - - if (am_I_pending) { - contextMnu.addAction(tr("Reject request"), this, SLOT(rejectInvite())); - contextMnu.addSeparator(); - } - - if (am_I_circle_admin) + + // 3. Get the subscription flags for the clicked item from the circle details + bool item_in_admin_list = false; // invited by admin + bool item_subscribed = false; // user has subscribed + bool item_found_in_circle = false; + + RsGxsCircleDetails details; + if (rsGxsCircles->getCircleDetails(RsGxsCircleId(mCircleGroup.mMeta.mGroupId), details)) { - RsGxsCircleDetails details; - if (rsGxsCircles->getCircleDetails(RsGxsCircleId(mCircleGroup.mMeta.mGroupId), details)) + auto it = details.mSubscriptionFlags.find(clicked_gxs_id); + if (it != details.mSubscriptionFlags.end()) { - auto it = details.mSubscriptionFlags.find(current_gxs_id); - if (it != details.mSubscriptionFlags.end()) - { - contextMnu.addSeparator(); - - if (it->second & GXS_EXTERNAL_CIRCLE_FLAGS_IN_ADMIN_LIST) - { - QAction *action = new QAction(tr("Revoke this member"), this); - connect(action, &QAction::triggered, this, &CreateCircleDialog::revokeCircleMembership); - contextMnu.addAction(action); - } - else - { - QAction *action = new QAction(tr("Grant membership"), this); - connect(action, &QAction::triggered, this, &CreateCircleDialog::grantCircleMembership); - contextMnu.addAction(action); - - //QAction *actionReject = new QAction(tr("Reject request"), this); - //connect(actionReject, &QAction::triggered, this, &CreateCircleDialog::revokeCircleMembership); - //contextMnu.addAction(actionReject); - } - } + item_found_in_circle = true; + item_in_admin_list = (it->second & GXS_EXTERNAL_CIRCLE_FLAGS_IN_ADMIN_LIST); + item_subscribed = (it->second & GXS_EXTERNAL_CIRCLE_FLAGS_SUBSCRIBED); } } - - contextMnu.exec(QCursor::pos()); + + // --- Build menu: Section 1 — Actions for my own ID --- + + if (is_own_id && item_found_in_circle) + { + if (item_in_admin_list && !item_subscribed) { + // I was invited by admin but haven't accepted yet + contextMnu.addAction(tr("Accept Invite"), this, SLOT(acceptInvite())); + } + if (!item_in_admin_list && item_subscribed) { + // I requested to join but admin hasn't approved yet + contextMnu.addAction(tr("Reject request"), this, SLOT(rejectInvite())); + } + } + + // --- Build menu: Section 2 — Admin actions on the clicked item --- + + if (is_admin && item_found_in_circle) + { + if (contextMnu.actions().count() > 0) + contextMnu.addSeparator(); + + if (item_in_admin_list && item_subscribed) { + // Full member — admin can revoke + QAction *action = new QAction(tr("Revoke this member"), &contextMnu); + connect(action, &QAction::triggered, this, &CreateCircleDialog::revokeCircleMembership); + contextMnu.addAction(action); + } + else if (item_in_admin_list && !item_subscribed) { + // Invited but not yet accepted — admin can revoke the invitation + QAction *action = new QAction(tr("Revoke this member"), &contextMnu); + connect(action, &QAction::triggered, this, &CreateCircleDialog::revokeCircleMembership); + contextMnu.addAction(action); + } + else if (!item_in_admin_list && item_subscribed) { + // Pending request — admin can grant membership + QAction *action = new QAction(tr("Grant membership"), &contextMnu); + connect(action, &QAction::triggered, this, &CreateCircleDialog::grantCircleMembership); + contextMnu.addAction(action); + } + } + + // --- Build menu: Section 3 — Edit mode: remove from tree widget --- + // Only for items that were manually added (invited via the editor, not yet in circle) + // Use Qt::UserRole data (bool) instead of comparing translated text + + if (!mReadOnly && !item_found_in_circle) + { + if (contextMnu.actions().count() > 0) + contextMnu.addSeparator(); + contextMnu.addAction(QIcon(":/images/delete.png"), tr("Remove Member"), this, SLOT(removeMember())); + } + + if (contextMnu.actions().count() > 0) + contextMnu.exec(QCursor::pos()); } void CreateCircleDialog::acceptInvite()