+
+
+
Network Connections
+
+
+
+
+
+
{{ neighborsError }}
+
+
+
+
+
+
+
+
+
+ {{ getRelationLabel(neighbor.relation) }}
+
+
+
+
+
+ Go to contact info
+
+ and send them the link in your clipboard and ask for an
+ introduction to this person.
+
+
+ This person is connected to you, but they are not in this
+ device's contacts. Copy this DID link and check on another
+ device or check with different people.
+
+
+
+ {{ neighbor.did }}
+
+
+
+
+
+
+
+
+
+
Location
@@ -159,7 +261,11 @@ export default class UserProfileView extends Vue {
activeDid = "";
allContacts: Array = [];
allMyDids: Array = [];
+ expandedNeighborDid: string | null = null;
isLoading = true;
+ loadingNeighbors = false;
+ neighbors: Array<{ did: string; relation: string }> = [];
+ neighborsError = "";
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
profile: UserProfile | null = null;
@@ -183,8 +289,8 @@ export default class UserProfileView extends Vue {
*/
async mounted() {
await this.initializeSettings();
- await this.loadContacts();
await this.loadProfile();
+ await this.loadNeighbors();
}
/**
@@ -199,12 +305,7 @@ export default class UserProfileView extends Vue {
this.activeDid = activeIdentity.activeDid || "";
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
- }
- /**
- * Loads all contacts from database
- */
- private async loadContacts() {
this.allContacts = await this.$getAllContacts();
this.allMyDids = await retrieveAccountDids();
}
@@ -249,23 +350,100 @@ export default class UserProfileView extends Vue {
}
/**
- * Copies profile link to clipboard
+ * Loads nearest neighbors from partner API
*
- * Creates a deep link to the profile and copies it to the clipboard
- * Shows success notification when completed
+ * Fetches network connections for the profile and displays them
+ * with appropriate relation labels
+ */
+ async loadNeighbors() {
+ const profileId: string = this.$route.params.id as string;
+ if (!profileId) {
+ return;
+ }
+
+ this.loadingNeighbors = true;
+ this.neighborsError = "";
+ try {
+ const response = await fetch(
+ `${this.partnerApiServer}/api/partner/userProfileNearestNeighbors/${encodeURIComponent(profileId)}`,
+ {
+ method: "GET",
+ headers: await getHeaders(this.activeDid),
+ },
+ );
+
+ if (response.status === 200) {
+ const result = await response.json();
+ this.neighbors = result.data;
+ this.neighborsError = "";
+ } else {
+ logger.warn("Failed to load neighbors:", response.status);
+ this.neighbors = [];
+ this.neighborsError = "Failed to load network connections.";
+ }
+ } catch (error) {
+ logger.error("Error loading neighbors:", error);
+ this.neighbors = [];
+ this.neighborsError =
+ "An error occurred while loading network connections.";
+ } finally {
+ this.loadingNeighbors = false;
+ }
+ }
+
+ /**
+ * Copies a deep link to the profile to the clipboard
*/
async onCopyLinkClick() {
- // Use production URL for sharing to avoid localhost issues in development
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
try {
await copyToClipboard(deepLink);
- this.notify.copied("profile link", TIMEOUTS.STANDARD);
+ this.notify.copied("Profile link", TIMEOUTS.STANDARD);
} catch (error) {
this.$logAndConsole(`Error copying profile link: ${error}`, true);
this.notify.error("Failed to copy profile link.");
}
}
+ /**
+ * Copies a deep link to the provided DID to the clipboard
+ */
+ async onCopyDidClick(did: string) {
+ const deepLink = `${APP_SERVER}/deep-link/did/${encodeURIComponent(did)}`;
+ try {
+ await copyToClipboard(deepLink);
+ this.notify.copied("DID link", TIMEOUTS.STANDARD);
+ } catch (error) {
+ this.$logAndConsole(`Error copying DID link: ${error}`, true);
+ this.notify.error("Failed to copy DID link.");
+ }
+ }
+
+ /**
+ * Handles clicking the expand button next to a neighbor's name
+ * Copies the profile link to clipboard and toggles the expanded section
+ */
+ async onNeighborExpandClick(did: string) {
+ if (this.expandedNeighborDid === did) {
+ this.expandedNeighborDid = null;
+ // don't copy the link
+ return;
+ }
+
+ // Copy the profile link
+ const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
+ try {
+ await copyToClipboard(deepLink);
+ this.notify.copied("Profile link", TIMEOUTS.STANDARD);
+ } catch (error) {
+ this.$logAndConsole(`Error copying profile link: ${error}`, true);
+ this.notify.error("Failed to copy profile link.");
+ }
+
+ // Toggle the expanded section
+ this.expandedNeighborDid = did;
+ }
+
/**
* Computed properties for template logic streamlining
*/
@@ -330,5 +508,64 @@ export default class UserProfileView extends Vue {
get tileLayerUrl() {
return "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
}
+
+ /**
+ * Gets display name for a neighbor's DID
+ * Uses didInfo utility to show contact name if available, otherwise DID
+ * @param did - The DID to get display name for
+ * @returns Formatted display name
+ */
+ getNeighborDisplayName(did: string): string {
+ return this.didInfo(did, this.activeDid, this.allMyDids, this.allContacts);
+ }
+
+ neighborIsNotInContacts(did: string) {
+ return !this.allContacts.some((contact) => contact.did === did);
+ }
+
+ noNeighborsAreInContacts() {
+ return this.neighbors.every(
+ (neighbor) =>
+ !this.allContacts.some((contact) => contact.did === neighbor.did),
+ );
+ }
+
+ /**
+ * Gets human-readable label for relation type
+ * @param relation - The relation type from API
+ * @returns Display label for the relation
+ */
+ getRelationLabel(relation: string): string {
+ switch (relation) {
+ case "REGISTERED_BY_YOU":
+ return "Registered by You";
+ case "REGISTERED_YOU":
+ return "Registered You";
+ case "TARGET":
+ return "Yourself";
+ default:
+ return relation;
+ }
+ }
+
+ /**
+ * Gets CSS classes for relation badge styling
+ * @param relation - The relation type from API
+ * @returns CSS class string for badge
+ */
+ getRelationBadgeClass(relation: string): string {
+ const baseClasses =
+ "text-xs font-semibold px-2 py-1 rounded whitespace-nowrap";
+ switch (relation) {
+ case "REGISTERED_BY_YOU":
+ return `${baseClasses} bg-blue-100 text-blue-700`;
+ case "REGISTERED_YOU":
+ return `${baseClasses} bg-green-100 text-green-700`;
+ case "TARGET":
+ return `${baseClasses} bg-purple-100 text-purple-700`;
+ default:
+ return `${baseClasses} bg-slate-100 text-slate-700`;
+ }
+ }
}