feat: add a link to connect to someone who is closer to any user profile target
This commit is contained in:
@@ -5,5 +5,6 @@ export interface UserProfile {
|
||||
locLat2?: number;
|
||||
locLon2?: number;
|
||||
issuerDid: string;
|
||||
issuerDidVisibleToDids?: Array<string>;
|
||||
rowId?: string; // set on profile retrieved from server
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
|
||||
|
||||
@@ -54,6 +54,161 @@
|
||||
</p>
|
||||
</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 -->
|
||||
<div v-if="hasFirstLocation" class="mt-4">
|
||||
<h2 class="text-lg font-semibold">Location</h2>
|
||||
@@ -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<UserProfile> = [];
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user