add missing file for contact-profile-checking & add info button for explanation

This commit is contained in:
2026-02-21 15:25:32 -07:00
parent 07ebd1c32f
commit 30c6df557c
3 changed files with 355 additions and 5 deletions

View File

@@ -21,7 +21,7 @@
</button>
<font-awesome
icon="circle-info"
class="text-2xl text-blue-500 ml-2"
class="text-2xl text-blue-500 ml-4"
@click="emitShowCopyInfo"
/>
</div>

View File

@@ -0,0 +1,340 @@
<template>
<QuickNav selected="Contacts" />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<TopMessage />
<div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight">
Profile Check
</h1>
<button
class="order-first text-lg text-center leading-none p-1"
@click="goBack()"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</button>
</div>
<div
v-if="contactResults.length === 0"
class="text-center text-slate-500 mt-8"
>
No contacts to check. Go back and select contacts first.
</div>
<div v-else>
<div class="mb-4 text-sm text-slate-600">
Checked {{ completedCount }} of {{ contactResults.length }}
{{ contactResults.length === 1 ? "contact" : "contacts" }}
</div>
<div class="w-full bg-gray-200 rounded-full h-2 mb-6">
<div
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
:style="{ width: progressPercent + '%' }"
/>
</div>
<ul class="space-y-3">
<li
v-for="result in contactResults"
:key="result.did"
class="bg-slate-50 rounded-lg p-4 border border-slate-200"
>
<div class="flex items-start justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span v-if="result.status === 'pending'" class="text-slate-400">
<font-awesome icon="clock" class="fa-fw" />
</span>
<span
v-else-if="result.status === 'checking'"
class="text-blue-500"
>
<font-awesome icon="spinner" class="fa-spin-pulse fa-fw" />
</span>
<span
v-else-if="result.status === 'has-profile'"
class="text-green-600"
>
<font-awesome icon="circle-check" class="fa-fw" />
</span>
<span
v-else-if="result.status === 'no-profile'"
class="text-amber-500"
>
<font-awesome icon="circle-xmark" class="fa-fw" />
</span>
<span
v-else-if="result.status === 'error'"
class="text-red-500"
>
<font-awesome icon="triangle-exclamation" class="fa-fw" />
</span>
<span class="font-medium text-slate-800 truncate">
{{ result.name || "(no name)" }}
</span>
</div>
<div class="mt-1 text-sm">
<span v-if="result.status === 'pending'" class="text-slate-400">
Waiting
</span>
<span
v-else-if="result.status === 'checking'"
class="text-blue-500"
>
Checking profile
</span>
<span
v-else-if="result.status === 'has-profile'"
class="text-green-700"
>
Has profile
</span>
<span
v-else-if="result.status === 'no-profile'"
class="text-amber-600"
>
No profile
</span>
<span
v-else-if="result.status === 'error'"
class="text-red-600"
>
Error: {{ result.error }}
</span>
</div>
<div
v-if="
result.status === 'has-profile' && result.profileDescription
"
class="mt-2 text-sm text-slate-600 line-clamp-2"
>
{{ result.profileDescription }}
</div>
</div>
<router-link
:to="{ name: 'did', params: { did: result.did } }"
class="flex-shrink-0 text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
>
Contact
</router-link>
</div>
</li>
</ul>
<div v-if="isComplete" class="mt-6 p-4 bg-slate-100 rounded-lg">
<h3 class="font-semibold text-slate-800 mb-2">Summary</h3>
<div class="text-sm text-slate-600 space-y-1">
<div>
<span class="text-green-600 font-medium">{{ profileCount }}</span>
{{ profileCount === 1 ? "contact has" : "contacts have" }} a profile
</div>
<div>
<span class="text-amber-600 font-medium">{{ noProfileCount }}</span>
{{ noProfileCount === 1 ? "contact has" : "contacts have" }} no
profile or it's not visible to you
</div>
<div v-if="errorCount > 0">
<span class="text-red-600 font-medium">{{ errorCount }}</span>
{{ errorCount === 1 ? "check" : "checks" }} failed
</div>
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import {
DEFAULT_PARTNER_API_SERVER,
NotificationIface,
} from "../constants/app";
import { getHeaders } from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
interface ContactCheckResult {
did: string;
name: string;
status: "pending" | "checking" | "has-profile" | "no-profile" | "error";
profileDescription?: string;
error?: string;
}
@Component({
name: "ContactProfileCheckView",
components: {
QuickNav,
TopMessage,
},
mixins: [PlatformServiceMixin],
})
export default class ContactProfileCheckView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
notify!: ReturnType<typeof createNotifyHelpers>;
activeDid = "";
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
contactResults: ContactCheckResult[] = [];
created() {
this.notify = createNotifyHelpers(this.$notify);
}
async mounted() {
const settings = await this.$accountSettings();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.partnerApiServer =
(settings.partnerApiServer as string) || DEFAULT_PARTNER_API_SERVER;
const didsParam = this.$route.query.dids as string;
if (!didsParam) {
return;
}
const isFresh = sessionStorage.getItem("profileCheckFresh") === "true";
sessionStorage.removeItem("profileCheckFresh");
if (!isFresh) {
const cachedDids = sessionStorage.getItem("profileCheckDids");
const cachedResults = sessionStorage.getItem("profileCheckResults");
if (cachedDids === didsParam && cachedResults) {
try {
this.contactResults = JSON.parse(cachedResults);
return;
} catch {
// fall through to fresh load
}
}
}
sessionStorage.removeItem("profileCheckResults");
sessionStorage.removeItem("profileCheckDids");
let dids: string[];
try {
dids = JSON.parse(didsParam);
} catch {
this.notify.error("Invalid contact data.", TIMEOUTS.LONG);
return;
}
for (const did of dids) {
const contact = await this.$getContact(did);
this.contactResults.push({
did,
name: contact?.name || "",
status: "pending",
});
}
await this.runChecks();
}
get completedCount(): number {
return this.contactResults.filter(
(r) => r.status !== "pending" && r.status !== "checking",
).length;
}
get progressPercent(): number {
if (this.contactResults.length === 0) return 0;
return Math.round((this.completedCount / this.contactResults.length) * 100);
}
get isComplete(): boolean {
return (
this.contactResults.length > 0 &&
this.completedCount === this.contactResults.length
);
}
get profileCount(): number {
return this.contactResults.filter((r) => r.status === "has-profile").length;
}
get noProfileCount(): number {
return this.contactResults.filter((r) => r.status === "no-profile").length;
}
get errorCount(): number {
return this.contactResults.filter((r) => r.status === "error").length;
}
private async runChecks() {
if (!this.activeDid) {
this.notify.error("No active identity.", TIMEOUTS.LONG);
return;
}
for (let i = 0; i < this.contactResults.length; i++) {
this.contactResults[i].status = "checking";
this.contactResults = [...this.contactResults];
try {
const headers = await getHeaders(this.activeDid);
const url =
`${this.partnerApiServer}/api/partner/userProfileForIssuer/` +
encodeURIComponent(this.contactResults[i].did);
const response = await this.axios.get(url, { headers });
const data = response.data?.data;
if (data && data.description) {
this.contactResults[i].status = "has-profile";
this.contactResults[i].profileDescription = data.description;
} else {
this.contactResults[i].status = "no-profile";
}
} catch (err: unknown) {
const axiosErr = err as {
response?: { status?: number; data?: { error?: string } };
};
if (axiosErr.response?.status === 404) {
this.contactResults[i].status = "no-profile";
} else {
this.contactResults[i].status = "error";
this.contactResults[i].error =
axiosErr.response?.data?.error || "Could not check profile";
logger.error(
`Failed to check profile for ${this.contactResults[i].did}:`,
err,
);
}
}
this.contactResults = [...this.contactResults];
}
const didsParam = this.$route.query.dids as string;
if (didsParam) {
sessionStorage.setItem("profileCheckDids", didsParam);
sessionStorage.setItem(
"profileCheckResults",
JSON.stringify(this.contactResults),
);
}
}
goBack() {
this.$router.back();
}
}
</script>

