|
|
|
|
@@ -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
|
|
|
|
|
|