add instructions to connect to any user profile (#224)
See https://app.clickup.com/t/86b76734v Reviewed-on: #224 Co-authored-by: Trent Larson <trent@trentlarson.com> Co-committed-by: Trent Larson <trent@trentlarson.com>
This commit was merged in pull request #224.
This commit is contained in:
@@ -54,6 +54,108 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Nearest Neighbors Section -->
|
||||
<div
|
||||
v-if="
|
||||
profile.issuerDid !== activeDid && // only show neighbors if they're not current user
|
||||
profile.issuerDid !== neighbors?.[0]?.did // and they're not directly connected (since there's no in-between)
|
||||
"
|
||||
class="mt-6"
|
||||
>
|
||||
<h2 class="text-lg font-semibold mb-3">Network Connections</h2>
|
||||
|
||||
<div v-if="loadingNeighbors">
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<font-awesome
|
||||
icon="spinner"
|
||||
class="fa-spin-pulse text-2xl text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="neighborsError"
|
||||
class="bg-red-50 border border-red-300 rounded-md p-4"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<font-awesome
|
||||
icon="exclamation-triangle"
|
||||
class="text-red-500 mt-0.5"
|
||||
/>
|
||||
<p class="text-red-700">{{ neighborsError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="neighbor in neighbors"
|
||||
:key="neighbor.did"
|
||||
class="bg-slate-50 border border-slate-300 rounded-md"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3 p-3">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<button
|
||||
title="Copy profile link and expand"
|
||||
class="text-blue-600 flex-shrink-0"
|
||||
@click="onNeighborExpandClick(neighbor.did)"
|
||||
>
|
||||
<font-awesome
|
||||
:icon="
|
||||
expandedNeighborDid === neighbor.did
|
||||
? 'chevron-down'
|
||||
: 'chevron-right'
|
||||
"
|
||||
class="text-sm"
|
||||
/>
|
||||
{{ getNeighborDisplayName(neighbor.did) }}
|
||||
</button>
|
||||
<span :class="getRelationBadgeClass(neighbor.relation)">
|
||||
{{ getRelationLabel(neighbor.relation) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expandedNeighborDid === neighbor.did"
|
||||
class="border-t border-slate-300 p-3 bg-white"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'did', params: { did: neighbor.did } }"
|
||||
class="text-blue-600 hover:text-blue-800 font-medium underline"
|
||||
>
|
||||
Go to contact info
|
||||
</router-link>
|
||||
and send them the link in your clipboard and ask for an
|
||||
introduction to this person.
|
||||
<div
|
||||
v-if="
|
||||
getNeighborDisplayName(neighbor.did) === '' ||
|
||||
neighborIsNotInContacts(neighbor.did)
|
||||
"
|
||||
class="flex flex-col gap-1 mt-2"
|
||||
>
|
||||
<p class="text-xs text-slate-600">
|
||||
This person is connected to you, but they are not in this
|
||||
device's contacts. Copy this DID link and check on another
|
||||
device or check with different people.
|
||||
</p>
|
||||
<span class="flex items-center gap-1 min-w-0">
|
||||
<span class="text-xs truncate text-slate-600 min-w-0">
|
||||
{{ neighbor.did }}
|
||||
</span>
|
||||
<button
|
||||
title="Copy DID Link"
|
||||
class="text-blue-600 hover:text-blue-800 underline cursor-pointer flex-shrink-0"
|
||||
@click.prevent="onCopyDidClick(neighbor.did)"
|
||||
>
|
||||
<font-awesome icon="copy" class="text-sm" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map for first coordinates -->
|
||||
<div v-if="hasFirstLocation" class="mt-4">
|
||||
<h2 class="text-lg font-semibold">Location</h2>
|
||||
@@ -159,7 +261,11 @@ export default class UserProfileView extends Vue {
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
allMyDids: Array<string> = [];
|
||||
expandedNeighborDid: string | null = null;
|
||||
isLoading = true;
|
||||
loadingNeighbors = false;
|
||||
neighbors: Array<{ did: string; relation: string }> = [];
|
||||
neighborsError = "";
|
||||
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
||||
profile: UserProfile | null = null;
|
||||
|
||||
@@ -183,8 +289,8 @@ export default class UserProfileView extends Vue {
|
||||
*/
|
||||
async mounted() {
|
||||
await this.initializeSettings();
|
||||
await this.loadContacts();
|
||||
await this.loadProfile();
|
||||
await this.loadNeighbors();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,12 +305,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 +350,100 @@ 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.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -330,5 +508,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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user