View File

@@ -80,7 +80,7 @@
<!-- Label Filter -->
<div v-if="allLabels.length > 0" class="mt-4 mb-2">
<div class="flex items-center justify-between pl-[16.666%] pr-[16.666%]">
<div class="flex items-center justify-between pl-[10%] pr-[10%]">
<button
class="text-sm font-medium text-blue-600 flex items-center gap-1"
@click="showLabelFilter = !showLabelFilter"
@@ -133,11 +133,14 @@
class="my-3 flex justify-center"
>
<button
class="text-sm uppercase bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
class="text-sm bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
@click="checkSelectedProfiles"
>
<font-awesome icon="magnifying-glass" class="fa-fw mr-1" />
Check for Profile
<font-awesome icon="circle-user" class="fa-fw mr-1" />
Check for Public Profile
</button>
<button class="ml-4 text-2xl text-blue-500" @click="showProfileCheckInfo">
<font-awesome icon="circle-info" class="fa-fw" />
</button>
</div>
@@ -617,6 +620,13 @@ export default class ContactsView extends Vue {
/**
* Navigate to profile check view with selected contacts
*/
showProfileCheckInfo() {
this.notify.info(
"This User Profile visibility check is a useful report for meeting organizers when running meetings that match people based on common interests.",
TIMEOUTS.VERY_LONG,
);
}
checkSelectedProfiles() {
sessionStorage.setItem("profileCheckFresh", "true");
this.$router.push({