Browse Source

feat: add a link to connect to someone who is closer to any user profile target

pull/217/head
Trent Larson 6 days ago
parent
commit
dca8d48feb
  1. 1
      src/libs/partnerServer.ts
  2. 2
      src/views/AccountViewView.vue
  3. 244
      src/views/UserProfileView.vue

1
src/libs/partnerServer.ts

@ -5,5 +5,6 @@ export interface UserProfile {
locLat2?: number; locLat2?: number;
locLon2?: number; locLon2?: number;
issuerDid: string; issuerDid: string;
issuerDidVisibleToDids?: Array<string>;
rowId?: string; // set on profile retrieved from server rowId?: string; // set on profile retrieved from server
} }

2
src/views/AccountViewView.vue

@ -2171,7 +2171,7 @@ export default class AccountViewView extends Vue {
const headers = await getHeaders(did); const headers = await getHeaders(did);
const response = await this.axios.delete( const response = await this.axios.delete(
`${this.partnerApiServer}/api/partner/userProfile/${did}`, `${this.partnerApiServer}/api/partner/userProfile`,
{ headers }, { headers },
); );

244
src/views/UserProfileView.vue

@ -54,6 +54,161 @@
</p> </p>
</div> </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 --> <!-- Map for first coordinates -->
<div v-if="hasFirstLocation" class="mt-4"> <div v-if="hasFirstLocation" class="mt-4">
<h2 class="text-lg font-semibold">Location</h2> <h2 class="text-lg font-semibold">Location</h2>
@ -113,7 +268,12 @@ import {
APP_SERVER, APP_SERVER,
} from "../constants/app"; } from "../constants/app";
import { Contact } from "../db/tables/contacts"; 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 { UserProfile } from "../libs/partnerServer";
import { retrieveAccountDids } from "../libs/util"; import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
@ -162,9 +322,13 @@ export default class UserProfileView extends Vue {
isLoading = true; isLoading = true;
partnerApiServer = DEFAULT_PARTNER_API_SERVER; partnerApiServer = DEFAULT_PARTNER_API_SERVER;
profile: UserProfile | null = null; profile: UserProfile | null = null;
showConnectInstructions = false;
nearestNeighbors: Array<UserProfile> = [];
loadingNearestNeighbors = false;
// make this function available to the Vue template // make this function available to the Vue template
didInfo = didInfo; didInfo = didInfo;
isHiddenDid = isHiddenDid;
/** Production share domain for deep links */ /** Production share domain for deep links */
APP_SERVER = APP_SERVER; 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 * 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 * Checks if the profile has first location coordinates
* @returns True if both latitude and longitude are available * @returns True if both latitude and longitude are available

Loading…
Cancel
Save