From dca8d48feb9bf0066c26b0088740f074d5a905db Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 3 Nov 2025 08:10:15 -0700 Subject: [PATCH] feat: add a link to connect to someone who is closer to any user profile target --- src/libs/partnerServer.ts | 1 + src/views/AccountViewView.vue | 2 +- src/views/UserProfileView.vue | 244 +++++++++++++++++++++++++++++++++- 3 files changed, 245 insertions(+), 2 deletions(-) diff --git a/src/libs/partnerServer.ts b/src/libs/partnerServer.ts index 1b63e490..0fe95ed5 100644 --- a/src/libs/partnerServer.ts +++ b/src/libs/partnerServer.ts @@ -5,5 +5,6 @@ export interface UserProfile { locLat2?: number; locLon2?: number; issuerDid: string; + issuerDidVisibleToDids?: Array; rowId?: string; // set on profile retrieved from server } diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 9b7efd3e..fe0eadb6 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -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/${did}`, + `${this.partnerApiServer}/api/partner/userProfile`, { headers }, ); diff --git a/src/views/UserProfileView.vue b/src/views/UserProfileView.vue index 5b7abdf8..0666c2af 100644 --- a/src/views/UserProfileView.vue +++ b/src/views/UserProfileView.vue @@ -54,6 +54,161 @@

+ +
+
+

+ + Contact Information +

+

+ {{ issuerContact.name || "Contact" }} is in your contacts. +

+
+

Contact Methods:

+
+ {{ method.label }}: + {{ method.value }} + ({{ method.type }}) +
+
+
+ +
+

+ + Not in Contacts +

+

+ This person has been connected but they're not in your contacts on + this device. +

+ + + +
+

How to Connect:

+ + +
+

+ 1. Copy a deep link to this profile and send it to someone in + your network: +

+ +
+ + +
+

+ 2. Send the link to one of these people: +

+ + +
+
+ People who can see this profile: +
+
+
+ + {{ + contact.name || "Unnamed Contact" + }} + ({{ contact.did.substring(0, 20) }}...) +
+
+
+ + +
+
+ Other nearby people: + Nearby people: +
+
+
+ + {{ + neighborDisplayName(neighbor) + }} + ({{ neighbor.issuerDid.substring(0, 20) }}...) +
+
+
+ + +
+ + Loading nearby people... +
+
+
+
+
+

Location

@@ -113,7 +268,12 @@ import { APP_SERVER, } from "../constants/app"; import { Contact } from "../db/tables/contacts"; -import { didInfo, getHeaders } from "../libs/endorserServer"; +import { + didInfo, + getHeaders, + isHiddenDid, + contactForDid, +} from "../libs/endorserServer"; import { UserProfile } from "../libs/partnerServer"; import { retrieveAccountDids } from "../libs/util"; import { logger } from "../utils/logger"; @@ -162,9 +322,13 @@ export default class UserProfileView extends Vue { isLoading = true; partnerApiServer = DEFAULT_PARTNER_API_SERVER; profile: UserProfile | null = null; + showConnectInstructions = false; + nearestNeighbors: Array = []; + loadingNearestNeighbors = false; // make this function available to the Vue template didInfo = didInfo; + isHiddenDid = isHiddenDid; /** Production share domain for deep links */ APP_SERVER = APP_SERVER; @@ -266,6 +430,63 @@ 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 */ @@ -283,6 +504,27 @@ 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