You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							957 lines
						
					
					
						
							28 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							957 lines
						
					
					
						
							28 KiB
						
					
					
				
								<template>
							 | 
						|
								  <div class="space-y-4">
							 | 
						|
								    <!-- Loading State -->
							 | 
						|
								    <div
							 | 
						|
								      v-if="isLoading"
							 | 
						|
								      class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
							 | 
						|
								    >
							 | 
						|
								      <font-awesome icon="spinner" class="fa-spin-pulse" />
							 | 
						|
								    </div>
							 | 
						|
								
							 | 
						|
								    <!-- Members List -->
							 | 
						|
								
							 | 
						|
								    <div v-else>
							 | 
						|
								      <div class="text-center text-red-600 my-4">
							 | 
						|
								        {{ decryptionErrorMessage() }}
							 | 
						|
								      </div>
							 | 
						|
								
							 | 
						|
								      <div v-if="missingMyself" class="py-4 text-red-600">
							 | 
						|
								        You are not currently admitted by the organizer.
							 | 
						|
								      </div>
							 | 
						|
								      <div v-if="!firstName" class="py-4 text-red-600">
							 | 
						|
								        Your name is not set, so others may not recognize you. Reload this page
							 | 
						|
								        to set it.
							 | 
						|
								      </div>
							 | 
						|
								
							 | 
						|
								      <ul class="list-disc text-sm ps-4 space-y-2 mb-4">
							 | 
						|
								        <li
							 | 
						|
								          v-if="membersToShow().length > 0 && showOrganizerTools && isOrganizer"
							 | 
						|
								        >
							 | 
						|
								          Click
							 | 
						|
								          <font-awesome icon="circle-plus" class="text-blue-500 text-sm" />
							 | 
						|
								          /
							 | 
						|
								          <font-awesome icon="circle-minus" class="text-rose-500 text-sm" />
							 | 
						|
								          to add/remove them to/from the meeting.
							 | 
						|
								        </li>
							 | 
						|
								        <li v-if="membersToShow().length > 0">
							 | 
						|
								          Click
							 | 
						|
								          <font-awesome icon="circle-user" class="text-green-600 text-sm" />
							 | 
						|
								          to add them to your contacts.
							 | 
						|
								        </li>
							 | 
						|
								      </ul>
							 | 
						|
								
							 | 
						|
								      <div class="flex justify-between">
							 | 
						|
								        <!-- 
							 | 
						|
								        always have at least one refresh button even without members in case the organizer 
							 | 
						|
								         changes the password 
							 | 
						|
								         -->
							 | 
						|
								        <button
							 | 
						|
								          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="Refresh members list now"
							 | 
						|
								          @click="manualRefresh"
							 | 
						|
								        >
							 | 
						|
								          <font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
							 | 
						|
								          Refresh
							 | 
						|
								          <span class="text-xs">({{ countdownTimer }}s)</span>
							 | 
						|
								        </button>
							 | 
						|
								      </div>
							 | 
						|
								      <ul
							 | 
						|
								        v-if="membersToShow().length > 0"
							 | 
						|
								        class="border-t border-slate-300 my-2"
							 | 
						|
								      >
							 | 
						|
								        <li
							 | 
						|
								          v-for="member in membersToShow()"
							 | 
						|
								          :key="member.member.memberId"
							 | 
						|
								          :class="[
							 | 
						|
								            'border-b px-2 sm:px-3 py-1.5',
							 | 
						|
								            {
							 | 
						|
								              'bg-blue-50 border-t border-blue-300 -mt-[1px]':
							 | 
						|
								                !member.member.admitted && isOrganizer,
							 | 
						|
								            },
							 | 
						|
								            { 'border-slate-300': member.member.admitted },
							 | 
						|
								          ]"
							 | 
						|
								        >
							 | 
						|
								          <div class="flex items-center gap-2 justify-between">
							 | 
						|
								            <div class="flex items-center gap-1 overflow-hidden">
							 | 
						|
								              <h3
							 | 
						|
								                :class="[
							 | 
						|
								                  'font-semibold truncate',
							 | 
						|
								                  { 'text-slate-500': !member.member.admitted && isOrganizer },
							 | 
						|
								                ]"
							 | 
						|
								              >
							 | 
						|
								                <font-awesome
							 | 
						|
								                  v-if="member.member.memberId === members[0]?.memberId"
							 | 
						|
								                  icon="crown"
							 | 
						|
								                  class="fa-fw text-amber-400"
							 | 
						|
								                />
							 | 
						|
								                <font-awesome
							 | 
						|
								                  v-if="!member.member.admitted && isOrganizer"
							 | 
						|
								                  icon="hourglass-half"
							 | 
						|
								                  class="fa-fw text-slate-400"
							 | 
						|
								                />
							 | 
						|
								                {{ member.name || unnamedMember }}
							 | 
						|
								              </h3>
							 | 
						|
								              <div
							 | 
						|
								                v-if="!getContactFor(member.did) && member.did !== activeDid"
							 | 
						|
								                class="flex items-center gap-1.5 ms-1"
							 | 
						|
								              >
							 | 
						|
								                <button
							 | 
						|
								                  class="btn-add-contact"
							 | 
						|
								                  title="Add as contact"
							 | 
						|
								                  @click="addAsContact(member)"
							 | 
						|
								                >
							 | 
						|
								                  <font-awesome icon="circle-user" />
							 | 
						|
								                </button>
							 | 
						|
								
							 | 
						|
								                <button
							 | 
						|
								                  class="btn-info-contact"
							 | 
						|
								                  title="Contact Info"
							 | 
						|
								                  @click="
							 | 
						|
								                    informAboutAddingContact(
							 | 
						|
								                      getContactFor(member.did) !== undefined,
							 | 
						|
								                    )
							 | 
						|
								                  "
							 | 
						|
								                >
							 | 
						|
								                  <font-awesome icon="circle-info" />
							 | 
						|
								                </button>
							 | 
						|
								              </div>
							 | 
						|
								            </div>
							 | 
						|
								            <span
							 | 
						|
								              v-if="
							 | 
						|
								                showOrganizerTools && isOrganizer && member.did !== activeDid
							 | 
						|
								              "
							 | 
						|
								              class="flex items-center gap-1.5"
							 | 
						|
								            >
							 | 
						|
								              <button
							 | 
						|
								                :class="
							 | 
						|
								                  member.member.admitted
							 | 
						|
								                    ? 'btn-admission-remove'
							 | 
						|
								                    : 'btn-admission-add'
							 | 
						|
								                "
							 | 
						|
								                :title="
							 | 
						|
								                  member.member.admitted ? 'Remove member' : 'Admit member'
							 | 
						|
								                "
							 | 
						|
								                @click="checkWhetherContactBeforeAdmitting(member)"
							 | 
						|
								              >
							 | 
						|
								                <font-awesome
							 | 
						|
								                  :icon="
							 | 
						|
								                    member.member.admitted ? 'circle-minus' : 'circle-plus'
							 | 
						|
								                  "
							 | 
						|
								                />
							 | 
						|
								              </button>
							 | 
						|
								
							 | 
						|
								              <button
							 | 
						|
								                class="btn-info-admission"
							 | 
						|
								                title="Admission Info"
							 | 
						|
								                @click="informAboutAdmission()"
							 | 
						|
								              >
							 | 
						|
								                <font-awesome icon="circle-info" />
							 | 
						|
								              </button>
							 | 
						|
								            </span>
							 | 
						|
								          </div>
							 | 
						|
								          <p class="text-xs text-gray-600 truncate">
							 | 
						|
								            {{ member.did }}
							 | 
						|
								          </p>
							 | 
						|
								        </li>
							 | 
						|
								      </ul>
							 | 
						|
								
							 | 
						|
								      <div v-if="membersToShow().length > 0" class="flex justify-between">
							 | 
						|
								        <!-- 
							 | 
						|
								        always have at least one refresh button even without members in case the organizer 
							 | 
						|
								         changes the password 
							 | 
						|
								         -->
							 | 
						|
								        <button
							 | 
						|
								          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="Refresh members list now"
							 | 
						|
								          @click="manualRefresh"
							 | 
						|
								        >
							 | 
						|
								          <font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
							 | 
						|
								          Refresh
							 | 
						|
								          <span class="text-xs">({{ countdownTimer }}s)</span>
							 | 
						|
								        </button>
							 | 
						|
								      </div>
							 | 
						|
								
							 | 
						|
								      <p v-if="members.length === 0" class="text-gray-500 py-4">
							 | 
						|
								        No members have joined this meeting yet
							 | 
						|
								      </p>
							 | 
						|
								    </div>
							 | 
						|
								  </div>
							 | 
						|
								
							 | 
						|
								  <!-- Admit Pending Members Dialog Component -->
							 | 
						|
								  <AdmitPendingMembersDialog
							 | 
						|
								    :visible="showAdmitPendingDialog"
							 | 
						|
								    :pending-members-data="pendingMembersData"
							 | 
						|
								    :active-did="activeDid"
							 | 
						|
								    :api-server="apiServer"
							 | 
						|
								    @close="closeAdmitPendingDialog"
							 | 
						|
								    @success="onAdmitPendingSuccess"
							 | 
						|
								  />
							 | 
						|
								
							 | 
						|
								  <!-- Set Visibility Dialog Component -->
							 | 
						|
								  <SetBulkVisibilityDialog
							 | 
						|
								    :visible="showSetVisibilityDialog"
							 | 
						|
								    :members-data="visibilityDialogMembers"
							 | 
						|
								    :active-did="activeDid"
							 | 
						|
								    :api-server="apiServer"
							 | 
						|
								    @close="closeSetVisibilityDialog"
							 | 
						|
								  />
							 | 
						|
								</template>
							 | 
						|
								
							 | 
						|
								<script lang="ts">
							 | 
						|
								import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
							 | 
						|
								
							 | 
						|
								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";
							 | 
						|
								import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
							 | 
						|
								import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
							 | 
						|
								import {
							 | 
						|
								  NOTIFY_ADD_CONTACT_FIRST,
							 | 
						|
								  NOTIFY_CONTINUE_WITHOUT_ADDING,
							 | 
						|
								} from "@/constants/notifications";
							 | 
						|
								import { SOMEONE_UNNAMED } from "@/constants/entities";
							 | 
						|
								import SetBulkVisibilityDialog from "./SetBulkVisibilityDialog.vue";
							 | 
						|
								import AdmitPendingMembersDialog from "./AdmitPendingMembersDialog.vue";
							 | 
						|
								
							 | 
						|
								interface Member {
							 | 
						|
								  admitted: boolean;
							 | 
						|
								  content: string;
							 | 
						|
								  memberId: number;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								interface DecryptedMember {
							 | 
						|
								  member: Member;
							 | 
						|
								  name: string;
							 | 
						|
								  did: string;
							 | 
						|
								  isRegistered: boolean;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								@Component({
							 | 
						|
								  components: {
							 | 
						|
								    SetBulkVisibilityDialog,
							 | 
						|
								    AdmitPendingMembersDialog,
							 | 
						|
								  },
							 | 
						|
								  mixins: [PlatformServiceMixin],
							 | 
						|
								})
							 | 
						|
								export default class MembersList extends Vue {
							 | 
						|
								  $notify!: (notification: NotificationIface, timeout?: number) => void;
							 | 
						|
								
							 | 
						|
								  notify!: ReturnType<typeof createNotifyHelpers>;
							 | 
						|
								  libsUtil = libsUtil;
							 | 
						|
								
							 | 
						|
								  @Prop({ required: true }) password!: string;
							 | 
						|
								  @Prop({ default: false }) showOrganizerTools!: boolean;
							 | 
						|
								
							 | 
						|
								  // Emit methods using @Emit decorator
							 | 
						|
								  @Emit("error")
							 | 
						|
								  emitError(message: string) {
							 | 
						|
								    return message;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  decryptedMembers: DecryptedMember[] = [];
							 | 
						|
								  firstName = "";
							 | 
						|
								  isLoading = true;
							 | 
						|
								  isOrganizer = false;
							 | 
						|
								  members: Member[] = [];
							 | 
						|
								  missingPassword = false;
							 | 
						|
								  missingMyself = false;
							 | 
						|
								  activeDid = "";
							 | 
						|
								  apiServer = "";
							 | 
						|
								
							 | 
						|
								  // Admit Pending Members Dialog state
							 | 
						|
								  showAdmitPendingDialog = false;
							 | 
						|
								  pendingMembersData: Array<{
							 | 
						|
								    did: string;
							 | 
						|
								    name: string;
							 | 
						|
								    isContact: boolean;
							 | 
						|
								    member: { memberId: string };
							 | 
						|
								  }> = [];
							 | 
						|
								  admitDialogDismissed = false;
							 | 
						|
								  isManualRefresh = false;
							 | 
						|
								
							 | 
						|
								  // Set Visibility Dialog state
							 | 
						|
								  showSetVisibilityDialog = false;
							 | 
						|
								  visibilityDialogMembers: Array<{
							 | 
						|
								    did: string;
							 | 
						|
								    name: string;
							 | 
						|
								    isContact: boolean;
							 | 
						|
								    member: { memberId: string };
							 | 
						|
								  }> = [];
							 | 
						|
								  contacts: Array<Contact> = [];
							 | 
						|
								
							 | 
						|
								  // Auto-refresh functionality
							 | 
						|
								  countdownTimer = 10;
							 | 
						|
								  autoRefreshInterval: NodeJS.Timeout | null = null;
							 | 
						|
								
							 | 
						|
								  // Track previous visibility members to detect changes
							 | 
						|
								  previousVisibilityMembers: string[] = [];
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Get the unnamed member constant
							 | 
						|
								   */
							 | 
						|
								  get unnamedMember(): string {
							 | 
						|
								    return SOMEONE_UNNAMED;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async created() {
							 | 
						|
								    this.notify = createNotifyHelpers(this.$notify);
							 | 
						|
								
							 | 
						|
								    const settings = await this.$accountSettings();
							 | 
						|
								
							 | 
						|
								    // Get activeDid from active_identity table (single source of truth)
							 | 
						|
								    // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						|
								    const activeIdentity = await (this as any).$getActiveIdentity();
							 | 
						|
								    this.activeDid = activeIdentity.activeDid || "";
							 | 
						|
								
							 | 
						|
								    this.apiServer = settings.apiServer || "";
							 | 
						|
								    this.firstName = settings.firstName || "";
							 | 
						|
								    await this.fetchMembers();
							 | 
						|
								    await this.loadContacts();
							 | 
						|
								
							 | 
						|
								    // Start auto-refresh
							 | 
						|
								    this.startAutoRefresh();
							 | 
						|
								
							 | 
						|
								    // Check if we should show the admit pending members dialog first
							 | 
						|
								    this.checkAndShowAdmitPendingDialog();
							 | 
						|
								
							 | 
						|
								    // If no pending members, check for visibility dialog
							 | 
						|
								    if (!this.showAdmitPendingDialog) {
							 | 
						|
								      this.checkAndShowVisibilityDialog();
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async refreshData() {
							 | 
						|
								    // Force refresh both contacts and members
							 | 
						|
								    await this.loadContacts();
							 | 
						|
								    await this.fetchMembers();
							 | 
						|
								
							 | 
						|
								    // Check if we should show the admit pending members dialog first
							 | 
						|
								    this.checkAndShowAdmitPendingDialog();
							 | 
						|
								
							 | 
						|
								    // If no pending members, check for visibility dialog
							 | 
						|
								    if (!this.showAdmitPendingDialog) {
							 | 
						|
								      this.checkAndShowVisibilityDialog();
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async fetchMembers() {
							 | 
						|
								    try {
							 | 
						|
								      this.isLoading = true;
							 | 
						|
								      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) {
							 | 
						|
								      this.$logAndConsole(
							 | 
						|
								        "Error fetching members: " + errorStringForLog(error),
							 | 
						|
								        true,
							 | 
						|
								      );
							 | 
						|
								      this.emitError(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,
							 | 
						|
								          isRegistered: !!content.isRegistered,
							 | 
						|
								        });
							 | 
						|
								        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;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  decryptionErrorMessage(): string {
							 | 
						|
								    if (this.isOrganizer) {
							 | 
						|
								      if (this.decryptedMembers.length < this.members.length) {
							 | 
						|
								        return "Some members have data that cannot be decrypted with that password.";
							 | 
						|
								      } else {
							 | 
						|
								        // the lists must be equal
							 | 
						|
								        return "";
							 | 
						|
								      }
							 | 
						|
								    } else {
							 | 
						|
								      // non-organizers should only see problems if the first (organizer) member is not decrypted
							 | 
						|
								      if (
							 | 
						|
								        this.decryptedMembers.length === 0 ||
							 | 
						|
								        this.decryptedMembers[0].member.memberId !== this.members[0].memberId
							 | 
						|
								      ) {
							 | 
						|
								        return "Your password is not the same as the organizer. Retry or have them check their password.";
							 | 
						|
								      } else {
							 | 
						|
								        // the first (organizer) member was decrypted OK
							 | 
						|
								        return "";
							 | 
						|
								      }
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  membersToShow(): DecryptedMember[] {
							 | 
						|
								    let members: DecryptedMember[] = [];
							 | 
						|
								
							 | 
						|
								    if (this.isOrganizer) {
							 | 
						|
								      if (this.showOrganizerTools) {
							 | 
						|
								        members = this.decryptedMembers;
							 | 
						|
								      } else {
							 | 
						|
								        members = this.decryptedMembers.filter(
							 | 
						|
								          (member: DecryptedMember) => member.member.admitted,
							 | 
						|
								        );
							 | 
						|
								      }
							 | 
						|
								    } else {
							 | 
						|
								      // non-organizers only get visible members from server
							 | 
						|
								      members = this.decryptedMembers;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // Sort members according to priority:
							 | 
						|
								    // 1. Organizer at the top
							 | 
						|
								    // 2. Non-admitted members next
							 | 
						|
								    // 3. Everyone else after
							 | 
						|
								    return members.sort((a, b) => {
							 | 
						|
								      // Check if either member is the organizer (first member in original list)
							 | 
						|
								      const aIsOrganizer = a.member.memberId === this.members[0]?.memberId;
							 | 
						|
								      const bIsOrganizer = b.member.memberId === this.members[0]?.memberId;
							 | 
						|
								
							 | 
						|
								      // Organizer always comes first
							 | 
						|
								      if (aIsOrganizer && !bIsOrganizer) return -1;
							 | 
						|
								      if (!aIsOrganizer && bIsOrganizer) return 1;
							 | 
						|
								
							 | 
						|
								      // If both are organizers or neither are organizers, sort by admission status
							 | 
						|
								      if (aIsOrganizer && bIsOrganizer) return 0; // Both organizers, maintain original order
							 | 
						|
								
							 | 
						|
								      // Non-admitted members come before admitted members
							 | 
						|
								      if (!a.member.admitted && b.member.admitted) return -1;
							 | 
						|
								      if (a.member.admitted && !b.member.admitted) return 1;
							 | 
						|
								
							 | 
						|
								      // If admission status is the same, maintain original order
							 | 
						|
								      return 0;
							 | 
						|
								    });
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  informAboutAdmission() {
							 | 
						|
								    this.notify.info(
							 | 
						|
								      "This is to register people in Time Safari and to admit them to the meeting. A (+) symbol means they are not yet admitted and you can register and admit them. A (-) symbol means you can remove them, but they will stay registered.",
							 | 
						|
								      TIMEOUTS.VERY_LONG,
							 | 
						|
								    );
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  informAboutAddingContact(contactImportedAlready: boolean) {
							 | 
						|
								    if (contactImportedAlready) {
							 | 
						|
								      this.notify.info(
							 | 
						|
								        "They are in your contacts. To remove them, use the contacts page.",
							 | 
						|
								        TIMEOUTS.VERY_LONG,
							 | 
						|
								      );
							 | 
						|
								    } else {
							 | 
						|
								      this.notify.info(
							 | 
						|
								        "This is to add them to your contacts. To remove them later, use the contacts page.",
							 | 
						|
								        TIMEOUTS.VERY_LONG,
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async loadContacts() {
							 | 
						|
								    this.contacts = await this.$getAllContacts();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  getContactFor(did: string): Contact | undefined {
							 | 
						|
								    return this.contacts.find((contact) => contact.did === did);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  getPendingMembers() {
							 | 
						|
								    return this.decryptedMembers
							 | 
						|
								      .filter((member) => {
							 | 
						|
								        // Exclude the current user
							 | 
						|
								        if (member.did === this.activeDid) {
							 | 
						|
								          return false;
							 | 
						|
								        }
							 | 
						|
								        // Only include non-admitted members
							 | 
						|
								        return !member.member.admitted;
							 | 
						|
								      })
							 | 
						|
								      .map((member) => ({
							 | 
						|
								        did: member.did,
							 | 
						|
								        name: member.name,
							 | 
						|
								        isContact: !!this.getContactFor(member.did),
							 | 
						|
								        member: {
							 | 
						|
								          memberId: member.member.memberId.toString(),
							 | 
						|
								        },
							 | 
						|
								      }));
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  getMembersForVisibility() {
							 | 
						|
								    const membersForVisibility = 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) => ({
							 | 
						|
								        did: member.did,
							 | 
						|
								        name: member.name,
							 | 
						|
								        isContact: !!this.getContactFor(member.did),
							 | 
						|
								        member: {
							 | 
						|
								          memberId: member.member.memberId.toString(),
							 | 
						|
								        },
							 | 
						|
								      }));
							 | 
						|
								
							 | 
						|
								    // Deduplicate members by DID to prevent duplicate entries
							 | 
						|
								    const uniqueMembers = membersForVisibility.filter(
							 | 
						|
								      (member, index, self) =>
							 | 
						|
								        index === self.findIndex((m) => m.did === member.did),
							 | 
						|
								    );
							 | 
						|
								
							 | 
						|
								    return uniqueMembers;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Check if we should show the visibility dialog
							 | 
						|
								   * Returns true if there are members for visibility and either:
							 | 
						|
								   * - This is the first time (no previous members tracked), OR
							 | 
						|
								   * - New members have been added since last check (not removed), OR
							 | 
						|
								   * - This is a manual refresh (isManualRefresh flag is set)
							 | 
						|
								   */
							 | 
						|
								  shouldShowVisibilityDialog(): boolean {
							 | 
						|
								    // Only show for members who can see other members (i.e., they are in the decrypted members list)
							 | 
						|
								    const currentUserMember = this.decryptedMembers.find(
							 | 
						|
								      (member) => member.did === this.activeDid,
							 | 
						|
								    );
							 | 
						|
								
							 | 
						|
								    // If the current user is not in the decrypted members list, they can't see anyone
							 | 
						|
								    if (!currentUserMember) {
							 | 
						|
								      return false;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    const currentMembers = this.getMembersForVisibility();
							 | 
						|
								
							 | 
						|
								    if (currentMembers.length === 0) {
							 | 
						|
								      return false;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // If no previous members tracked, show dialog
							 | 
						|
								    if (this.previousVisibilityMembers.length === 0) {
							 | 
						|
								      return true;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // If this is a manual refresh, always show dialog if there are members
							 | 
						|
								    if (this.isManualRefresh) {
							 | 
						|
								      return true;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // Check if new members have been added (not just any change)
							 | 
						|
								    const currentMemberIds = currentMembers.map((m) => m.did);
							 | 
						|
								    const previousMemberIds = this.previousVisibilityMembers;
							 | 
						|
								
							 | 
						|
								    // Find new members (members in current but not in previous)
							 | 
						|
								    const newMembers = currentMemberIds.filter(
							 | 
						|
								      (id) => !previousMemberIds.includes(id),
							 | 
						|
								    );
							 | 
						|
								
							 | 
						|
								    // Only show dialog if there are new members added
							 | 
						|
								    return newMembers.length > 0;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Update the tracking of previous visibility members
							 | 
						|
								   */
							 | 
						|
								  updatePreviousVisibilityMembers() {
							 | 
						|
								    const currentMembers = this.getMembersForVisibility();
							 | 
						|
								    this.previousVisibilityMembers = currentMembers.map((m) => m.did);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Check if we should show the admit pending members dialog
							 | 
						|
								   */
							 | 
						|
								  shouldShowAdmitPendingDialog(): boolean {
							 | 
						|
								    // Don't show if already dismissed
							 | 
						|
								    if (this.admitDialogDismissed) {
							 | 
						|
								      return false;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // Only show for the organizer of the meeting
							 | 
						|
								    if (!this.isOrganizer) {
							 | 
						|
								      return false;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    const pendingMembers = this.getPendingMembers();
							 | 
						|
								    return pendingMembers.length > 0;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Show the admit pending members dialog if conditions are met
							 | 
						|
								   */
							 | 
						|
								  checkAndShowAdmitPendingDialog() {
							 | 
						|
								    if (this.shouldShowAdmitPendingDialog()) {
							 | 
						|
								      this.showAdmitPendingDialogMethod();
							 | 
						|
								    } else {
							 | 
						|
								      // Ensure dialog state is false when no pending members
							 | 
						|
								      this.showAdmitPendingDialog = false;
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Show the visibility dialog if conditions are met
							 | 
						|
								   */
							 | 
						|
								  checkAndShowVisibilityDialog() {
							 | 
						|
								    if (this.shouldShowVisibilityDialog()) {
							 | 
						|
								      this.showSetBulkVisibilityDialog();
							 | 
						|
								    }
							 | 
						|
								    this.updatePreviousVisibilityMembers();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
							 | 
						|
								    const contact = this.getContactFor(decrMember.did);
							 | 
						|
								    if (!decrMember.member.admitted && !contact) {
							 | 
						|
								      // If not a contact, show confirmation dialog
							 | 
						|
								      this.$notify(
							 | 
						|
								        {
							 | 
						|
								          group: "modal",
							 | 
						|
								          type: "confirm",
							 | 
						|
								          title: NOTIFY_ADD_CONTACT_FIRST.title,
							 | 
						|
								          text: NOTIFY_ADD_CONTACT_FIRST.text,
							 | 
						|
								          yesText: NOTIFY_ADD_CONTACT_FIRST.yesText,
							 | 
						|
								          noText: NOTIFY_ADD_CONTACT_FIRST.noText,
							 | 
						|
								          onYes: async () => {
							 | 
						|
								            await this.addAsContact(decrMember);
							 | 
						|
								            // After adding as contact, proceed with admission
							 | 
						|
								            await this.toggleAdmission(decrMember);
							 | 
						|
								          },
							 | 
						|
								          onNo: async () => {
							 | 
						|
								            // If they choose not to add as contact, show second confirmation
							 | 
						|
								            this.$notify(
							 | 
						|
								              {
							 | 
						|
								                group: "modal",
							 | 
						|
								                type: "confirm",
							 | 
						|
								                title: NOTIFY_CONTINUE_WITHOUT_ADDING.title,
							 | 
						|
								                text: NOTIFY_CONTINUE_WITHOUT_ADDING.text,
							 | 
						|
								                yesText: NOTIFY_CONTINUE_WITHOUT_ADDING.yesText,
							 | 
						|
								                onYes: async () => {
							 | 
						|
								                  await this.toggleAdmission(decrMember);
							 | 
						|
								                },
							 | 
						|
								                onCancel: async () => {
							 | 
						|
								                  // Do nothing, effectively canceling the operation
							 | 
						|
								                },
							 | 
						|
								              },
							 | 
						|
								              TIMEOUTS.MODAL,
							 | 
						|
								            );
							 | 
						|
								          },
							 | 
						|
								        },
							 | 
						|
								        TIMEOUTS.MODAL,
							 | 
						|
								      );
							 | 
						|
								    } else {
							 | 
						|
								      // If already a contact, proceed directly with admission
							 | 
						|
								      this.toggleAdmission(decrMember);
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async toggleAdmission(decrMember: DecryptedMember) {
							 | 
						|
								    try {
							 | 
						|
								      const headers = await getHeaders(this.activeDid);
							 | 
						|
								      await this.axios.put(
							 | 
						|
								        `${this.apiServer}/api/partner/groupOnboardMember/${decrMember.member.memberId}`,
							 | 
						|
								        { admitted: !decrMember.member.admitted },
							 | 
						|
								        { headers },
							 | 
						|
								      );
							 | 
						|
								      // Update local state
							 | 
						|
								      decrMember.member.admitted = !decrMember.member.admitted;
							 | 
						|
								
							 | 
						|
								      const oldContact = this.getContactFor(decrMember.did);
							 | 
						|
								      // if admitted, now register that user if they are not registered
							 | 
						|
								      if (
							 | 
						|
								        decrMember.member.admitted &&
							 | 
						|
								        !decrMember.isRegistered &&
							 | 
						|
								        !oldContact?.registered
							 | 
						|
								      ) {
							 | 
						|
								        const contactOldOrNew: Contact = oldContact || {
							 | 
						|
								          did: decrMember.did,
							 | 
						|
								          name: decrMember.name,
							 | 
						|
								        };
							 | 
						|
								        try {
							 | 
						|
								          const result = await register(
							 | 
						|
								            this.activeDid,
							 | 
						|
								            this.apiServer,
							 | 
						|
								            this.axios,
							 | 
						|
								            contactOldOrNew,
							 | 
						|
								          );
							 | 
						|
								          if (result.success) {
							 | 
						|
								            decrMember.isRegistered = true;
							 | 
						|
								            if (oldContact) {
							 | 
						|
								              await this.$updateContact(decrMember.did, { registered: true });
							 | 
						|
								              oldContact.registered = true;
							 | 
						|
								            }
							 | 
						|
								            this.notify.success(
							 | 
						|
								              "Besides being admitted, they were also registered.",
							 | 
						|
								              TIMEOUTS.STANDARD,
							 | 
						|
								            );
							 | 
						|
								          } else {
							 | 
						|
								            throw result;
							 | 
						|
								          }
							 | 
						|
								          // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						|
								        } catch (error: any) {
							 | 
						|
								          // registration failure is likely explained by a message from the server$notif
							 | 
						|
								          const additionalInfo =
							 | 
						|
								            serverMessageForUser(error) || error?.error || "";
							 | 
						|
								          this.notify.warning(
							 | 
						|
								            "They were admitted to the meeting. However, registration failed. You can register them from the contacts screen. " +
							 | 
						|
								              additionalInfo,
							 | 
						|
								            TIMEOUTS.VERY_LONG,
							 | 
						|
								          );
							 | 
						|
								        }
							 | 
						|
								      }
							 | 
						|
								    } catch (error) {
							 | 
						|
								      this.$logAndConsole(
							 | 
						|
								        "Error toggling admission: " + errorStringForLog(error),
							 | 
						|
								        true,
							 | 
						|
								      );
							 | 
						|
								      this.emitError(
							 | 
						|
								        serverMessageForUser(error) ||
							 | 
						|
								          "Failed to update member admission status.",
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async addAsContact(member: DecryptedMember) {
							 | 
						|
								    try {
							 | 
						|
								      const newContact = {
							 | 
						|
								        did: member.did,
							 | 
						|
								        name: member.name,
							 | 
						|
								      };
							 | 
						|
								
							 | 
						|
								      await this.$insertContact(newContact);
							 | 
						|
								      this.contacts.push(newContact);
							 | 
						|
								
							 | 
						|
								      this.notify.success(
							 | 
						|
								        "They were added to your contacts.",
							 | 
						|
								        TIMEOUTS.STANDARD,
							 | 
						|
								      );
							 | 
						|
								    } catch (err) {
							 | 
						|
								      this.$logAndConsole(
							 | 
						|
								        "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.error(message, TIMEOUTS.LONG);
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  showAdmitPendingDialogMethod() {
							 | 
						|
								    // Filter members to show only pending (non-admitted) members
							 | 
						|
								    const pendingMembers = this.getPendingMembers();
							 | 
						|
								
							 | 
						|
								    // Only show dialog if there are pending members
							 | 
						|
								    if (pendingMembers.length === 0) {
							 | 
						|
								      this.showAdmitPendingDialog = false;
							 | 
						|
								      return;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // Pause auto-refresh when dialog opens
							 | 
						|
								    this.stopAutoRefresh();
							 | 
						|
								
							 | 
						|
								    // Open the dialog directly
							 | 
						|
								    this.pendingMembersData = pendingMembers;
							 | 
						|
								    this.showAdmitPendingDialog = true;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  showSetBulkVisibilityDialog() {
							 | 
						|
								    // Filter members to show only those who need visibility set
							 | 
						|
								    const membersForVisibility = this.getMembersForVisibility();
							 | 
						|
								
							 | 
						|
								    // Pause auto-refresh when dialog opens
							 | 
						|
								    this.stopAutoRefresh();
							 | 
						|
								
							 | 
						|
								    // Open the dialog directly
							 | 
						|
								    this.visibilityDialogMembers = membersForVisibility;
							 | 
						|
								    this.showSetVisibilityDialog = true;
							 | 
						|
								
							 | 
						|
								    // Reset manual refresh flag after dialog is shown
							 | 
						|
								    this.isManualRefresh = false;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  startAutoRefresh() {
							 | 
						|
								    let lastRefreshTime = Date.now();
							 | 
						|
								    this.countdownTimer = 10;
							 | 
						|
								
							 | 
						|
								    this.autoRefreshInterval = setInterval(() => {
							 | 
						|
								      const now = Date.now();
							 | 
						|
								      const timeSinceLastRefresh = (now - lastRefreshTime) / 1000;
							 | 
						|
								
							 | 
						|
								      if (timeSinceLastRefresh >= 10) {
							 | 
						|
								        // Time to refresh
							 | 
						|
								        this.refreshData();
							 | 
						|
								        lastRefreshTime = now;
							 | 
						|
								        this.countdownTimer = 10;
							 | 
						|
								      } else {
							 | 
						|
								        // Update countdown
							 | 
						|
								        this.countdownTimer = Math.max(
							 | 
						|
								          0,
							 | 
						|
								          Math.round(10 - timeSinceLastRefresh),
							 | 
						|
								        );
							 | 
						|
								      }
							 | 
						|
								    }, 1000); // Update every second
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  stopAutoRefresh() {
							 | 
						|
								    if (this.autoRefreshInterval) {
							 | 
						|
								      clearInterval(this.autoRefreshInterval);
							 | 
						|
								      this.autoRefreshInterval = null;
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async manualRefresh() {
							 | 
						|
								    // Clear existing auto-refresh interval
							 | 
						|
								    if (this.autoRefreshInterval) {
							 | 
						|
								      clearInterval(this.autoRefreshInterval);
							 | 
						|
								      this.autoRefreshInterval = null;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // Set manual refresh flag
							 | 
						|
								    this.isManualRefresh = true;
							 | 
						|
								    // Reset the dismissed flag on manual refresh
							 | 
						|
								    this.admitDialogDismissed = false;
							 | 
						|
								
							 | 
						|
								    // Trigger immediate refresh
							 | 
						|
								    await this.refreshData();
							 | 
						|
								
							 | 
						|
								    // Only start auto-refresh if no dialogs are showing
							 | 
						|
								    if (!this.showAdmitPendingDialog && !this.showSetVisibilityDialog) {
							 | 
						|
								      this.startAutoRefresh();
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // Admit Pending Members Dialog methods
							 | 
						|
								  async closeAdmitPendingDialog() {
							 | 
						|
								    this.showAdmitPendingDialog = false;
							 | 
						|
								    this.pendingMembersData = [];
							 | 
						|
								    this.admitDialogDismissed = true;
							 | 
						|
								
							 | 
						|
								    // Handle manual refresh flow
							 | 
						|
								    if (this.isManualRefresh) {
							 | 
						|
								      await this.handleManualRefreshFlow();
							 | 
						|
								      this.isManualRefresh = false;
							 | 
						|
								    } else {
							 | 
						|
								      // Normal flow: refresh data and resume auto-refresh
							 | 
						|
								      this.refreshData();
							 | 
						|
								      this.startAutoRefresh();
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async handleManualRefreshFlow() {
							 | 
						|
								    // Refresh data to reflect any changes made in the admit dialog
							 | 
						|
								    await this.refreshData();
							 | 
						|
								
							 | 
						|
								    // Use the same logic as normal flow to check for visibility dialog
							 | 
						|
								    this.checkAndShowVisibilityDialog();
							 | 
						|
								
							 | 
						|
								    // If no visibility dialog was shown, resume auto-refresh
							 | 
						|
								    if (!this.showSetVisibilityDialog) {
							 | 
						|
								      this.startAutoRefresh();
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async onAdmitPendingSuccess(_result: {
							 | 
						|
								    admittedCount: number;
							 | 
						|
								    contactAddedCount: number;
							 | 
						|
								    visibilitySetCount: number;
							 | 
						|
								  }) {
							 | 
						|
								    // After admitting pending members, close the admit dialog
							 | 
						|
								    // The visibility dialog will be handled by the closeAdmitPendingDialog flow
							 | 
						|
								    await this.closeAdmitPendingDialog();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // Set Visibility Dialog methods
							 | 
						|
								  closeSetVisibilityDialog() {
							 | 
						|
								    this.showSetVisibilityDialog = false;
							 | 
						|
								    this.visibilityDialogMembers = [];
							 | 
						|
								    // Refresh data when dialog is closed to reflect any changes made
							 | 
						|
								    this.refreshData();
							 | 
						|
								    // Resume auto-refresh when dialog is closed
							 | 
						|
								    this.startAutoRefresh();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  beforeDestroy() {
							 | 
						|
								    this.stopAutoRefresh();
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								</script>
							 | 
						|
								
							 | 
						|
								<style scoped>
							 | 
						|
								/* Button Classes */
							 | 
						|
								.btn-action-refresh {
							 | 
						|
								  /* stylelint-disable-next-line at-rule-no-unknown */
							 | 
						|
								  @apply 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;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								.btn-add-contact {
							 | 
						|
								  /* stylelint-disable-next-line at-rule-no-unknown */
							 | 
						|
								  @apply text-lg text-green-600 hover:text-green-800 
							 | 
						|
								    transition-colors;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								.btn-info-contact,
							 | 
						|
								.btn-info-admission {
							 | 
						|
								  /* stylelint-disable-next-line at-rule-no-unknown */
							 | 
						|
								  @apply text-slate-400 hover:text-slate-600 
							 | 
						|
								    transition-colors;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								.btn-admission-add {
							 | 
						|
								  /* stylelint-disable-next-line at-rule-no-unknown */
							 | 
						|
								  @apply text-lg text-blue-500 hover:text-blue-700 
							 | 
						|
								    transition-colors;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								.btn-admission-remove {
							 | 
						|
								  /* stylelint-disable-next-line at-rule-no-unknown */
							 | 
						|
								  @apply text-lg text-rose-500 hover:text-rose-700 
							 | 
						|
								    transition-colors;
							 | 
						|
								}
							 | 
						|
								</style>
							 | 
						|
								
							 |