Browse Source

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.
pull/208/head
Jose Olarte III 4 days ago
parent
commit
07b538cadc
  1. 312
      src/App.vue
  2. 41
      src/components/MembersList.vue
  3. 9
      src/constants/app.ts
  4. 4
      src/views/NotFoundView.vue

312
src/App.vue

@ -371,49 +371,65 @@
</p>
<!-- Custom table area - you can customize this -->
<div class="mb-4">
<div
v-if="shouldInitializeSelection(notification)"
class="mb-4"
>
<table
class="w-full border-collapse border border-slate-300 text-sm text-start"
>
<thead>
<thead
v-if="
notification.membersData &&
notification.membersData.length > 0
"
>
<tr class="bg-slate-100 font-medium">
<th class="border border-slate-300 px-3 py-2">
<label class="flex items-center gap-2">
<input type="checkbox" checked />
<input
type="checkbox"
:checked="isAllSelected(notification)"
:indeterminate="isIndeterminate(notification)"
@change="toggleSelectAll(notification)"
/>
Select All
</label>
</th>
</tr>
</thead>
<tbody>
<!-- Sample data - replace with your actual data -->
<tr>
<td class="border border-slate-300 px-3 py-2">
<div class="flex items-center justify-between gap-2">
<label class="flex items-center gap-2">
<input type="checkbox" checked />
John Doe
</label>
<!-- Friend indicator -->
<font-awesome
icon="user-circle"
class="fa-fw ms-auto text-slate-400 cursor-pointer hover:text-slate-600"
@click="showContactInfo"
/>
</div>
<!-- Dynamic data from MembersList -->
<tr
v-if="
!notification.membersData ||
notification.membersData.length === 0
"
>
<td
class="border border-slate-300 px-3 py-2 text-center italic text-gray-500"
>
No members need visibility settings
</td>
</tr>
<tr>
<tr
v-for="member in notification.membersData || []"
:key="member.member.memberId"
>
<td class="border border-slate-300 px-3 py-2">
<div class="flex items-center justify-between gap-2">
<label class="flex items-center gap-2">
<input type="checkbox" checked />
Jane Smith
<input
type="checkbox"
:checked="isMemberSelected(member.did)"
@change="toggleMemberSelection(member.did)"
/>
{{ member.name || SOMEONE_UNNAMED }}
</label>
<!-- Friend indicator -->
<!-- Friend indicator - only show if they are already a contact -->
<font-awesome
v-if="member.isContact"
icon="user-circle"
class="fa-fw ms-auto text-slate-400 cursor-pointer hover:text-slate-600"
@click="showContactInfo"
@ -421,45 +437,40 @@
</div>
</td>
</tr>
<tr>
<td class="border border-slate-300 px-3 py-2">
<div class="flex items-center justify-between gap-2">
<label class="flex items-center gap-2">
<input type="checkbox" checked />
Jim Beam
</label>
</div>
</td>
</tr>
<tr>
<td class="border border-slate-300 px-3 py-2">
<div class="flex items-center justify-between gap-2">
<label class="flex items-center gap-2">
<input type="checkbox" checked />
Susie Q
</label>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="space-y-2">
<button
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md"
v-if="
notification.membersData &&
notification.membersData.length > 0
"
:disabled="!hasSelectedMembers"
:class="[
'block w-full text-center text-md font-bold uppercase px-2 py-2 rounded-md',
hasSelectedMembers
? 'bg-blue-600 text-white cursor-pointer'
: 'bg-slate-400 text-slate-200 cursor-not-allowed',
]"
@click="
notification.onYes ? notification.onYes() : null;
close(notification.id);
setVisibilityForSelectedMembers(notification);
closeDialog(notification, close);
"
>
Set Visibility
</button>
<button
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@click="close(notification.id)"
@click="closeDialog(notification, close)"
>
Maybe Later
{{
notification.membersData &&
notification.membersData.length > 0
? "Maybe Later"
: "Cancel"
}}
</button>
</div>
</div>
@ -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(

41
src/components/MembersList.vue

@ -67,6 +67,7 @@
</button>
<button
v-if="getMembersForVisibility().length > 0"
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
title="Set visibility for meeting members"
@click="showAddMembersNotification"
@ -162,6 +163,7 @@
</button>
<button
v-if="getMembersForVisibility().length > 0"
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
title="Set visibility for meeting members"
@click="showAddMembersNotification"
@ -264,6 +266,12 @@ export default class MembersList extends Vue {
await this.loadContacts();
}
async refreshData() {
// Force refresh both contacts and members
await this.loadContacts();
await this.fetchMembers();
}
async fetchMembers() {
try {
this.isLoading = true;
@ -391,6 +399,28 @@ export default class MembersList extends Vue {
return this.contacts.find((contact) => contact.did === did);
}
getMembersForVisibility() {
return this.decryptedMembers
.filter((member) => {
// Exclude the current user
if (member.did === this.activeDid) {
return false;
}
const contact = this.getContactFor(member.did);
// Include members who:
// 1. Haven't been added as contacts yet, OR
// 2. Are contacts but don't have visibility set (seesMe property)
return !contact || !contact.seesMe;
})
.map((member) => ({
...member,
isContact: !!this.getContactFor(member.did),
contact: this.getContactFor(member.did),
}));
}
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
const contact = this.getContactFor(decrMember.did);
if (!decrMember.member.admitted && !contact) {
@ -530,14 +560,17 @@ export default class MembersList extends Vue {
}
showAddMembersNotification() {
// Filter members to show only those who need visibility set
const membersForVisibility = this.getMembersForVisibility();
this.$notify(
{
group: "modal",
type: "set-visibility-meeting-members",
title: "Set Visibility for Meeting Members",
onYes: async () => {
// Handle the "Add Selected" action - you can implement the actual logic here
console.log("User confirmed adding selected members as contacts");
membersData: membersForVisibility, // Pass the filtered members data
callback: async () => {
// Refresh data when dialog is closed (regardless of action taken)
await this.refreshData();
},
},
-1,

9
src/constants/app.ts

@ -59,7 +59,7 @@ export const PASSKEYS_ENABLED =
export interface NotificationIface {
group: string; // "alert" | "modal"
type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string;
title?: string;
text?: string;
callback?: (success: boolean) => Promise<void>; // if this triggered an action
noText?: string;
@ -68,4 +68,11 @@ export interface NotificationIface {
onYes?: () => Promise<void>;
promptToStopAsking?: boolean;
yesText?: string;
membersData?: Array<{
member: { admitted: boolean; content: string; memberId: number };
name: string;
did: string;
isContact: boolean;
contact?: { did: string; name?: string; seesMe?: boolean };
}>; // For passing member data to visibility dialog
}

4
src/views/NotFoundView.vue

@ -60,7 +60,9 @@
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2
2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1
1 0 011 1v4a1 1 0 001 1m-6 0h6"
></path>
</svg>
Go Home

Loading…
Cancel
Save