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