|
|
|
@ -1,197 +1,235 @@ |
|
|
|
<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> |
|
|
|
<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> |
|
|
|
|
|
|
|
<ul class="list-disc text-sm ps-4 space-y-2 mb-4"> |
|
|
|
<li |
|
|
|
v-if="membersToShow().length > 0 && showOrganizerTools && isOrganizer" |
|
|
|
> |
|
|
|
Click |
|
|
|
<span |
|
|
|
class="inline-block w-5 h-5 rounded-full bg-blue-100 text-blue-600 text-center" |
|
|
|
<!-- 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 |
|
|
|
" |
|
|
|
> |
|
|
|
<font-awesome icon="plus" class="text-sm" /> |
|
|
|
</span> |
|
|
|
/ |
|
|
|
<span |
|
|
|
class="inline-block w-5 h-5 rounded-full bg-blue-100 text-blue-600 text-center" |
|
|
|
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 && getNonContactMembers().length > 0 |
|
|
|
" |
|
|
|
> |
|
|
|
<font-awesome icon="minus" class="text-sm" /> |
|
|
|
</span> |
|
|
|
to add/remove them to/from the meeting. |
|
|
|
</li> |
|
|
|
<li v-if="membersToShow().length > 0"> |
|
|
|
Click |
|
|
|
<span |
|
|
|
class="inline-block w-5 h-5 rounded-full bg-green-100 text-green-600 text-center" |
|
|
|
> |
|
|
|
<font-awesome icon="circle-user" class="text-sm" /> |
|
|
|
</span> |
|
|
|
to add them to your contacts. |
|
|
|
</li> |
|
|
|
</ul> |
|
|
|
|
|
|
|
<div class="flex justify-between"> |
|
|
|
<!-- |
|
|
|
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 border-slate-300 py-1.5" |
|
|
|
<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="refreshData(false)" |
|
|
|
> |
|
|
|
<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" |
|
|
|
> |
|
|
|
<div class="flex items-center gap-2 justify-between"> |
|
|
|
<div class="flex items-center gap-1 overflow-hidden"> |
|
|
|
<h3 class="font-semibold truncate"> |
|
|
|
{{ member.name || unnamedMember }} |
|
|
|
</h3> |
|
|
|
<div |
|
|
|
v-if="!getContactFor(member.did) && member.did !== activeDid" |
|
|
|
class="flex items-center gap-1" |
|
|
|
<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 || member.did === activeDid), |
|
|
|
}, |
|
|
|
{ '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 || member.did === activeDid), |
|
|
|
}, |
|
|
|
]" |
|
|
|
> |
|
|
|
<font-awesome |
|
|
|
v-if="member.member.memberId === members[0]?.memberId" |
|
|
|
icon="crown" |
|
|
|
class="fa-fw text-amber-400" |
|
|
|
/> |
|
|
|
<font-awesome |
|
|
|
v-if="member.did === activeDid" |
|
|
|
icon="hand" |
|
|
|
class="fa-fw text-blue-500" |
|
|
|
/> |
|
|
|
<font-awesome |
|
|
|
v-if=" |
|
|
|
!member.member.admitted && |
|
|
|
(isOrganizer || member.did === activeDid) |
|
|
|
" |
|
|
|
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="btn-add-contact" |
|
|
|
title="Add as contact" |
|
|
|
@click="addAsContact(member)" |
|
|
|
:class=" |
|
|
|
member.member.admitted |
|
|
|
? 'btn-admission-remove' |
|
|
|
: 'btn-admission-add' |
|
|
|
" |
|
|
|
:title=" |
|
|
|
member.member.admitted ? 'Remove member' : 'Admit member' |
|
|
|
" |
|
|
|
@click="checkWhetherContactBeforeAdmitting(member)" |
|
|
|
> |
|
|
|
<font-awesome icon="circle-user" /> |
|
|
|
<font-awesome |
|
|
|
:icon=" |
|
|
|
member.member.admitted ? 'circle-minus' : 'circle-plus' |
|
|
|
" |
|
|
|
/> |
|
|
|
</button> |
|
|
|
|
|
|
|
<button |
|
|
|
class="btn-info-contact" |
|
|
|
title="Contact Info" |
|
|
|
@click=" |
|
|
|
informAboutAddingContact( |
|
|
|
getContactFor(member.did) !== undefined, |
|
|
|
) |
|
|
|
" |
|
|
|
class="btn-info-admission" |
|
|
|
title="Admission Info" |
|
|
|
@click="informAboutAdmission()" |
|
|
|
> |
|
|
|
<font-awesome icon="circle-info" class="text-sm" /> |
|
|
|
<font-awesome icon="circle-info" /> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<span |
|
|
|
v-if=" |
|
|
|
showOrganizerTools && isOrganizer && member.did !== activeDid |
|
|
|
" |
|
|
|
class="flex items-center gap-1" |
|
|
|
> |
|
|
|
<button |
|
|
|
class="btn-admission" |
|
|
|
:title=" |
|
|
|
member.member.admitted ? 'Remove member' : 'Admit member' |
|
|
|
" |
|
|
|
@click="checkWhetherContactBeforeAdmitting(member)" |
|
|
|
> |
|
|
|
<font-awesome |
|
|
|
:icon="member.member.admitted ? 'minus' : 'plus'" |
|
|
|
/> |
|
|
|
</button> |
|
|
|
|
|
|
|
<button |
|
|
|
class="btn-info-admission" |
|
|
|
title="Admission Info" |
|
|
|
@click="informAboutAdmission()" |
|
|
|
> |
|
|
|
<font-awesome icon="circle-info" class="text-sm" /> |
|
|
|
</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"> |
|
|
|
<!-- |
|
|
|
<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> |
|
|
|
<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="refreshData(false)" |
|
|
|
> |
|
|
|
<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> |
|
|
|
|
|
|
|
<p v-if="members.length === 0" class="text-gray-500 py-4"> |
|
|
|
No members have joined this meeting yet |
|
|
|
</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Set Visibility Dialog Component --> |
|
|
|
<SetBulkVisibilityDialog |
|
|
|
:visible="showSetVisibilityDialog" |
|
|
|
:members-data="visibilityDialogMembers" |
|
|
|
:active-did="activeDid" |
|
|
|
:api-server="apiServer" |
|
|
|
@close="closeSetVisibilityDialog" |
|
|
|
/> |
|
|
|
<!-- Bulk Members Dialog for both admitting and setting visibility --> |
|
|
|
<BulkMembersDialog |
|
|
|
ref="bulkMembersDialog" |
|
|
|
:active-did="activeDid" |
|
|
|
:api-server="apiServer" |
|
|
|
:dialog-type="isOrganizer ? 'admit' : 'visibility'" |
|
|
|
:is-organizer="isOrganizer" |
|
|
|
@close="closeBulkMembersDialogCallback" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script lang="ts"> |
|
|
|
import { Component, Vue, Prop, Emit } from "vue-facing-decorator"; |
|
|
|
|
|
|
|
import { NotificationIface } from "@/constants/app"; |
|
|
|
import { |
|
|
|
NOTIFY_ADD_CONTACT_FIRST, |
|
|
|
NOTIFY_CONTINUE_WITHOUT_ADDING, |
|
|
|
} from "@/constants/notifications"; |
|
|
|
import { SOMEONE_UNNAMED } from "@/constants/entities"; |
|
|
|
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"; |
|
|
|
} from "@/libs/endorserServer"; |
|
|
|
import { decryptMessage } from "@/libs/crypto"; |
|
|
|
import { Contact } from "@/db/tables/contacts"; |
|
|
|
import { MemberData } from "@/interfaces"; |
|
|
|
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 BulkMembersDialog from "./BulkMembersDialog.vue"; |
|
|
|
|
|
|
|
interface Member { |
|
|
|
admitted: boolean; |
|
|
|
@ -208,7 +246,7 @@ interface DecryptedMember { |
|
|
|
|
|
|
|
@Component({ |
|
|
|
components: { |
|
|
|
SetBulkVisibilityDialog, |
|
|
|
BulkMembersDialog, |
|
|
|
}, |
|
|
|
mixins: [PlatformServiceMixin], |
|
|
|
}) |
|
|
|
@ -216,7 +254,6 @@ 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; |
|
|
|
@ -227,6 +264,7 @@ export default class MembersList extends Vue { |
|
|
|
return message; |
|
|
|
} |
|
|
|
|
|
|
|
contacts: Array<Contact> = []; |
|
|
|
decryptedMembers: DecryptedMember[] = []; |
|
|
|
firstName = ""; |
|
|
|
isLoading = true; |
|
|
|
@ -237,23 +275,11 @@ export default class MembersList extends Vue { |
|
|
|
activeDid = ""; |
|
|
|
apiServer = ""; |
|
|
|
|
|
|
|
// 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; |
|
|
|
lastRefreshTime = 0; |
|
|
|
|
|
|
|
// Track previous visibility members to detect changes |
|
|
|
previousVisibilityMembers: string[] = []; |
|
|
|
previousMemberDidsIgnored: string[] = []; |
|
|
|
|
|
|
|
/** |
|
|
|
* Get the unnamed member constant |
|
|
|
@ -274,23 +300,8 @@ export default class MembersList extends Vue { |
|
|
|
|
|
|
|
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 visibility dialog on initial load |
|
|
|
this.checkAndShowVisibilityDialog(); |
|
|
|
} |
|
|
|
|
|
|
|
async refreshData() { |
|
|
|
// Force refresh both contacts and members |
|
|
|
await this.loadContacts(); |
|
|
|
await this.fetchMembers(); |
|
|
|
|
|
|
|
// Check if we should show the visibility dialog after refresh |
|
|
|
this.checkAndShowVisibilityDialog(); |
|
|
|
this.refreshData(); |
|
|
|
} |
|
|
|
|
|
|
|
async fetchMembers() { |
|
|
|
@ -336,7 +347,10 @@ export default class MembersList extends Vue { |
|
|
|
const content = JSON.parse(decryptedContent); |
|
|
|
|
|
|
|
this.decryptedMembers.push({ |
|
|
|
member: member, |
|
|
|
member: { |
|
|
|
...member, |
|
|
|
admitted: member.admitted !== undefined ? member.admitted : true, // Default to true for non-organizers |
|
|
|
}, |
|
|
|
name: content.name, |
|
|
|
did: content.did, |
|
|
|
isRegistered: !!content.isRegistered, |
|
|
|
@ -378,17 +392,76 @@ export default class MembersList extends Vue { |
|
|
|
} |
|
|
|
|
|
|
|
membersToShow(): DecryptedMember[] { |
|
|
|
let members: DecryptedMember[] = []; |
|
|
|
|
|
|
|
if (this.isOrganizer) { |
|
|
|
if (this.showOrganizerTools) { |
|
|
|
return this.decryptedMembers; |
|
|
|
members = this.decryptedMembers; |
|
|
|
} else { |
|
|
|
return this.decryptedMembers.filter( |
|
|
|
members = this.decryptedMembers.filter( |
|
|
|
(member: DecryptedMember) => member.member.admitted, |
|
|
|
); |
|
|
|
} |
|
|
|
} else { |
|
|
|
// non-organizers only get visible members from server, plus themselves |
|
|
|
|
|
|
|
// Check if current user is already in the decrypted members list |
|
|
|
if ( |
|
|
|
!this.decryptedMembers.find((member) => member.did === this.activeDid) |
|
|
|
) { |
|
|
|
// this is a stub for this user just in case they are waiting to get in |
|
|
|
// which is especially useful so they can see their own DID |
|
|
|
const currentUser: DecryptedMember = { |
|
|
|
member: { |
|
|
|
admitted: false, |
|
|
|
content: "{}", |
|
|
|
memberId: -1, |
|
|
|
}, |
|
|
|
name: this.firstName, |
|
|
|
did: this.activeDid, |
|
|
|
isRegistered: false, |
|
|
|
}; |
|
|
|
members = [currentUser, ...this.decryptedMembers]; |
|
|
|
} else { |
|
|
|
members = this.decryptedMembers; |
|
|
|
} |
|
|
|
} |
|
|
|
// non-organizers only get visible members from server |
|
|
|
return this.decryptedMembers; |
|
|
|
|
|
|
|
// Sort members according to priority: |
|
|
|
// 1. Organizer at the top |
|
|
|
// 2. Current user next |
|
|
|
// 3. Non-admitted members next |
|
|
|
// 4. 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; |
|
|
|
|
|
|
|
// Check if either member is the current user |
|
|
|
const aIsCurrentUser = a.did === this.activeDid; |
|
|
|
const bIsCurrentUser = b.did === this.activeDid; |
|
|
|
|
|
|
|
// Organizer always comes first |
|
|
|
if (aIsOrganizer && !bIsOrganizer) return -1; |
|
|
|
if (!aIsOrganizer && bIsOrganizer) return 1; |
|
|
|
|
|
|
|
// If both are organizers, maintain original order |
|
|
|
if (aIsOrganizer && bIsOrganizer) return 0; |
|
|
|
|
|
|
|
// Current user comes second (after organizer) |
|
|
|
if (aIsCurrentUser && !bIsCurrentUser && !bIsOrganizer) return -1; |
|
|
|
if (!aIsCurrentUser && bIsCurrentUser && !aIsOrganizer) return 1; |
|
|
|
|
|
|
|
// If both are current users, maintain original order |
|
|
|
if (aIsCurrentUser && bIsCurrentUser) return 0; |
|
|
|
|
|
|
|
// 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() { |
|
|
|
@ -412,92 +485,85 @@ export default class MembersList extends Vue { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async loadContacts() { |
|
|
|
this.contacts = await this.$getAllContacts(); |
|
|
|
} |
|
|
|
|
|
|
|
getContactFor(did: string): Contact | undefined { |
|
|
|
return this.contacts.find((contact) => contact.did === did); |
|
|
|
} |
|
|
|
|
|
|
|
getMembersForVisibility() { |
|
|
|
getPendingMembersToAdmit(): MemberData[] { |
|
|
|
return this.decryptedMembers |
|
|
|
.filter((member) => { |
|
|
|
// Exclude the current user |
|
|
|
if (member.did === this.activeDid) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
.filter( |
|
|
|
(member) => member.did !== this.activeDid && !member.member.admitted, |
|
|
|
) |
|
|
|
.map(this.convertDecryptedMemberToMemberData); |
|
|
|
} |
|
|
|
|
|
|
|
const contact = this.getContactFor(member.did); |
|
|
|
getNonContactMembers(): MemberData[] { |
|
|
|
return this.decryptedMembers |
|
|
|
.filter( |
|
|
|
(member) => |
|
|
|
member.did !== this.activeDid && !this.getContactFor(member.did), |
|
|
|
) |
|
|
|
.map(this.convertDecryptedMemberToMemberData); |
|
|
|
} |
|
|
|
|
|
|
|
// 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(), |
|
|
|
}, |
|
|
|
})); |
|
|
|
convertDecryptedMemberToMemberData( |
|
|
|
decryptedMember: DecryptedMember, |
|
|
|
): MemberData { |
|
|
|
return { |
|
|
|
did: decryptedMember.did, |
|
|
|
name: decryptedMember.name, |
|
|
|
isContact: !!this.getContactFor(decryptedMember.did), |
|
|
|
member: { |
|
|
|
memberId: decryptedMember.member.memberId.toString(), |
|
|
|
}, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 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) |
|
|
|
* Show the bulk members dialog if conditions are met |
|
|
|
* (admit pending members for organizers, add to contacts for non-organizers) |
|
|
|
*/ |
|
|
|
shouldShowVisibilityDialog(): boolean { |
|
|
|
const currentMembers = this.getMembersForVisibility(); |
|
|
|
async refreshData(bypassPromptIfAllWereIgnored = true) { |
|
|
|
// Force refresh both contacts and members |
|
|
|
this.contacts = await this.$getAllContacts(); |
|
|
|
await this.fetchMembers(); |
|
|
|
|
|
|
|
if (currentMembers.length === 0) { |
|
|
|
return false; |
|
|
|
const pendingMembers = this.isOrganizer |
|
|
|
? this.getPendingMembersToAdmit() |
|
|
|
: this.getNonContactMembers(); |
|
|
|
if (pendingMembers.length === 0) { |
|
|
|
this.startAutoRefresh(); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// If no previous members tracked, show dialog |
|
|
|
if (this.previousVisibilityMembers.length === 0) { |
|
|
|
return true; |
|
|
|
if (bypassPromptIfAllWereIgnored) { |
|
|
|
// only show if there are members that have not been ignored |
|
|
|
const pendingMembersNotIgnored = pendingMembers.filter( |
|
|
|
(member) => !this.previousMemberDidsIgnored.includes(member.did), |
|
|
|
); |
|
|
|
if (pendingMembersNotIgnored.length === 0) { |
|
|
|
this.startAutoRefresh(); |
|
|
|
// everyone waiting has been ignored |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 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; |
|
|
|
this.stopAutoRefresh(); |
|
|
|
(this.$refs.bulkMembersDialog as BulkMembersDialog).open(pendingMembers); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Update the tracking of previous visibility members |
|
|
|
*/ |
|
|
|
updatePreviousVisibilityMembers() { |
|
|
|
const currentMembers = this.getMembersForVisibility(); |
|
|
|
this.previousVisibilityMembers = currentMembers.map((m) => m.did); |
|
|
|
} |
|
|
|
// Bulk Members Dialog methods |
|
|
|
async closeBulkMembersDialogCallback( |
|
|
|
result: { notSelectedMemberDids: string[] } | undefined, |
|
|
|
) { |
|
|
|
this.previousMemberDidsIgnored = result?.notSelectedMemberDids || []; |
|
|
|
|
|
|
|
/** |
|
|
|
* Show the visibility dialog if conditions are met |
|
|
|
*/ |
|
|
|
checkAndShowVisibilityDialog() { |
|
|
|
if (this.shouldShowVisibilityDialog()) { |
|
|
|
this.showSetBulkVisibilityDialog(); |
|
|
|
} |
|
|
|
this.updatePreviousVisibilityMembers(); |
|
|
|
await this.refreshData(); |
|
|
|
} |
|
|
|
|
|
|
|
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) { |
|
|
|
const contact = this.getContactFor(decrMember.did); |
|
|
|
if (!decrMember.member.admitted && !contact) { |
|
|
|
// If not a contact, show confirmation dialog |
|
|
|
// If not a contact, stop auto-refresh and show confirmation dialog |
|
|
|
this.stopAutoRefresh(); |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "modal", |
|
|
|
@ -510,6 +576,7 @@ export default class MembersList extends Vue { |
|
|
|
await this.addAsContact(decrMember); |
|
|
|
// After adding as contact, proceed with admission |
|
|
|
await this.toggleAdmission(decrMember); |
|
|
|
this.startAutoRefresh(); |
|
|
|
}, |
|
|
|
onNo: async () => { |
|
|
|
// If they choose not to add as contact, show second confirmation |
|
|
|
@ -522,14 +589,19 @@ export default class MembersList extends Vue { |
|
|
|
yesText: NOTIFY_CONTINUE_WITHOUT_ADDING.yesText, |
|
|
|
onYes: async () => { |
|
|
|
await this.toggleAdmission(decrMember); |
|
|
|
this.startAutoRefresh(); |
|
|
|
}, |
|
|
|
onCancel: async () => { |
|
|
|
// Do nothing, effectively canceling the operation |
|
|
|
this.startAutoRefresh(); |
|
|
|
}, |
|
|
|
}, |
|
|
|
TIMEOUTS.MODAL, |
|
|
|
); |
|
|
|
}, |
|
|
|
onCancel: async () => { |
|
|
|
this.startAutoRefresh(); |
|
|
|
}, |
|
|
|
}, |
|
|
|
TIMEOUTS.MODAL, |
|
|
|
); |
|
|
|
@ -632,19 +704,8 @@ export default class MembersList extends Vue { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
startAutoRefresh() { |
|
|
|
this.stopAutoRefresh(); |
|
|
|
this.lastRefreshTime = Date.now(); |
|
|
|
this.countdownTimer = 10; |
|
|
|
|
|
|
|
@ -674,33 +735,6 @@ export default class MembersList extends Vue { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
manualRefresh() { |
|
|
|
// Clear existing auto-refresh interval |
|
|
|
if (this.autoRefreshInterval) { |
|
|
|
clearInterval(this.autoRefreshInterval); |
|
|
|
this.autoRefreshInterval = null; |
|
|
|
} |
|
|
|
|
|
|
|
// Trigger immediate refresh and restart timer |
|
|
|
this.refreshData(); |
|
|
|
this.startAutoRefresh(); |
|
|
|
|
|
|
|
// Always show dialog on manual refresh if there are members for visibility |
|
|
|
if (this.getMembersForVisibility().length > 0) { |
|
|
|
this.showSetBulkVisibilityDialog(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Set Visibility Dialog methods |
|
|
|
closeSetVisibilityDialog() { |
|
|
|
this.showSetVisibilityDialog = false; |
|
|
|
this.visibilityDialogMembers = []; |
|
|
|
// Refresh data when dialog is closed |
|
|
|
this.refreshData(); |
|
|
|
// Resume auto-refresh when dialog is closed |
|
|
|
this.startAutoRefresh(); |
|
|
|
} |
|
|
|
|
|
|
|
beforeDestroy() { |
|
|
|
this.stopAutoRefresh(); |
|
|
|
} |
|
|
|
@ -718,23 +752,26 @@ export default class MembersList extends Vue { |
|
|
|
|
|
|
|
.btn-add-contact { |
|
|
|
/* stylelint-disable-next-line at-rule-no-unknown */ |
|
|
|
@apply w-6 h-6 flex items-center justify-center rounded-full |
|
|
|
bg-green-100 text-green-600 hover:bg-green-200 hover:text-green-800 |
|
|
|
@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 w-6 h-6 flex items-center justify-center rounded-full |
|
|
|
bg-slate-100 text-slate-400 hover:text-slate-600 |
|
|
|
@apply text-slate-400 hover:text-slate-600 |
|
|
|
transition-colors; |
|
|
|
} |
|
|
|
|
|
|
|
.btn-admission { |
|
|
|
.btn-admission-add { |
|
|
|
/* stylelint-disable-next-line at-rule-no-unknown */ |
|
|
|
@apply 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 |
|
|
|
@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> |
|
|
|
|