Compare commits
4 Commits
contact-pa
...
bulk-membe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c06ffec466 | ||
|
|
8b199ec76c | ||
| 7e861e2fca | |||
| 73806e78bc |
@@ -111,7 +111,7 @@
|
||||
? 'bg-blue-600 text-white cursor-pointer'
|
||||
: 'bg-slate-400 text-slate-200 cursor-not-allowed',
|
||||
]"
|
||||
@click="handleMainAction"
|
||||
@click="processSelectedMembers"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
@@ -134,8 +134,9 @@ 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 { setVisibilityUtil, getHeaders, register } from "@/libs/endorserServer";
|
||||
import { createNotifyHelpers } from "@/utils/notify";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
|
||||
@Component({
|
||||
mixins: [PlatformServiceMixin],
|
||||
@@ -144,7 +145,7 @@ import { createNotifyHelpers } from "@/utils/notify";
|
||||
export default class BulkMembersDialog extends Vue {
|
||||
@Prop({ default: "" }) activeDid!: string;
|
||||
@Prop({ default: "" }) apiServer!: string;
|
||||
@Prop({ required: true }) dialogType!: "admit" | "visibility";
|
||||
// isOrganizer: true = organizer mode (admit members), false = member mode (set visibility)
|
||||
@Prop({ required: true }) isOrganizer!: boolean;
|
||||
|
||||
// Vue notification system
|
||||
@@ -251,35 +252,35 @@ export default class BulkMembersDialog extends Vue {
|
||||
return this.selectedMembers.includes(memberDid);
|
||||
}
|
||||
|
||||
async handleMainAction() {
|
||||
if (this.dialogType === "admit") {
|
||||
await this.admitWithVisibility();
|
||||
} else {
|
||||
await this.addContactWithVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
async admitWithVisibility() {
|
||||
async processSelectedMembers() {
|
||||
try {
|
||||
const selectedMembers = this.membersData.filter((member) =>
|
||||
const selectedMembers: MemberData[] = this.membersData.filter((member) =>
|
||||
this.selectedMembers.includes(member.did),
|
||||
);
|
||||
const notSelectedMembers = this.membersData.filter(
|
||||
const notSelectedMembers: MemberData[] = this.membersData.filter(
|
||||
(member) => !this.selectedMembers.includes(member.did),
|
||||
);
|
||||
|
||||
let admittedCount = 0;
|
||||
let contactAddedCount = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const member of selectedMembers) {
|
||||
try {
|
||||
// First, admit the member
|
||||
await this.admitMember(member);
|
||||
admittedCount++;
|
||||
// Organizer mode: admit and register the member first
|
||||
if (this.isOrganizer) {
|
||||
await this.admitMember(member);
|
||||
await this.registerMember(member);
|
||||
admittedCount++;
|
||||
}
|
||||
|
||||
// If they're not a contact yet, add them as a contact
|
||||
if (!member.isContact) {
|
||||
await this.addAsContact(member);
|
||||
// Organizer mode: set isRegistered to true, member mode: undefined
|
||||
await this.addAsContact(
|
||||
member,
|
||||
this.isOrganizer ? true : undefined,
|
||||
);
|
||||
contactAddedCount++;
|
||||
}
|
||||
|
||||
@@ -289,85 +290,62 @@ export default class BulkMembersDialog extends Vue {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Error processing member ${member.did}:`, error);
|
||||
// Continue with other members even if one fails
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
// Show success notification
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Members Admitted Successfully",
|
||||
text: `${admittedCount} member${admittedCount === 1 ? "" : "s"} admitted${contactAddedCount === 0 ? "" : admittedCount === contactAddedCount ? " and" : `, ${contactAddedCount}`}${contactAddedCount === 0 ? "" : ` added as contact${contactAddedCount === 1 ? "" : "s"}`}.`,
|
||||
},
|
||||
10000,
|
||||
);
|
||||
if (this.isOrganizer) {
|
||||
if (admittedCount > 0) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Members Admitted Successfully",
|
||||
text: `${admittedCount} member${admittedCount === 1 ? "" : "s"} admitted and registered${contactAddedCount === 0 ? "" : admittedCount === contactAddedCount ? " and" : `, ${contactAddedCount}`}${contactAddedCount === 0 ? "" : ` added as contact${contactAddedCount === 1 ? "" : "s"}`}.`,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
if (errors > 0) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to fully admit some members. Work with them individually below.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Member mode: show contacts added notification
|
||||
if (contactAddedCount > 0) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Contacts Added Successfully",
|
||||
text: `${contactAddedCount} member${contactAddedCount === 1 ? "" : "s"} added as contact${contactAddedCount === 1 ? "" : "s"}.`,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.close(notSelectedMembers.map((member) => member.did));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error admitting members:", error);
|
||||
console.error(
|
||||
`Error ${this.isOrganizer ? "admitting members" : "adding contacts"}:`,
|
||||
error,
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to admit some members. Please try again.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async addContactWithVisibility() {
|
||||
try {
|
||||
const selectedMembers = this.membersData.filter((member) =>
|
||||
this.selectedMembers.includes(member.did),
|
||||
);
|
||||
const notSelectedMembers = this.membersData.filter(
|
||||
(member) => !this.selectedMembers.includes(member.did),
|
||||
);
|
||||
|
||||
let contactsAddedCount = 0;
|
||||
|
||||
for (const member of selectedMembers) {
|
||||
try {
|
||||
// If they're not a contact yet, add them as a contact first
|
||||
if (!member.isContact) {
|
||||
await this.addAsContact(member);
|
||||
contactsAddedCount++;
|
||||
}
|
||||
|
||||
// Set their seesMe to true
|
||||
await this.updateContactVisibility(member.did, true);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Error processing member ${member.did}:`, error);
|
||||
// Continue with other members even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
// Show success notification
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Contacts Added Successfully",
|
||||
text: `${contactsAddedCount} member${contactsAddedCount === 1 ? "" : "s"} added as contact${contactsAddedCount === 1 ? "" : "s"}.`,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
|
||||
this.close(notSelectedMembers.map((member) => member.did));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error adding contacts:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to add some members as contacts. Please try again.",
|
||||
text: "Some errors occurred. Work with members individually below.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
@@ -393,11 +371,39 @@ export default class BulkMembersDialog extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async addAsContact(member: { did: string; name: string }) {
|
||||
async registerMember(member: MemberData) {
|
||||
try {
|
||||
const newContact = {
|
||||
const contact: Contact = { did: member.did };
|
||||
const result = await register(
|
||||
this.activeDid,
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
contact,
|
||||
);
|
||||
if (result.success) {
|
||||
if (result.embeddedRecordError) {
|
||||
throw new Error(result.embeddedRecordError);
|
||||
}
|
||||
await this.$updateContact(member.did, { registered: true });
|
||||
} else {
|
||||
throw result;
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error registering member:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async addAsContact(
|
||||
member: { did: string; name: string },
|
||||
isRegistered?: boolean,
|
||||
) {
|
||||
try {
|
||||
const newContact: Contact = {
|
||||
did: member.did,
|
||||
name: member.name,
|
||||
registered: isRegistered,
|
||||
};
|
||||
|
||||
await this.$insertContact(newContact);
|
||||
@@ -440,10 +446,10 @@ export default class BulkMembersDialog extends Vue {
|
||||
}
|
||||
|
||||
showContactInfo() {
|
||||
const message =
|
||||
this.dialogType === "admit"
|
||||
? "This user is already your contact, but they are not yet admitted to the meeting."
|
||||
: "This user is already your contact, but your activities are not visible to them yet.";
|
||||
// isOrganizer: true = admit mode, false = visibility mode
|
||||
const message = this.isOrganizer
|
||||
? "This user is already your contact, but they are not yet admitted to the meeting."
|
||||
: "This user is already your contact, but your activities are not visible to them yet.";
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
<font-awesome
|
||||
v-if="member.did === activeDid"
|
||||
icon="hand"
|
||||
class="fa-fw text-blue-500"
|
||||
class="fa-fw text-slate-500"
|
||||
/>
|
||||
<font-awesome
|
||||
v-if="
|
||||
@@ -113,10 +113,10 @@
|
||||
</h3>
|
||||
<div
|
||||
v-if="!getContactFor(member.did) && member.did !== activeDid"
|
||||
class="flex items-center gap-1.5 ms-1"
|
||||
class="flex items-center gap-1.5 ml-2 ms-1"
|
||||
>
|
||||
<button
|
||||
class="btn-add-contact"
|
||||
class="btn-add-contact ml-2"
|
||||
title="Add as contact"
|
||||
@click="addAsContact(member)"
|
||||
>
|
||||
@@ -124,7 +124,7 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn-info-contact"
|
||||
class="btn-info-contact ml-2"
|
||||
title="Contact Info"
|
||||
@click="
|
||||
informAboutAddingContact(
|
||||
@@ -135,6 +135,27 @@
|
||||
<font-awesome icon="circle-info" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="getContactFor(member.did) && member.did !== activeDid"
|
||||
class="flex items-center gap-1.5 ms-1"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'contact-edit', params: { did: member.did } }"
|
||||
>
|
||||
<font-awesome
|
||||
icon="pen"
|
||||
class="text-sm text-blue-500 ml-2 mb-1"
|
||||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'did', params: { did: member.did } }"
|
||||
>
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="text-sm text-blue-500 ml-2 mb-1"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="
|
||||
@@ -202,7 +223,6 @@
|
||||
ref="bulkMembersDialog"
|
||||
:active-did="activeDid"
|
||||
:api-server="apiServer"
|
||||
:dialog-type="isOrganizer ? 'admit' : 'visibility'"
|
||||
:is-organizer="isOrganizer"
|
||||
@close="closeBulkMembersDialogCallback"
|
||||
/>
|
||||
|
||||
@@ -510,14 +510,6 @@ export const NOTIFY_REGISTER_CONTACT = {
|
||||
text: "Do you want to register them?",
|
||||
};
|
||||
|
||||
// Used in: ContactsView.vue (showOnboardMeetingDialog method - complex modal for onboarding meeting)
|
||||
export const NOTIFY_ONBOARDING_MEETING = {
|
||||
title: "Onboarding Meeting",
|
||||
text: "Would you like to start a new meeting?",
|
||||
yesText: "Start New Meeting",
|
||||
noText: "Join Existing Meeting",
|
||||
};
|
||||
|
||||
// TestView.vue specific constants
|
||||
// Used in: TestView.vue (executeSql method - SQL error handling)
|
||||
export const NOTIFY_SQL_ERROR = {
|
||||
|
||||
@@ -70,15 +70,6 @@ export interface AxiosErrorResponse {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
did: string;
|
||||
name: string;
|
||||
publicEncKey: string;
|
||||
registered: boolean;
|
||||
profileImageUrl?: string;
|
||||
nextPublicEncKeyHash?: string;
|
||||
}
|
||||
|
||||
export interface CreateAndSubmitClaimResult {
|
||||
success: boolean;
|
||||
embeddedRecordError?: string;
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from "./common";
|
||||
export * from "./deepLinks";
|
||||
export * from "./limits";
|
||||
export * from "./records";
|
||||
export * from "./user";
|
||||
|
||||
@@ -42,9 +42,6 @@ import {
|
||||
PlanActionClaim,
|
||||
RegisterActionClaim,
|
||||
TenureClaim,
|
||||
} from "../interfaces/claims";
|
||||
|
||||
import {
|
||||
GenericCredWrapper,
|
||||
GenericVerifiableCredential,
|
||||
AxiosErrorResponse,
|
||||
@@ -55,13 +52,11 @@ import {
|
||||
QuantitativeValue,
|
||||
KeyMetaWithPrivate,
|
||||
KeyMetaMaybeWithPrivate,
|
||||
} from "../interfaces/common";
|
||||
import {
|
||||
OfferSummaryRecord,
|
||||
OfferToPlanSummaryRecord,
|
||||
PlanSummaryAndPreviousClaim,
|
||||
PlanSummaryRecord,
|
||||
} from "../interfaces/records";
|
||||
} from "../interfaces";
|
||||
import { logger, safeStringify } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { APP_SERVER } from "@/constants/app";
|
||||
@@ -1662,30 +1657,35 @@ export async function register(
|
||||
message?: string;
|
||||
}>(url, { jwtEncoded: vcJwt });
|
||||
|
||||
if (resp.data?.success?.handleId) {
|
||||
return { success: true };
|
||||
} else if (resp.data?.success?.embeddedRecordError) {
|
||||
if (resp.data?.success?.embeddedRecordError) {
|
||||
let message =
|
||||
"There was some problem with the registration and so it may not be complete.";
|
||||
if (typeof resp.data.success.embeddedRecordError === "string") {
|
||||
message += " " + resp.data.success.embeddedRecordError;
|
||||
}
|
||||
return { error: message };
|
||||
} else if (resp.data?.success?.handleId) {
|
||||
return { success: true };
|
||||
} else {
|
||||
logger.error("Registration error:", JSON.stringify(resp.data));
|
||||
return { error: "Got a server error when registering." };
|
||||
logger.error("Registration non-thrown error:", JSON.stringify(resp.data));
|
||||
return {
|
||||
error:
|
||||
(resp.data?.error as { message?: string })?.message ||
|
||||
(resp.data?.error as string) ||
|
||||
"Got a server error when registering.",
|
||||
};
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === "object") {
|
||||
const err = error as AxiosErrorResponse;
|
||||
const errorMessage =
|
||||
err.message ||
|
||||
(err.response?.data &&
|
||||
typeof err.response.data === "object" &&
|
||||
"message" in err.response.data
|
||||
? (err.response.data as { message: string }).message
|
||||
: undefined);
|
||||
logger.error("Registration error:", errorMessage || JSON.stringify(err));
|
||||
err.response?.data?.error?.message ||
|
||||
err.response?.data?.error ||
|
||||
err.message;
|
||||
logger.error(
|
||||
"Registration thrown error:",
|
||||
errorMessage || JSON.stringify(err),
|
||||
);
|
||||
return { error: errorMessage || "Got a server error when registering." };
|
||||
}
|
||||
return { error: "Got a server error when registering." };
|
||||
|
||||
@@ -5,6 +5,5 @@ export interface UserProfile {
|
||||
locLat2?: number;
|
||||
locLon2?: number;
|
||||
issuerDid: string;
|
||||
issuerDidVisibleToDids?: Array<string>;
|
||||
rowId?: string; // set on profile retrieved from server
|
||||
}
|
||||
|
||||
@@ -2171,7 +2171,7 @@ export default class AccountViewView extends Vue {
|
||||
const headers = await getHeaders(did);
|
||||
|
||||
const response = await this.axios.delete(
|
||||
`${this.partnerApiServer}/api/partner/userProfile`,
|
||||
`${this.partnerApiServer}/api/partner/userProfile/${did}`,
|
||||
{ headers },
|
||||
);
|
||||
|
||||
|
||||
@@ -346,9 +346,7 @@ export default class ContactEditView extends Vue {
|
||||
|
||||
// Notify success and redirect
|
||||
this.notify.success(NOTIFY_CONTACT_SAVED.message, TIMEOUTS.STANDARD);
|
||||
(this.$router as Router).push({
|
||||
path: "/did/" + encodeURIComponent(this.contact?.did || ""),
|
||||
});
|
||||
this.$router.back();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -171,9 +171,11 @@ import {
|
||||
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
||||
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
||||
} from "../libs/endorserServer";
|
||||
import { GiveSummaryRecord } from "@/interfaces/records";
|
||||
import { UserInfo } from "@/interfaces/common";
|
||||
import { VerifiableCredential } from "@/interfaces/claims-result";
|
||||
import {
|
||||
GiveSummaryRecord,
|
||||
UserInfo,
|
||||
VerifiableCredential,
|
||||
} from "@/interfaces";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import {
|
||||
generateSaveAndActivateIdentity,
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
</h1>
|
||||
|
||||
<!-- Back -->
|
||||
<router-link
|
||||
<button
|
||||
class="order-first text-lg text-center leading-none p-1"
|
||||
:to="{ name: 'contacts' }"
|
||||
@click="goBack()"
|
||||
>
|
||||
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
|
||||
</router-link>
|
||||
</button>
|
||||
|
||||
<!-- Help button -->
|
||||
<router-link
|
||||
:to="{ name: 'help' }"
|
||||
<button
|
||||
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
|
||||
@click="goToHelp()"
|
||||
>
|
||||
<font-awesome icon="question" class="block text-center w-[1em]" />
|
||||
</router-link>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Identity Details -->
|
||||
@@ -476,7 +476,7 @@ export default class DIDView extends Vue {
|
||||
* Navigation helper methods
|
||||
*/
|
||||
goBack() {
|
||||
this.$router.go(-1);
|
||||
this.$router.back();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -473,6 +473,7 @@ export default class OnboardMeetingView extends Vue {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const password: string = this.newOrUpdatedMeetingInputs.password;
|
||||
|
||||
// create content with user's name & DID encrypted with password
|
||||
const content = {
|
||||
@@ -482,7 +483,7 @@ export default class OnboardMeetingView extends Vue {
|
||||
};
|
||||
const encryptedContent = await encryptMessage(
|
||||
JSON.stringify(content),
|
||||
this.newOrUpdatedMeetingInputs.password,
|
||||
password,
|
||||
);
|
||||
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
@@ -505,6 +506,11 @@ export default class OnboardMeetingView extends Vue {
|
||||
|
||||
this.newOrUpdatedMeetingInputs = null;
|
||||
this.notify.success(NOTIFY_MEETING_CREATED.message, TIMEOUTS.STANDARD);
|
||||
// redirect to the same page with the password parameter set
|
||||
this.$router.push({
|
||||
name: "onboard-meeting-setup",
|
||||
query: { password: password },
|
||||
});
|
||||
} else {
|
||||
throw { response: response };
|
||||
}
|
||||
|
||||
@@ -54,161 +54,6 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Issuer Information Section -->
|
||||
<div v-if="profile" class="mt-6">
|
||||
<div
|
||||
v-if="issuerContact"
|
||||
class="bg-blue-50 border border-blue-200 rounded-lg p-4"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-blue-800 mb-2">
|
||||
<font-awesome icon="user-check" class="fa-fw mr-2" />
|
||||
Contact Information
|
||||
</h3>
|
||||
<p class="text-sm text-blue-700 mb-3">
|
||||
{{ issuerContact.name || "Contact" }} is in your contacts.
|
||||
</p>
|
||||
<div
|
||||
v-if="
|
||||
issuerContact.contactMethods &&
|
||||
issuerContact.contactMethods.length > 0
|
||||
"
|
||||
class="space-y-2"
|
||||
>
|
||||
<h4 class="font-medium text-blue-800">Contact Methods:</h4>
|
||||
<div
|
||||
v-for="method in issuerContact.contactMethods"
|
||||
:key="method.label"
|
||||
class="flex items-center text-sm"
|
||||
>
|
||||
<span class="font-medium text-blue-700 w-20"
|
||||
>{{ method.label }}:</span
|
||||
>
|
||||
<span class="text-blue-600">{{ method.value }}</span>
|
||||
<span class="text-xs text-blue-500 ml-2"
|
||||
>({{ method.type }})</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="bg-yellow-50 border border-yellow-200 rounded-lg p-4"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-yellow-800 mb-2">
|
||||
<font-awesome icon="user-plus" class="fa-fw mr-2" />
|
||||
Not in Contacts
|
||||
</h3>
|
||||
<p class="text-sm text-yellow-700 mb-3">
|
||||
This person has been connected but they're not in your contacts on
|
||||
this device.
|
||||
</p>
|
||||
<button
|
||||
class="text-blue-600 hover:text-blue-800 font-medium text-sm flex items-center"
|
||||
@click="toggleConnectInstructions"
|
||||
>
|
||||
<font-awesome icon="link" class="fa-fw mr-1" />
|
||||
Connect Me...
|
||||
<font-awesome
|
||||
:icon="showConnectInstructions ? 'chevron-up' : 'chevron-down'"
|
||||
class="fa-fw ml-1"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- Expandable Instructions -->
|
||||
<div
|
||||
v-if="showConnectInstructions"
|
||||
class="mt-4 p-4 bg-white border border-yellow-300 rounded-md"
|
||||
>
|
||||
<h4 class="font-medium text-gray-800 mb-3">How to Connect:</h4>
|
||||
|
||||
<!-- Copy Profile Link Instructions -->
|
||||
<div class="mb-4">
|
||||
<p class="text-sm text-gray-700 mb-2">
|
||||
1. Copy a deep link to this profile and send it to someone in
|
||||
your network:
|
||||
</p>
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white text-sm px-3 py-1 rounded flex items-center"
|
||||
@click="onCopyLinkClick()"
|
||||
>
|
||||
<font-awesome icon="copy" class="fa-fw mr-1" />
|
||||
Copy Profile Link
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Contact List -->
|
||||
<div class="mb-4">
|
||||
<p class="text-sm text-gray-700 mb-2">
|
||||
2. Send the link to one of these people:
|
||||
</p>
|
||||
|
||||
<!-- Contacts from issuerDidVisibleToDids -->
|
||||
<div v-if="visibleToContacts.length > 0" class="mb-3">
|
||||
<h5 class="text-xs font-medium text-gray-600 mb-2">
|
||||
People who can see this profile:
|
||||
</h5>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="contact in visibleToContacts"
|
||||
:key="contact.did"
|
||||
class="flex items-center text-sm bg-gray-50 p-2 rounded"
|
||||
>
|
||||
<font-awesome
|
||||
icon="user"
|
||||
class="fa-fw text-gray-500 mr-2"
|
||||
/>
|
||||
<span class="font-medium">{{
|
||||
contact.name || "Unnamed Contact"
|
||||
}}</span>
|
||||
<span class="text-xs text-gray-500 ml-2"
|
||||
>({{ contact.did.substring(0, 20) }}...)</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nearest Neighbors -->
|
||||
<div v-if="nearestNeighbors.length > 0" class="mb-3">
|
||||
<h5 class="text-xs font-medium text-gray-600 mb-2">
|
||||
<span v-if="visibleToContacts.length > 0"
|
||||
>Other nearby people:</span
|
||||
>
|
||||
<span v-else>Nearby people:</span>
|
||||
</h5>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="neighbor in nearestNeighbors"
|
||||
:key="neighbor.issuerDid"
|
||||
class="flex items-center text-sm bg-gray-50 p-2 rounded"
|
||||
>
|
||||
<font-awesome
|
||||
icon="user"
|
||||
class="fa-fw text-gray-500 mr-2"
|
||||
/>
|
||||
<span class="font-medium">{{
|
||||
neighborDisplayName(neighbor)
|
||||
}}</span>
|
||||
<span class="text-xs text-gray-500 ml-2"
|
||||
>({{ neighbor.issuerDid.substring(0, 20) }}...)</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state for nearest neighbors -->
|
||||
<div
|
||||
v-if="loadingNearestNeighbors"
|
||||
class="text-sm text-gray-500 flex items-center"
|
||||
>
|
||||
<font-awesome icon="spinner" class="fa-spin fa-fw mr-2" />
|
||||
Loading nearby people...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map for first coordinates -->
|
||||
<div v-if="hasFirstLocation" class="mt-4">
|
||||
<h2 class="text-lg font-semibold">Location</h2>
|
||||
@@ -268,12 +113,7 @@ import {
|
||||
APP_SERVER,
|
||||
} from "../constants/app";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import {
|
||||
didInfo,
|
||||
getHeaders,
|
||||
isHiddenDid,
|
||||
contactForDid,
|
||||
} from "../libs/endorserServer";
|
||||
import { didInfo, getHeaders } from "../libs/endorserServer";
|
||||
import { UserProfile } from "../libs/partnerServer";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
@@ -322,13 +162,9 @@ export default class UserProfileView extends Vue {
|
||||
isLoading = true;
|
||||
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
||||
profile: UserProfile | null = null;
|
||||
showConnectInstructions = false;
|
||||
nearestNeighbors: Array<UserProfile> = [];
|
||||
loadingNearestNeighbors = false;
|
||||
|
||||
// make this function available to the Vue template
|
||||
didInfo = didInfo;
|
||||
isHiddenDid = isHiddenDid;
|
||||
/** Production share domain for deep links */
|
||||
APP_SERVER = APP_SERVER;
|
||||
|
||||
@@ -430,63 +266,6 @@ export default class UserProfileView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads nearest neighbors from the partner API
|
||||
* Called when the Connect Me section is expanded
|
||||
*/
|
||||
async loadNearestNeighbors() {
|
||||
if (!this.profile?.issuerDid || this.loadingNearestNeighbors) return;
|
||||
|
||||
this.loadingNearestNeighbors = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${this.partnerApiServer}/api/partner/userProfileNearestNeighbors?issuerDid=${encodeURIComponent(this.profile.issuerDid)}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: await getHeaders(this.activeDid),
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
const result = await response.json();
|
||||
this.nearestNeighbors = result.data || [];
|
||||
} else {
|
||||
logger.warn("Failed to load nearest neighbors:", response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error loading nearest neighbors:", error);
|
||||
} finally {
|
||||
this.loadingNearestNeighbors = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets display name for a neighbor profile
|
||||
* @param neighbor UserProfile object
|
||||
* @returns Display name for the neighbor
|
||||
*/
|
||||
neighborDisplayName(neighbor: UserProfile) {
|
||||
return this.didInfo(
|
||||
neighbor.issuerDid,
|
||||
this.activeDid,
|
||||
this.allMyDids,
|
||||
this.allContacts,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the Connect Me instructions and loads nearest neighbors if needed
|
||||
*/
|
||||
async toggleConnectInstructions() {
|
||||
this.showConnectInstructions = !this.showConnectInstructions;
|
||||
|
||||
// Load nearest neighbors when expanding for the first time
|
||||
if (this.showConnectInstructions && this.nearestNeighbors.length === 0) {
|
||||
await this.loadNearestNeighbors();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed properties for template logic streamlining
|
||||
*/
|
||||
@@ -504,27 +283,6 @@ export default class UserProfileView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contact information for the profile issuer
|
||||
* @returns Contact object if issuer is in contacts, undefined otherwise
|
||||
*/
|
||||
get issuerContact() {
|
||||
if (!this.profile?.issuerDid) return undefined;
|
||||
return contactForDid(this.profile.issuerDid, this.allContacts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets contacts that are in the issuerDidVisibleToDids list
|
||||
* @returns Array of contacts who can see this profile
|
||||
*/
|
||||
get visibleToContacts() {
|
||||
if (!this.profile?.issuerDidVisibleToDids) return [];
|
||||
|
||||
return this.profile.issuerDidVisibleToDids
|
||||
.map((did) => contactForDid(did, this.allContacts))
|
||||
.filter((contact): contact is Contact => contact !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the profile has first location coordinates
|
||||
* @returns True if both latitude and longitude are available
|
||||
|
||||
Reference in New Issue
Block a user