From 07b538cadcd136fe1c203a501b7bb8714eb4353c Mon Sep 17 00:00:00 2001
From: Jose Olarte III
Date: Tue, 14 Oct 2025 21:21:35 +0800
Subject: [PATCH] feat: implement member visibility dialog with checkbox
selection and refresh
- Add "Set Visibility" dialog for meeting members who need visibility settings
- Filter members to show only those not in contacts or without seesMe set
- Implement checkbox selection with "Select All" functionality
- Add reactive checkbox behavior with proper state management
- Default all checkboxes to checked when dialog opens
- Implement "Set Visibility" action to add contacts and set seesMe property
- Add success notifications with count of affected members
- Disable "Set Visibility" button when no members are selected
- Use notification callbacks for data refresh
- Hide "Set Visibility" buttons when no members need visibility settings
- Add proper dialog state management and cleanup
- Ensure dialog closes before triggering data refresh to prevent stale states
The implementation provides a smooth user experience for managing member visibility settings with proper state synchronization between components.
---
src/App.vue | 312 ++++++++++++++++++++++++++++-----
src/components/MembersList.vue | 41 ++++-
src/constants/app.ts | 9 +-
src/views/NotFoundView.vue | 4 +-
4 files changed, 312 insertions(+), 54 deletions(-)
diff --git a/src/App.vue b/src/App.vue
index c4638fe0..c4e943a0 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -371,49 +371,65 @@
-
+
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+ No members need visibility settings
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -477,6 +488,9 @@ import { Vue, Component } from "vue-facing-decorator";
import { NotificationIface } from "./constants/app";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { logger } from "./utils/logger";
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import { SOMEONE_UNNAMED } from "@/constants/entities";
+import { setVisibilityUtil } from "./libs/endorserServer";
interface Settings {
notifyingNewActivityTime?: string;
@@ -491,6 +505,208 @@ export default class App extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
stopAsking = false;
+ selectedMembers: string[] = [];
+ selectionInitialized = false;
+
+ get hasSelectedMembers() {
+ return this.selectedMembers.length > 0;
+ }
+ activeDid = "";
+ apiServer = "";
+
+ async created() {
+ // Initialize settings
+ const settings = await this.$accountSettings();
+ this.apiServer = settings.apiServer || "";
+
+ // Get activeDid from active_identity table
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+ }
+
+ isAllSelected(notification: NotificationIface) {
+ const membersData = notification?.membersData || [];
+ if (!membersData || membersData.length === 0) return false;
+ return membersData.every((member) =>
+ this.selectedMembers.includes(member.did),
+ );
+ }
+
+ isIndeterminate(notification: NotificationIface) {
+ const membersData = notification?.membersData || [];
+ if (!membersData || membersData.length === 0) return false;
+ const selectedCount = membersData.filter((member) =>
+ this.selectedMembers.includes(member.did),
+ ).length;
+ return selectedCount > 0 && selectedCount < membersData.length;
+ }
+
+ toggleSelectAll(notification: NotificationIface) {
+ const membersData = notification?.membersData || [];
+ if (!membersData || membersData.length === 0) return;
+
+ if (this.isAllSelected(notification)) {
+ // Deselect all
+ this.selectedMembers = [];
+ } else {
+ // Select all
+ this.selectedMembers = membersData.map((member) => member.did);
+ }
+ }
+
+ toggleMemberSelection(memberDid: string) {
+ const index = this.selectedMembers.indexOf(memberDid);
+ if (index > -1) {
+ this.selectedMembers.splice(index, 1);
+ } else {
+ this.selectedMembers.push(memberDid);
+ }
+ }
+
+ isMemberSelected(memberDid: string) {
+ return this.selectedMembers.includes(memberDid);
+ }
+
+ shouldInitializeSelection(notification: NotificationIface) {
+ // This method will initialize selection when the dialog opens
+ if (
+ notification?.type === "set-visibility-meeting-members" &&
+ !this.selectionInitialized
+ ) {
+ this.initializeSelection(notification);
+ this.selectionInitialized = true;
+ }
+ return true;
+ }
+
+ initializeSelection(notification: NotificationIface) {
+ // Reset selection when dialog opens
+ this.selectedMembers = [];
+ // Select all by default
+ const membersData = notification?.membersData || [];
+ this.selectedMembers = membersData.map((member) => member.did);
+ }
+
+ resetSelection() {
+ this.selectedMembers = [];
+ this.selectionInitialized = false;
+ }
+
+ async setVisibilityForSelectedMembers(notification: NotificationIface) {
+ try {
+ const membersData = notification?.membersData || [];
+ const selectedMembers = membersData.filter((member) =>
+ this.selectedMembers.includes(member.did),
+ );
+
+ let successCount = 0;
+
+ for (const member of selectedMembers) {
+ try {
+ // If they're not a contact yet, add them as a contact first
+ if (!member.isContact) {
+ await this.addAsContact(member);
+ }
+
+ // Set their seesMe to true
+ await this.updateContactVisibility(member.did, true);
+
+ successCount++;
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(`Error processing member ${member.did}:`, error);
+ // Continue with other members even if one fails
+ }
+ }
+
+ // Show success notification
+ this.$notify(
+ {
+ group: "alert",
+ type: "success",
+ title: "Visibility Set Successfully",
+ text: `Visibility set for ${successCount} member${successCount === 1 ? "" : "s"}.`,
+ },
+ 5000,
+ );
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error("Error setting visibility:", error);
+ this.$notify(
+ {
+ group: "alert",
+ type: "danger",
+ title: "Error",
+ text: "Failed to set visibility for some members. Please try again.",
+ },
+ 5000,
+ );
+ }
+ }
+
+ async addAsContact(member: { did: string; name: string }) {
+ try {
+ const newContact = {
+ did: member.did,
+ name: member.name,
+ };
+
+ await this.$insertContact(newContact);
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error("Error adding contact:", err);
+ if (err instanceof Error && err.message?.indexOf("already exists") > -1) {
+ // Contact already exists, continue
+ } else {
+ throw err; // Re-throw if it's not a duplicate error
+ }
+ }
+ }
+
+ async updateContactVisibility(did: string, seesMe: boolean) {
+ try {
+ // Get the contact object
+ const contact = await this.$getContact(did);
+ if (!contact) {
+ throw new Error(`Contact not found for DID: ${did}`);
+ }
+
+ // Use the proper API to set visibility on the server
+ const result = await setVisibilityUtil(
+ this.activeDid,
+ this.apiServer,
+ this.axios,
+ contact,
+ seesMe,
+ );
+
+ if (!result.success) {
+ throw new Error(result.error || "Failed to set visibility");
+ }
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error("Error updating contact visibility:", err);
+ throw err;
+ }
+ }
+
+ async closeDialog(
+ notification: NotificationIface,
+ closeFn: (id: string) => void,
+ ) {
+ this.resetSelection();
+
+ // Close the notification first
+ closeFn(notification.id);
+
+ // Then call the callback after a short delay to ensure dialog is closed
+ setTimeout(async () => {
+ if (notification.callback) {
+ await notification.callback();
+ }
+ }, 100);
+ }
showContactInfo() {
this.$notify(
diff --git a/src/components/MembersList.vue b/src/components/MembersList.vue
index 65da0c0c..ef3106fe 100644
--- a/src/components/MembersList.vue
+++ b/src/components/MembersList.vue
@@ -67,6 +67,7 @@