Browse Source

refactor: make the meeting member "set visibility" screen much like the organizer's "admit" screen

pull/211/head
Trent Larson 1 week ago
parent
commit
9e1c267bc0
  1. 1
      .husky/pre-commit
  2. 53
      src/components/AdmitPendingMembersDialog.vue
  3. 70
      src/components/DeepLinkTest.vue
  4. 178
      src/components/MembersList.vue
  5. 73
      src/components/SetBulkVisibilityDialog.vue
  6. 1
      src/interfaces/index.ts
  7. 9
      src/interfaces/user.ts

1
.husky/pre-commit

@ -37,7 +37,6 @@ if [ "$git_status_before" != "$git_status_after" ]; then
echo
printf "Choose [c/a]: "
# The `< /dev/tty` is necessary to make read work in git's non-interactive shell
# The `|| choice="a"` is useful to set a default value to abort if read fails
read choice < /dev/tty
case $choice in

53
src/components/AdmitPendingMembersDialog.vue

@ -16,7 +16,7 @@
<table
class="w-full border-collapse border border-slate-300 text-sm text-start"
>
<thead v-if="pendingMembersData && pendingMembersData.length > 0">
<thead v-if="membersData && membersData.length > 0">
<tr class="bg-slate-100 font-medium">
<th class="border border-slate-300 px-3 py-2">
<label class="flex items-center gap-2">
@ -33,7 +33,7 @@
</thead>
<tbody>
<!-- Dynamic data from MembersList -->
<tr v-if="!pendingMembersData || pendingMembersData.length === 0">
<tr v-if="!membersData || membersData.length === 0">
<td
class="border border-slate-300 px-3 py-2 text-center italic text-gray-500"
>
@ -41,7 +41,7 @@
</td>
</tr>
<tr
v-for="member in pendingMembersData || []"
v-for="member in membersData || []"
:key="member.member.memberId"
>
<td class="border border-slate-300 px-3 py-2">
@ -85,7 +85,7 @@
<div class="space-y-2">
<button
v-if="pendingMembersData && pendingMembersData.length > 0"
v-if="membersData && membersData.length > 0"
:disabled="!hasSelectedMembers"
:class="[
'block w-full text-center text-md font-bold uppercase px-2 py-2 rounded-md',
@ -95,7 +95,7 @@
]"
@click="admitWithVisibility"
>
Admit + Add Contact
Admit + Add to Contacts
</button>
<button
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@ -114,23 +114,15 @@ import { Vue, Component, Prop } from "vue-facing-decorator";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { SOMEONE_UNNAMED } from "@/constants/entities";
import { MemberData } from "@/interfaces";
import { setVisibilityUtil, getHeaders } from "@/libs/endorserServer";
import { createNotifyHelpers } from "@/utils/notify";
interface PendingMemberData {
did: string;
name: string;
isContact: boolean;
member: {
memberId: string;
};
}
@Component({
mixins: [PlatformServiceMixin],
emits: ["close"],
})
export default class AdmitPendingMembersDialog extends Vue {
@Prop({ default: () => [] }) pendingMembersData!: PendingMemberData[];
@Prop({ default: "" }) activeDid!: string;
@Prop({ default: "" }) apiServer!: string;
@ -144,6 +136,7 @@ export default class AdmitPendingMembersDialog extends Vue {
notify!: ReturnType<typeof createNotifyHelpers>;
// Component state
membersData: MemberData[] = [];
selectedMembers: string[] = [];
visible = false;
@ -156,30 +149,29 @@ export default class AdmitPendingMembersDialog extends Vue {
}
get isAllSelected() {
if (!this.pendingMembersData || this.pendingMembersData.length === 0)
return false;
return this.pendingMembersData.every((member) =>
if (!this.membersData || this.membersData.length === 0) return false;
return this.membersData.every((member) =>
this.selectedMembers.includes(member.did),
);
}
get isIndeterminate() {
if (!this.pendingMembersData || this.pendingMembersData.length === 0)
return false;
const selectedCount = this.pendingMembersData.filter((member) =>
if (!this.membersData || this.membersData.length === 0) return false;
const selectedCount = this.membersData.filter((member) =>
this.selectedMembers.includes(member.did),
).length;
return selectedCount > 0 && selectedCount < this.pendingMembersData.length;
return selectedCount > 0 && selectedCount < this.membersData.length;
}
created() {
this.notify = createNotifyHelpers(this.$notify);
}
open() {
open(members: MemberData[]) {
this.visible = true;
this.membersData = members;
// Select all by default
this.selectedMembers = this.pendingMembersData.map((member) => member.did);
this.selectedMembers = this.membersData.map((member) => member.did);
}
close(notSelectedMemberDids: string[]) {
@ -188,21 +180,18 @@ export default class AdmitPendingMembersDialog extends Vue {
}
cancel() {
this.close(this.pendingMembersData.map((member) => member.did));
this.close(this.membersData.map((member) => member.did));
}
toggleSelectAll() {
if (!this.pendingMembersData || this.pendingMembersData.length === 0)
return;
if (!this.membersData || this.membersData.length === 0) return;
if (this.isAllSelected) {
// Deselect all
this.selectedMembers = [];
} else {
// Select all
this.selectedMembers = this.pendingMembersData.map(
(member) => member.did,
);
this.selectedMembers = this.membersData.map((member) => member.did);
}
}
@ -221,10 +210,10 @@ export default class AdmitPendingMembersDialog extends Vue {
async admitWithVisibility() {
try {
const selectedMembers = this.pendingMembersData.filter((member) =>
const selectedMembers = this.membersData.filter((member) =>
this.selectedMembers.includes(member.did),
);
const notSelectedMembers = this.pendingMembersData.filter(
const notSelectedMembers = this.membersData.filter(
(member) => !this.selectedMembers.includes(member.did),
);

70
src/components/DeepLinkTest.vue

@ -0,0 +1,70 @@
<template>
<div>
<p>Deep Link Test Component Loaded</p>
<p>Platform: {{ platform }}</p>
<p>Status: {{ status }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
const platform = process.env.VITE_PLATFORM;
const status = ref("Initializing...");
const router = useRouter();
onMounted(async () => {
console.log("[DeepLinkTest] Component mounted, platform:", platform);
alert(`[DeepLinkTest] Component mounted, platform: ${platform}`);
if (platform !== "capacitor") {
status.value = "Not Capacitor platform";
return;
}
try {
console.log("[DeepLinkTest] Importing Capacitor App...");
const { App } = await import("@capacitor/app");
console.log("[DeepLinkTest] Getting app info...");
const appInfo = await App.getInfo();
console.log("[DeepLinkTest] App info:", appInfo);
alert(`[DeepLinkTest] App version: ${appInfo.version}`);
console.log("[DeepLinkTest] Registering appUrlOpen listener...");
App.addListener("appUrlOpen", (data: { url: string }) => {
console.log("[DeepLinkTest] Deep link received:", data.url);
alert(`[DeepLinkTest] Deep link received: ${data.url}`);
// Simple URL parsing without DeepLinkHandler
try {
const url = new URL(data.url);
const path = url.pathname;
console.log("[DeepLinkTest] Parsed path:", path);
// Simple navigation test
if (path.startsWith("/claim/")) {
const claimId = path.replace("/claim/", "");
router.push(`/claim/${claimId}`);
alert(`[DeepLinkTest] Navigated to claim: ${claimId}`);
} else {
router.push(path);
alert(`[DeepLinkTest] Navigated to: ${path}`);
}
} catch (error) {
console.error("[DeepLinkTest] URL parsing error:", error);
alert(`[DeepLinkTest] URL parsing error: ${error}`);
}
});
status.value = "Deep link listener registered";
console.log("[DeepLinkTest] Setup complete");
alert("[DeepLinkTest] Setup complete");
} catch (error) {
console.error("[DeepLinkTest] Error:", error);
alert(`[DeepLinkTest] Error: ${error}`);
status.value = `Error: ${error}`;
}
});
</script>

178
src/components/MembersList.vue

@ -1,4 +1,5 @@
<template>
<div>
<div class="space-y-4">
<!-- Loading State -->
<div
@ -48,7 +49,7 @@
<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"
@click="refreshData(false)"
>
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
Refresh
@ -163,7 +164,7 @@
<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"
@click="refreshData(false)"
>
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
Refresh
@ -177,43 +178,44 @@
</div>
</div>
<!-- Admit Pending Members Dialog Component -->
<!-- This Admit component is for the organizer to admit pending members to the meeting -->
<AdmitPendingMembersDialog
ref="admitPendingMembersDialog"
:active-did="activeDid"
:api-server="apiServer"
:pending-members-data="pendingMembersData"
@close="closeAdmitPendingDialog"
@close="closeMemberSelectionDialogCallback"
/>
<!-- This Bulk Visibility component is for non-organizer members to add other members to their contacts and set their visibility -->
<SetBulkVisibilityDialog
:visible="visibleBulkVisibilityDialog"
ref="setBulkVisibilityDialog"
:active-did="activeDid"
:api-server="apiServer"
:members-data="pendingMembersData"
@close="closeSetBulkVisibilityDialog"
@close="closeMemberSelectionDialogCallback"
/>
</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 * as libsUtil from "@/libs/util";
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 AdmitPendingMembersDialog from "./AdmitPendingMembersDialog.vue";
import SetBulkVisibilityDialog from "./SetBulkVisibilityDialog.vue";
@ -263,16 +265,6 @@ export default class MembersList extends Vue {
activeDid = "";
apiServer = "";
// Admit Pending Members Dialog state
pendingMembersData: Array<{
did: string;
name: string;
isContact: boolean;
member: { memberId: string };
}> = [];
visibleBulkVisibilityDialog = false;
// Auto-refresh functionality
countdownTimer = 10;
autoRefreshInterval: NodeJS.Timeout | null = null;
@ -302,16 +294,6 @@ export default class MembersList extends Vue {
this.refreshData();
}
async refreshData(showPendingEvenIfAllWereIgnored = false) {
// Force refresh both contacts and members
this.contacts = await this.$getAllContacts();
await this.fetchMembers();
// Check if we should show the admit pending members dialog first
this.checkAndShowAdmitPendingDialog(showPendingEvenIfAllWereIgnored);
}
async fetchMembers() {
try {
this.isLoading = true;
@ -408,8 +390,22 @@ export default class MembersList extends Vue {
);
}
} else {
// non-organizers only get visible members from server
members = this.decryptedMembers;
// non-organizers only get visible members from server, plus themselves
// 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,
};
const otherMembersPlusUser = [ currentUser, ...this.decryptedMembers ];
members = otherMembersPlusUser;
}
// Sort members according to priority:
@ -462,61 +458,51 @@ export default class MembersList extends Vue {
return this.contacts.find((contact) => contact.did === did);
}
getPendingMembers(): {
did: string;
name: string;
isContact: boolean;
member: { memberId: string };
}[] {
getPendingMembersToAdmit(): MemberData[] {
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(),
},
}));
.filter((member) =>
member.did !== this.activeDid && !member.member.admitted
)
.map(this.convertDecryptedMemberToMemberData);
}
getNonContactMembers(): {
did: string;
name: string;
isContact: boolean;
member: { memberId: string };
}[] {
getNonContactMembers(): MemberData[] {
return this.decryptedMembers
.filter((member) => !this.getContactFor(member.did))
.map((member) => ({
did: member.did,
name: member.name,
isContact: false,
member: {
memberId: member.member.memberId.toString(),
},
}));
.filter((member) =>
member.did !== this.activeDid && !this.getContactFor(member.did)
)
.map(this.convertDecryptedMemberToMemberData);
}
convertDecryptedMemberToMemberData(
decryptedMember: DecryptedMember,
): MemberData {
return {
did: decryptedMember.did,
name: decryptedMember.name,
isContact: !!this.getContactFor(decryptedMember.did),
member: {
memberId: decryptedMember.member.memberId.toString(),
},
};
}
/**
* Show the admit pending members dialog if conditions are met
*/
checkAndShowAdmitPendingDialog(showPendingEvenIfAllWereIgnored = false) {
async refreshData(bypassPromptIfAllWereIgnored = true) {
// Force refresh both contacts and members
this.contacts = await this.$getAllContacts();
await this.fetchMembers();
const pendingMembers = this.isOrganizer
? this.getPendingMembers()
? this.getPendingMembersToAdmit()
: this.getNonContactMembers();
if (pendingMembers.length === 0) {
this.startAutoRefresh();
return;
}
if (!showPendingEvenIfAllWereIgnored) {
if (bypassPromptIfAllWereIgnored) {
// only show if there are pending members that have not been ignored
const pendingMembersNotIgnored = pendingMembers.filter(
(member) => !this.previousMemberDidsIgnored.includes(member.did),
@ -528,31 +514,21 @@ export default class MembersList extends Vue {
}
}
this.stopAutoRefresh();
this.pendingMembersData = pendingMembers;
if (this.isOrganizer) {
(
this.$refs.admitPendingMembersDialog as AdmitPendingMembersDialog
).open();
).open(pendingMembers);
} else {
this.visibleBulkVisibilityDialog = true;
(
this.$refs.setBulkVisibilityDialog as SetBulkVisibilityDialog
).open(pendingMembers);
}
}
// Admit Pending Members Dialog methods
async closeAdmitPendingDialog(
async closeMemberSelectionDialogCallback(
result: { notSelectedMemberDids: string[] } | undefined,
) {
this.pendingMembersData = [];
this.previousMemberDidsIgnored = result?.notSelectedMemberDids || [];
await this.refreshData();
}
async closeSetBulkVisibilityDialog(
result: { notSelectedMemberDids: string[] } | undefined,
) {
this.visibleBulkVisibilityDialog = false;
this.pendingMembersData = [];
this.previousMemberDidsIgnored = result?.notSelectedMemberDids || [];
await this.refreshData();
@ -697,6 +673,7 @@ export default class MembersList extends Vue {
}
startAutoRefresh() {
this.stopAutoRefresh();
this.lastRefreshTime = Date.now();
this.countdownTimer = 10;
@ -726,17 +703,6 @@ export default class MembersList extends Vue {
}
}
async manualRefresh() {
// Clear existing auto-refresh interval
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval);
this.autoRefreshInterval = null;
}
// Trigger immediate refresh
await this.refreshData(true);
}
beforeDestroy() {
this.stopAutoRefresh();
}

73
src/components/SetBulkVisibilityDialog.vue

@ -3,15 +3,14 @@
<div class="dialog">
<div class="text-slate-900 text-center">
<h3 class="text-lg font-semibold leading-[1.25] mb-2">
Set Visibility to Meeting Members
Add Members to Contacts
</h3>
<p class="text-sm mb-4">
Would you like to <b>make your activities visible</b> to the following
members? (This will also add them as contacts if they aren't already.)
Would you like to add these members to your contacts?
</p>
<!-- Custom table area - you can customize this -->
<div v-if="shouldInitializeSelection" class="mb-4">
<div class="mb-4">
<table
class="w-full border-collapse border border-slate-300 text-sm text-start"
>
@ -36,7 +35,7 @@
<td
class="border border-slate-300 px-3 py-2 text-center italic text-gray-500"
>
No members need visibility settings
No members are not in your contacts
</td>
</tr>
<tr
@ -80,15 +79,13 @@
]"
@click="setVisibilityForSelectedMembers"
>
Set Visibility
Add to Contacts
</button>
<button
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@click="cancel"
>
{{
membersData && membersData.length > 0 ? "Maybe Later" : "Cancel"
}}
Maybe Later
</button>
</div>
</div>
@ -101,24 +98,15 @@ import { Vue, Component, Prop } from "vue-facing-decorator";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { SOMEONE_UNNAMED } from "@/constants/entities";
import { MemberData } from "@/interfaces";
import { setVisibilityUtil } from "@/libs/endorserServer";
import { createNotifyHelpers } from "@/utils/notify";
interface MemberData {
did: string;
name: string;
isContact: boolean;
member: {
memberId: string;
};
}
@Component({
mixins: [PlatformServiceMixin],
emits: ["close"],
})
export default class SetBulkVisibilityDialog extends Vue {
@Prop({ default: false }) visible!: boolean;
@Prop({ default: () => [] }) membersData!: MemberData[];
@Prop({ default: "" }) activeDid!: string;
@Prop({ default: "" }) apiServer!: string;
@ -132,8 +120,9 @@ export default class SetBulkVisibilityDialog extends Vue {
notify!: ReturnType<typeof createNotifyHelpers>;
// Component state
membersData: MemberData[] = [];
selectedMembers: string[] = [];
selectionInitialized = false;
visible = false;
// Constants
// In Vue templates, imported constants need to be explicitly made available to the template
@ -158,29 +147,24 @@ export default class SetBulkVisibilityDialog extends Vue {
return selectedCount > 0 && selectedCount < this.membersData.length;
}
get shouldInitializeSelection() {
// This method will initialize selection when the dialog opens
if (!this.selectionInitialized) {
this.initializeSelection();
this.selectionInitialized = true;
}
return true;
}
created() {
this.notify = createNotifyHelpers(this.$notify);
}
initializeSelection() {
// Reset selection when dialog opens
this.selectedMembers = [];
open(members: MemberData[]) {
this.visible = true;
this.membersData = members;
// Select all by default
this.selectedMembers = this.membersData.map((member) => member.did);
}
resetSelection() {
this.selectedMembers = [];
this.selectionInitialized = false;
close(notSelectedMemberDids: string[]) {
this.visible = false;
this.$emit("close", { notSelectedMemberDids: notSelectedMemberDids });
}
cancel() {
this.close(this.membersData.map((member) => member.did));
}
toggleSelectAll() {
@ -248,11 +232,7 @@ export default class SetBulkVisibilityDialog extends Vue {
5000,
);
// Emit success event
this.$emit("close", {
notSelectedMemberDids: notSelectedMembers.map((member) => member.did),
});
this.close();
this.close(notSelectedMembers.map((member) => member.did));
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error setting visibility:", error);
@ -325,16 +305,5 @@ export default class SetBulkVisibilityDialog extends Vue {
5000,
);
}
close() {
this.resetSelection();
this.$emit("close", {
notSelectedMemberDids: this.membersData.map((member) => member.did),
});
}
cancel() {
this.close();
}
}
</script>

1
src/interfaces/index.ts

@ -27,6 +27,7 @@ export type {
export type {
// From user.ts
UserInfo,
MemberData,
} from "./user";
export * from "./limits";

9
src/interfaces/user.ts

@ -6,3 +6,12 @@ export interface UserInfo {
profileImageUrl?: string;
nextPublicEncKeyHash?: string;
}
export interface MemberData {
did: string;
name: string;
isContact: boolean;
member: {
memberId: string;
};
}

Loading…
Cancel
Save