From eded4a7df33f1c0918714c0afde43409268a9934 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 16 Nov 2025 19:18:36 -0700 Subject: [PATCH 1/2] feat: add instructions to connect to any profile --- src/constants/contacts.ts | 29 ++++ src/views/ContactEditView.vue | 21 +-- src/views/DIDView.vue | 35 +++++ src/views/UserProfileView.vue | 246 ++++++++++++++++++++++++++++++++-- 4 files changed, 306 insertions(+), 25 deletions(-) create mode 100644 src/constants/contacts.ts diff --git a/src/constants/contacts.ts b/src/constants/contacts.ts new file mode 100644 index 00000000..3273de91 --- /dev/null +++ b/src/constants/contacts.ts @@ -0,0 +1,29 @@ +/** + * Constants for contact-related functionality + * Created: 2025-11-16 + */ + +/** + * Contact method types with user-friendly labels + * Used in: ContactEditView.vue, DIDView.vue + */ +export const CONTACT_METHOD_TYPES = [ + { value: "CELL", label: "Mobile" }, + { value: "EMAIL", label: "Email" }, + { value: "WHATSAPP", label: "WhatsApp" }, +] as const; + +/** + * Type for contact method type values + */ +export type ContactMethodType = (typeof CONTACT_METHOD_TYPES)[number]["value"]; + +/** + * Helper function to get label for a contact method type + * @param type - The contact method type value (e.g., "CELL", "EMAIL") + * @returns The user-friendly label or the original type if not found + */ +export function getContactMethodLabel(type: string): string { + const methodType = CONTACT_METHOD_TYPES.find((m) => m.value === type); + return methodType ? methodType.label : type; +} diff --git a/src/views/ContactEditView.vue b/src/views/ContactEditView.vue index a3ec73ce..8ddd27ea 100644 --- a/src/views/ContactEditView.vue +++ b/src/views/ContactEditView.vue @@ -85,22 +85,12 @@ class="absolute bg-white border border-gray-300 rounded-md mt-1" >
- CELL -
-
- EMAIL -
-
- WHATSAPP + {{ methodType.label }}
@@ -157,6 +147,7 @@ import { } from "../constants/notifications"; import { Contact, ContactMethod } from "../db/tables/contacts"; import { AppString } from "../constants/app"; +import { CONTACT_METHOD_TYPES } from "../constants/contacts"; /** * Contact Edit View Component @@ -224,6 +215,8 @@ export default class ContactEditView extends Vue { /** App string constants */ AppString = AppString; + /** Contact method types for dropdown */ + contactMethodTypes = CONTACT_METHOD_TYPES; /** * Component lifecycle hook that initializes the contact edit form diff --git a/src/views/DIDView.vue b/src/views/DIDView.vue index 473ee95e..1515c881 100644 --- a/src/views/DIDView.vue +++ b/src/views/DIDView.vue @@ -42,6 +42,39 @@ + + +
+

+ {{ contactFromDid.notes }} +

+
+ + +
+
+
+ {{ getContactMethodLabel(method.type) }}: + {{ method.label }} + {{ method.value }} + + + +
+
+
+ + + + + + {{ getRelationLabel(neighbor.relation) }} + + + + + +

Location

@@ -160,8 +275,11 @@ export default class UserProfileView extends Vue { allContacts: Array = []; allMyDids: Array = []; isLoading = true; + loadingNeighbors = false; + neighborsError = ""; partnerApiServer = DEFAULT_PARTNER_API_SERVER; profile: UserProfile | null = null; + neighbors: Array<{ did: string; relation: string }> = []; // make this function available to the Vue template didInfo = didInfo; @@ -183,8 +301,8 @@ export default class UserProfileView extends Vue { */ async mounted() { await this.initializeSettings(); - await this.loadContacts(); await this.loadProfile(); + await this.loadNeighbors(); } /** @@ -199,12 +317,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 +362,75 @@ 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."); + } + } + /** * Computed properties for template logic streamlining */ @@ -330,5 +495,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`; + } + } } -- 2.49.1 From ab4d22af590ecf4100a1f5a5850a5231c347521c Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 19 Nov 2025 11:53:29 -0700 Subject: [PATCH 2/2] feat: simplify the user-profile-sharing prompts (where you contact via a current user) --- src/views/UserProfileView.vue | 117 +++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 52 deletions(-) diff --git a/src/views/UserProfileView.vue b/src/views/UserProfileView.vue index abcb08dd..86775c49 100644 --- a/src/views/UserProfileView.vue +++ b/src/views/UserProfileView.vue @@ -57,8 +57,8 @@
@@ -85,62 +85,52 @@
-
-

- The following - {{ neighbors.length === 1 ? "user is" : "users are" }} - closer to the person who owns this profile. -

-
-
- 1 -

- - Click to copy this profile reference - - to your clipboard -

-
-
- 2 -

- Contact a user listed below and share the reference to request - an introduction -

-
-
-
- +
+ + + {{ getRelationLabel(neighbor.relation) }} + +
+
+
-

- {{ getNeighborDisplayName(neighbor.did) }} -

+ + 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 @@ -160,10 +150,7 @@

- - - {{ getRelationLabel(neighbor.relation) }} - +
@@ -274,12 +261,13 @@ 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; - neighbors: Array<{ did: string; relation: string }> = []; // make this function available to the Vue template didInfo = didInfo; @@ -431,6 +419,31 @@ export default class UserProfileView extends Vue { } } + /** + * 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 */ -- 2.49.1