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