<template> <div class="space-y-4"> <!-- Loading State --> <div v-if="isLoading" class="flex justify-center items-center py-8"> <fa icon="spinner" class="fa-spin-pulse" /> </div> <!-- Members List --> <p v-if="decryptedMembers.length < members.length" class="text-center text-red-600 py-4" > {{ decryptFailureMessage || "Your password failed. Please go back and try again." }} </p> <div v-else class="space-y-4"> <div v-if="missingMyself" class="py-4 text-red-600"> You are not admitted. The organizer will admit you. </div> <div> <span v-if="showOrganizerTools && isOrganizer" class="inline-flex items-center flex-wrap" > <span class="inline-flex items-center"> Use <span class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600" > <fa icon="plus" class="text-sm" /> </span> and <span class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600" > <fa icon="minus" class="text-sm" /> </span> to add/remove them to/from the meeting. </span> </span> </div> <div> <span class="inline-flex items-center"> Use <span class="mx-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600" > <fa icon="circle-user" class="text-xl" /> </span> to add them to your contacts. </span> </div> <div v-if="members.length > 0" class="flex justify-center"> <button @click="fetchMembers" class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors" title="Refresh members list" > <fa icon="rotate" :class="{ 'fa-spin': isLoading }" /> </button> </div> <div v-for="member in membersToShow()" :key="member.member.memberId" class="p-4 bg-gray-50 rounded-lg" > <div class="flex items-center justify-between"> <div class="flex items-center"> <h3 class="text-lg font-medium">{{ member.name }}</h3> <div v-if="!getContactFor(member.did) && member.did !== activeDid" class="flex justify-end" > <button @click="addAsContact(member)" class="ml-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600 hover:bg-green-200 hover:text-green-800 transition-colors" title="Add as contact" > <fa icon="circle-user" class="text-xl" /> </button> </div> <button v-if="member.did !== activeDid" @click=" informAboutAddingContact( getContactFor(member.did) !== undefined, ) " class="ml-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-colors" title="Contact info" > <fa icon="circle-info" class="text-base" /> </button> </div> <div class="flex"> <span v-if=" showOrganizerTools && isOrganizer && member.did !== activeDid " class="flex items-center" > <button @click="checkWhetherContactBeforeAdmitting(member)" class="mr-2 w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors" :title=" member.member.admitted ? 'Remove member' : 'Admit member' " > <fa :icon="member.member.admitted ? 'minus' : 'plus'" class="text-sm" /> </button> <button @click="informAboutAdmission()" class="mr-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-colors" title="Admission info" > <fa icon="circle-info" class="text-base" /> </button> </span> </div> </div> <p class="text-sm text-gray-600 truncate"> {{ member.did }} </p> </div> <div v-if="members.length > 0" class="flex justify-center mt-4"> <button @click="fetchMembers" class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors" title="Refresh members list" > <fa icon="rotate" :class="{ 'fa-spin': isLoading }" /> </button> </div> <p v-if="members.length === 0" class="text-gray-500 py-4"> No members have joined this meeting yet </p> </div> </div> </template> <script lang="ts"> import { Component, Vue, Prop } from "vue-facing-decorator"; import { logConsoleAndDb, retrieveSettingsForActiveAccount, db, } from "@/db/index"; import { errorStringForLog, getHeaders, register, serverMessageForUser, } from "@/libs/endorserServer"; import { decryptMessage } from "@/libs/crypto"; import { Contact } from "@/db/tables/contacts"; import * as libsUtil from "@/libs/util"; import { NotificationIface } from "@/constants/app"; interface Member { admitted: boolean; content: string; memberId: number; registered: boolean; } interface DecryptedMember { member: Member; name: string; did: string; } @Component export default class MembersList extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; libsUtil = libsUtil; @Prop({ required: true }) password!: string; @Prop({ default: "Your password failed. Please go back and try again." }) decryptFailureMessage!: string; @Prop({ default: false }) showOrganizerTools!: boolean; decryptedMembers: DecryptedMember[] = []; missingPassword = false; missingMyself = false; isLoading = false; isOrganizer = false; members: Member[] = []; activeDid = ""; apiServer = ""; contacts: Array<Contact> = []; async created() { const settings = await retrieveSettingsForActiveAccount(); this.activeDid = settings.activeDid || ""; this.apiServer = settings.apiServer || ""; await this.fetchMembers(); await this.loadContacts(); } async fetchMembers() { this.isLoading = true; try { const headers = await getHeaders(this.activeDid); const response = await this.axios.get( `${this.apiServer}/api/partner/groupOnboardMembers`, { headers }, ); if (response.data && response.data.data) { this.members = response.data.data; await this.decryptMemberContents(); } } catch (error) { logConsoleAndDb( "Error fetching members: " + errorStringForLog(error), true, ); this.$emit( "error", serverMessageForUser(error) || "Failed to fetch members.", ); } finally { this.isLoading = false; } } async decryptMemberContents() { this.decryptedMembers = []; if (!this.password) { this.missingPassword = true; return; } let isFirstEntry = true, foundMyself = false; for (const member of this.members) { try { const decryptedContent = await decryptMessage( member.content, this.password, ); const content = JSON.parse(decryptedContent); this.decryptedMembers.push({ member: member, name: content.name, did: content.did, }); if (isFirstEntry && content.did === this.activeDid) { this.isOrganizer = true; } if (content.did === this.activeDid) { foundMyself = true; } } catch (error) { // do nothing, relying on the count of members to determine if there was an error } isFirstEntry = false; } this.missingMyself = !foundMyself; } membersToShow(): DecryptedMember[] { if (this.isOrganizer) { if (this.showOrganizerTools) { return this.decryptedMembers; } else { return this.decryptedMembers.filter( (member: DecryptedMember) => member.member.admitted, ); } } // non-organizers only get visible members from server return this.decryptedMembers; } informAboutAdmission() { this.$notify( { group: "alert", type: "info", title: "Admission info", text: "This is to register people and admit them to the meeting. A '+' symbol means they are not yet admitted and you can register and admit them. A '-' means you can remove them, but they will stay registered.", }, 10000, ); } informAboutAddingContact(contactImportedAlready: boolean) { if (contactImportedAlready) { this.$notify( { group: "alert", type: "info", title: "Contact Exists", text: "They are in your contacts. If you want to remove them, you must do that from the contacts screen.", }, 10000, ); } else { this.$notify( { group: "alert", type: "info", title: "Contact Available", text: "This is to add them to your contacts. If you want to remove them later, you must do that from the contacts screen.", }, 10000, ); } } async loadContacts() { this.contacts = await db.contacts.toArray(); } getContactFor(did: string): Contact | undefined { return this.contacts.find((contact) => contact.did === did); } checkWhetherContactBeforeAdmitting(member: DecryptedMember) { const contact = this.getContactFor(member.did); if (!member.member.admitted && !contact) { // If not a contact, show confirmation dialog this.$notify({ group: "modal", type: "confirm", title: "Add as Contact First?", text: "This person is not in your contacts. Would you like to add them as a contact first?", yesText: "Add as Contact", noText: "Skip Adding Contact", onYes: async () => { await this.addAsContact(member); // After adding as contact, proceed with admission await this.toggleAdmission(member); }, onNo: async () => { // If they choose not to add as contact, show second confirmation this.$notify({ group: "modal", type: "confirm", title: "Continue Without Adding?", text: "Are you sure you want to proceed with admission even though they are not a contact?", yesText: "Continue", onYes: async () => { await this.toggleAdmission(member); }, onCancel: async () => { // Do nothing, effectively canceling the operation }, }, -1, ); }, }, -1, ); } else { // If already a contact, proceed directly with admission this.toggleAdmission(member); } } async toggleAdmission(member: DecryptedMember) { try { const headers = await getHeaders(this.activeDid); await this.axios.put( `${this.apiServer}/api/partner/groupOnboardMember/${member.member.memberId}`, { admitted: !member.member.admitted }, { headers }, ); // Update local state member.member.admitted = !member.member.admitted; const oldContact = this.getContactFor(member.did); // if admitted, now register that user if they are not registered if (member.member.admitted && !oldContact?.registered) { const contactOldOrNew: Contact = oldContact || { did: member.did, name: member.name, } const result = await register( this.activeDid, this.apiServer, this.axios, contactOldOrNew, ); if (result.success) { member.member.registered = true; if (oldContact) { await db.contacts.update(member.did, { registered: true }); oldContact.registered = true; } this.$notify( { group: "alert", type: "success", title: "Registered", text: "Besides being admitted, they were also registered.", }, 3000, ); } else { const additionalInfo = result.error || ""; this.$notify( { group: "alert", type: "danger", title: "Registration failed", text: "They were admitted, but registration failed. You can try again, or register from your contacts screen. " + additionalInfo, }, 10000, ); } } } catch (error) { logConsoleAndDb( "Error toggling admission: " + errorStringForLog(error), true, ); this.$emit( "error", serverMessageForUser(error) || "Failed to update member admission status.", ); } } async addAsContact(member: DecryptedMember) { try { const newContact = { did: member.did, name: member.name, }; await db.contacts.add(newContact); this.contacts.push(newContact); this.$notify( { group: "alert", type: "success", title: "Contact Added", text: "They were added to your contacts.", }, 3000, ); } catch (err) { logConsoleAndDb("Error adding contact: " + errorStringForLog(err), true); let message = "An error prevented adding this contact."; if (err instanceof Error && err.message?.indexOf("already exists") > -1) { message = "This person is already in your contact list."; } this.$notify( { group: "alert", type: "danger", title: "Contact Not Added", text: message, }, 5000, ); } } } </script>