Browse Source

add discovery of people's profiles, and update profile endpoints for latest server version

master
Trent Larson 2 weeks ago
parent
commit
f3f8aeefc3
  1. 6
      src/db/tables/settings.ts
  2. 10
      src/libs/endorserServer.ts
  3. 7
      src/libs/partnerServer.ts
  4. 63
      src/views/AccountViewView.vue
  5. 427
      src/views/DiscoverView.vue
  6. 4
      src/views/NewEditProjectView.vue

6
src/db/tables/settings.ts

@ -13,14 +13,14 @@ export type BoundingBox = {
*/ */
export type Settings = { export type Settings = {
// default entry is keyed with MASTER_SETTINGS_KEY; other entries are linked to an account with account ID // default entry is keyed with MASTER_SETTINGS_KEY; other entries are linked to an account with account ID
id?: number; // this is only blank on input, when the database assigns it id?: number; // this is erased for all those entries that are keyed with accountDid
// if supplied, this settings record overrides the master record when the user switches to this account // if supplied, this settings record overrides the master record when the user switches to this account
accountDid?: string; // not used in the MASTER_SETTINGS_KEY entry accountDid?: string; // not used in the MASTER_SETTINGS_KEY entry
// active Decentralized ID // active Decentralized ID
activeDid?: string; // only used in the MASTER_SETTINGS_KEY entry activeDid?: string; // only used in the MASTER_SETTINGS_KEY entry
apiServer?: string; // API server URL apiServer: string; // API server URL
filterFeedByNearby?: boolean; // filter by nearby filterFeedByNearby?: boolean; // filter by nearby
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden
@ -29,7 +29,7 @@ export type Settings = {
firstName?: string; // user's full name, may be null if unwanted for a particular account firstName?: string; // user's full name, may be null if unwanted for a particular account
hideRegisterPromptOnNewContact?: boolean; hideRegisterPromptOnNewContact?: boolean;
isRegistered?: boolean; isRegistered?: boolean;
imageServer?: string; // imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable
lastName?: string; // deprecated - put all names in firstName lastName?: string; // deprecated - put all names in firstName
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing

10
src/libs/endorserServer.ts

@ -191,13 +191,9 @@ export interface PlanVerifiableCredential extends GenericVerifiableCredential {
* Represents data about a project * Represents data about a project
* *
* @deprecated * @deprecated
* We should use PlanSummaryRecord instead. * We should use PlanSummaryRecord instead, adding rowid to it.
**/ **/
export interface PlanData { export interface PlanData {
/**
* Name of the project
**/
name: string;
/** /**
* Description of the project * Description of the project
**/ **/
@ -211,6 +207,10 @@ export interface PlanData {
* The DID of the issuer * The DID of the issuer
*/ */
issuerDid: string; issuerDid: string;
/**
* Name of the project
**/
name: string;
/** /**
* The identifier of the project -- different from jwtId, needs to be fixed * The identifier of the project -- different from jwtId, needs to be fixed
**/ **/

7
src/libs/partnerServer.ts

@ -0,0 +1,7 @@
export interface UserProfile {
description: string;
locLat?: number;
locLon?: number;
issuerDid: string;
rowid?: string;
}

63
src/views/AccountViewView.vue

@ -82,13 +82,12 @@
<div v-else class="text-center"> <div v-else class="text-center">
<div class @click="openImageDialog()"> <div class @click="openImageDialog()">
<fa <fa
icon="camera" icon="image-portrait"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-l" class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-l"
/> />
<fa <fa
icon="image-portrait" icon="camera"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-r" class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-r"
@click="openImageDialog()"
/> />
</div> </div>
</div> </div>
@ -250,16 +249,15 @@
<div <div
id="sectionSearchLocation" id="sectionSearchLocation"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8" class="flex justify-between bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
> >
<!-- label --> <!-- label -->
<div class="mb-2 font-bold">Location for Searches</div> <span class="mb-2 font-bold">Location for Searches</span>
<router-link <router-link
:to="{ name: 'search-area' }" :to="{ name: 'search-area' }"
class="block w-full text-center text-m bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-6" class="text-m bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2"
> >
Set Search Area {{ isSearchAreasSet ? "Change" : "Set" }} Search Area
<!-- If already set, change button label to "Change Search Area" -->
</router-link> </router-link>
</div> </div>
@ -285,10 +283,7 @@
:class="{ 'bg-slate-100': loadingProfile || savingProfile }" :class="{ 'bg-slate-100': loadingProfile || savingProfile }"
></textarea> ></textarea>
<div <div class="flex items-center mb-4" @click="toggleUserProfileLocation">
class="flex items-center mb-4"
@click="toggleUserProfileLocation"
>
<input <input
type="checkbox" type="checkbox"
class="mr-2" class="mr-2"
@ -680,7 +675,7 @@
<h2 class="text-slate-500 text-sm font-bold mb-2"> <h2 class="text-slate-500 text-sm font-bold mb-2">
Notification Push Server Notification Push Server
</h2> </h2>
<div id="sectionNotificationPushServer" class="px-3 py-4"> <div class="px-3 py-4">
<input <input
type="text" type="text"
class="block w-full rounded border border-slate-400 px-3 py-2" class="block w-full rounded border border-slate-400 px-3 py-2"
@ -757,7 +752,7 @@
{{ DEFAULT_PARTNER_API_SERVER }} {{ DEFAULT_PARTNER_API_SERVER }}
</span> </span>
<div id="sectionImageServerURL" class="mt-2"> <div class="mt-2">
<span class="text-slate-500 text-sm font-bold">Image Server URL</span> <span class="text-slate-500 text-sm font-bold">Image Server URL</span>
&nbsp; &nbsp;
<span class="text-sm">{{ DEFAULT_IMAGE_API_SERVER }}</span> <span class="text-sm">{{ DEFAULT_IMAGE_API_SERVER }}</span>
@ -930,6 +925,7 @@ import {
DIRECT_PUSH_TITLE, DIRECT_PUSH_TITLE,
retrieveAccountMetadata, retrieveAccountMetadata,
} from "@/libs/util"; } from "@/libs/util";
import { UserProfile } from "@/libs/partnerServer";
const inputImportFileNameRef = ref<Blob>(); const inputImportFileNameRef = ref<Blob>();
@ -964,9 +960,9 @@ export default class AccountViewView extends Vue {
givenName = ""; givenName = "";
hideRegisterPromptOnNewContact = false; hideRegisterPromptOnNewContact = false;
imageLimits: ImageRateLimits | null = null; imageLimits: ImageRateLimits | null = null;
imageServer = "";
includeUserProfileLocation = false; includeUserProfileLocation = false;
isRegistered = false; isRegistered = false;
isSearchAreasSet = false;
limitsMessage = ""; limitsMessage = "";
loadingLimits = false; loadingLimits = false;
loadingProfile = true; loadingProfile = true;
@ -975,8 +971,8 @@ export default class AccountViewView extends Vue {
notifyingReminder = false; notifyingReminder = false;
notifyingReminderMessage = ""; notifyingReminderMessage = "";
notifyingReminderTime = ""; notifyingReminderTime = "";
partnerApiServer = ""; partnerApiServer = DEFAULT_PARTNER_API_SERVER;
partnerApiServerInput = ""; partnerApiServerInput = DEFAULT_PARTNER_API_SERVER;
passkeyExpirationDescription = ""; passkeyExpirationDescription = "";
passkeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES; passkeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
previousPasskeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES; previousPasskeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
@ -997,8 +993,8 @@ export default class AccountViewView extends Vue {
subscription: PushSubscription | null = null; subscription: PushSubscription | null = null;
warnIfProdServer = false; warnIfProdServer = false;
warnIfTestServer = false; warnIfTestServer = false;
webPushServer = ""; webPushServer = DEFAULT_PUSH_SERVER;
webPushServerInput = ""; webPushServerInput = DEFAULT_PUSH_SERVER;
userProfileDesc = ""; userProfileDesc = "";
userProfileLatitude = 0; userProfileLatitude = 0;
userProfileLongitude = 0; userProfileLongitude = 0;
@ -1021,7 +1017,7 @@ export default class AccountViewView extends Vue {
try { try {
const headers = await getHeaders(this.activeDid); const headers = await getHeaders(this.activeDid);
const response = await this.axios.get( const response = await this.axios.get(
this.apiServer + "/api/partner/user-profile/" + this.activeDid, this.apiServer + "/api/partner/userProfile/" + this.activeDid,
{ headers }, { headers },
); );
if (response.status === 200) { if (response.status === 200) {
@ -1127,14 +1123,15 @@ export default class AccountViewView extends Vue {
this.hideRegisterPromptOnNewContact = this.hideRegisterPromptOnNewContact =
!!settings.hideRegisterPromptOnNewContact; !!settings.hideRegisterPromptOnNewContact;
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.imageServer = settings.imageServer || ""; this.isSearchAreasSet = !!settings.searchBoxes;
this.notifyingNewActivity = !!settings.notifyingNewActivityTime; this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || ""; this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
this.notifyingReminder = !!settings.notifyingReminderTime; this.notifyingReminder = !!settings.notifyingReminderTime;
this.notifyingReminderMessage = settings.notifyingReminderMessage || ""; this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
this.notifyingReminderTime = settings.notifyingReminderTime || ""; this.notifyingReminderTime = settings.notifyingReminderTime || "";
this.partnerApiServer = settings.partnerApiServer || ""; this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
this.partnerApiServerInput = settings.partnerApiServer || ""; this.partnerApiServerInput =
settings.partnerApiServer || this.partnerApiServerInput;
this.profileImageUrl = settings.profileImageUrl; this.profileImageUrl = settings.profileImageUrl;
this.showContactGives = !!settings.showContactGivesInline; this.showContactGives = !!settings.showContactGivesInline;
this.passkeyExpirationMinutes = this.passkeyExpirationMinutes =
@ -1144,8 +1141,8 @@ export default class AccountViewView extends Vue {
this.showShortcutBvc = !!settings.showShortcutBvc; this.showShortcutBvc = !!settings.showShortcutBvc;
this.warnIfProdServer = !!settings.warnIfProdServer; this.warnIfProdServer = !!settings.warnIfProdServer;
this.warnIfTestServer = !!settings.warnIfTestServer; this.warnIfTestServer = !!settings.warnIfTestServer;
this.webPushServer = settings.webPushServer || ""; this.webPushServer = settings.webPushServer || this.webPushServer;
this.webPushServerInput = settings.webPushServer || ""; this.webPushServerInput = settings.webPushServer || this.webPushServerInput;
} }
// call fn, copy text to the clipboard, then redo fn after 2 seconds // call fn, copy text to the clipboard, then redo fn after 2 seconds
@ -1793,15 +1790,25 @@ export default class AccountViewView extends Vue {
this.savingProfile = true; this.savingProfile = true;
try { try {
const headers = await getHeaders(this.activeDid); const headers = await getHeaders(this.activeDid);
const payload = { const payload: UserProfile = {
description: this.userProfileDesc, description: this.userProfileDesc,
}; };
if (this.userProfileLatitude && this.userProfileLongitude) { if (this.userProfileLatitude && this.userProfileLongitude) {
payload.locLat = this.userProfileLatitude; payload.locLat = this.userProfileLatitude;
payload.locLon = this.userProfileLongitude; payload.locLon = this.userProfileLongitude;
} else if (this.includeUserProfileLocation) {
this.$notify(
{
group: "alert",
type: "toast",
title: "",
text: "No profile location is saved.",
},
3000,
);
} }
const response = await this.axios.post( const response = await this.axios.post(
this.apiServer + "/api/partner/user-profile", this.apiServer + "/api/partner/userProfile",
payload, payload,
{ headers }, { headers },
); );
@ -1845,6 +1852,7 @@ export default class AccountViewView extends Vue {
if (!this.includeUserProfileLocation) { if (!this.includeUserProfileLocation) {
this.userProfileLatitude = 0; this.userProfileLatitude = 0;
this.userProfileLongitude = 0; this.userProfileLongitude = 0;
this.zoom = 2;
} }
} }
@ -1866,6 +1874,7 @@ export default class AccountViewView extends Vue {
eraseLatLong() { eraseLatLong() {
this.userProfileLatitude = 0; this.userProfileLatitude = 0;
this.userProfileLongitude = 0; this.userProfileLongitude = 0;
this.zoom = 2;
this.includeUserProfileLocation = false; this.includeUserProfileLocation = false;
} }
} }

427
src/views/DiscoverView.vue

@ -6,7 +6,7 @@
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light"> <h1 id="ViewHeading" class="text-4xl text-center font-light">
Discover Projects Discover Projects & People
</h1> </h1>
<OnboardingDialog ref="onboardingDialog" /> <OnboardingDialog ref="onboardingDialog" />
@ -15,7 +15,6 @@
<div <div
id="QuickSearch" id="QuickSearch"
class="mt-8 mb-4 flex" class="mt-8 mb-4 flex"
v-on:keyup.enter="searchSelected()"
:style="{ visibility: isSearchVisible ? 'visible' : 'hidden' }" :style="{ visibility: isSearchVisible ? 'visible' : 'hidden' }"
> >
<input <input
@ -23,16 +22,54 @@
v-model="searchTerms" v-model="searchTerms"
placeholder="Search…" placeholder="Search…"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2" class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
v-on:keyup.enter="searchSelected()"
/> />
<button <button
@click="searchSelected()"
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400" class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
@click="searchSelected()"
> >
<fa icon="magnifying-glass" class="fa-fw"></fa> <fa icon="magnifying-glass" class="fa-fw"></fa>
</button> </button>
</div> </div>
<!-- Result Tabs --> <!-- Result Tabs -->
<!-- Top Level Selection -->
<div class="text-center text-slate-500 border-b border-slate-300 mb-4">
<ul class="flex flex-wrap justify-center gap-4 -mb-px">
<li>
<a
href="#"
@click="
projects = [];
userProfiles = [];
isProjectsActive = true;
isPeopleActive = false;
searchSelected();
"
v-bind:class="computedProjectsTabStyleClassNames()"
>
Projects
</a>
</li>
<li>
<a
href="#"
@click="
projects = [];
userProfiles = [];
isProjectsActive = false;
isPeopleActive = true;
searchSelected();
"
v-bind:class="computedPeopleTabStyleClassNames()"
>
People
</a>
</li>
</ul>
</div>
<!-- Secondary Tabs -->
<div class="text-center text-slate-500 border-b border-slate-300"> <div class="text-center text-slate-500 border-b border-slate-300">
<ul class="flex flex-wrap justify-center gap-4 -mb-px"> <ul class="flex flex-wrap justify-center gap-4 -mb-px">
<li> <li>
@ -40,9 +77,10 @@
href="#" href="#"
@click=" @click="
projects = []; projects = [];
userProfiles = [];
isLocalActive = true; isLocalActive = true;
isMappedActive = false; isMappedActive = false;
isRemoteActive = false; isAnywhereActive = false;
isSearchVisible = true; isSearchVisible = true;
tempSearchBox = null; tempSearchBox = null;
searchLocal(); searchLocal();
@ -65,15 +103,17 @@
href="#" href="#"
@click=" @click="
projects = []; projects = [];
userProfiles = [];
isLocalActive = false; isLocalActive = false;
isMappedActive = true; isMappedActive = true;
isRemoteActive = false; isAnywhereActive = false;
isSearchVisible = false; isSearchVisible = false;
searchTerms = ''; searchTerms = '';
tempSearchBox = null; tempSearchBox = null;
" "
v-bind:class="computedMappedTabStyleClassNames()" v-bind:class="computedMappedTabStyleClassNames()"
> >
<!-- search is triggered when map component gets to "ready" state -->
Mapped Mapped
</a> </a>
</li> </li>
@ -82,9 +122,10 @@
href="#" href="#"
@click=" @click="
projects = []; projects = [];
userProfiles = [];
isLocalActive = false; isLocalActive = false;
isMappedActive = false; isMappedActive = false;
isRemoteActive = true; isAnywhereActive = true;
isSearchVisible = true; isSearchVisible = true;
tempSearchBox = null; tempSearchBox = null;
searchAll(); searchAll();
@ -95,7 +136,7 @@
<!-- restore when the links don't jump around for different numbers <!-- restore when the links don't jump around for different numbers
<span <span
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md" class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
v-if="isRemoteActive" v-if="isAnywhereActive"
> >
{{ remoteCount > -1 ? remoteCount : "?" }} {{ remoteCount > -1 ? remoteCount : "?" }}
</span> </span>
@ -143,13 +184,13 @@
> >
<fa icon="spinner" class="fa-spin-pulse"></fa> <fa icon="spinner" class="fa-spin-pulse"></fa>
</div> </div>
<div v-else-if="projects.length === 0" class="text-center mt-8"> <div v-else-if="projects.length === 0 && userProfiles.length === 0" class="text-center mt-8">
<p class="text-lg text-slate-500"> <p class="text-lg text-slate-500">
<span v-if="isLocalActive"> <span v-if="isLocalActive">
<span v-if="searchBox"> None found in the selected area. </span> <span v-if="searchBox"> None found in the selected area. </span>
<!-- Otherwise there's no search area selected so we'll just leave the search box for them to click. --> <!-- Otherwise there's no search area selected so we'll just leave the search box for them to click. -->
</span> </span>
<span v-else-if="isRemoteActive" <span v-else-if="isAnywhereActive"
>No projects were found with that search.</span >No projects were found with that search.</span
> >
</p> </p>
@ -158,35 +199,68 @@
<!-- Results List --> <!-- Results List -->
<InfiniteScroll @reached-bottom="loadMoreData"> <InfiniteScroll @reached-bottom="loadMoreData">
<ul id="listDiscoverResults"> <ul id="listDiscoverResults">
<li <!-- Projects List -->
class="border-b border-slate-300" <template v-if="isProjectsActive">
v-for="project in projects" <li
:key="project.handleId" class="border-b border-slate-300"
> v-for="project in projects"
<a :key="project.handleId"
@click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4 cursor-pointer"
> >
<div> <a
<ProjectIcon @click="onClickLoadItem(project.handleId)"
:entityId="project.handleId" class="block py-4 flex gap-4 cursor-pointer"
:iconSize="48" >
:imageUrl="project.image" <div>
class="block border border-slate-300 rounded-md max-h-12 max-w-12" <ProjectIcon
/> :entityId="project.handleId"
</div> :iconSize="48"
:imageUrl="project.image"
<div class="grow"> class="block border border-slate-300 rounded-md max-h-12 max-w-12"
<h2 class="text-base font-semibold">{{ project.name }}</h2> />
<div class="text-sm">
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{
didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
}}
</div> </div>
</div>
</a> <div class="grow">
</li> <h2 class="text-base font-semibold">{{ project.name }}</h2>
<div class="text-sm">
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{
didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
}}
</div>
</div>
</a>
</li>
</template>
<!-- Profiles List -->
<template v-else>
<li
class="border-b border-slate-300"
v-for="profile in userProfiles"
:key="profile.issuerDid"
>
<a
@click="onClickLoadItem(profile.issuerDid)"
class="block py-4 flex gap-4 cursor-pointer"
>
<div class="grow">
<div class="text-sm">
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{
didInfo(profile.issuerDid, activeDid, allMyDids, allContacts)
}}
</div>
<p v-if="profile.description" class="mt-1 text-sm text-slate-600">
{{ profile.description }}
</p>
<div v-if="profile.locLat && profile.locLon" class="mt-1 text-xs text-slate-500">
<fa icon="location-dot" class="fa-fw"></fa>
{{ profile.locLat.toFixed(2) }}, {{ profile.locLon.toFixed(2) }}
</div>
</div>
</a>
</li>
</template>
</ul> </ul>
</InfiniteScroll> </InfiniteScroll>
</section> </section>
@ -197,14 +271,14 @@ import "leaflet/dist/leaflet.css";
import * as L from "leaflet"; import * as L from "leaflet";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { LMap, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { LMap, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { Router } from "vue-router"; import { Router, RouteLocationNormalizedLoaded } from "vue-router";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import ProjectIcon from "@/components/ProjectIcon.vue"; import ProjectIcon from "@/components/ProjectIcon.vue";
import OnboardingDialog from "@/components/OnboardingDialog.vue"; import OnboardingDialog from "@/components/OnboardingDialog.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app"; import { DEFAULT_PARTNER_API_SERVER, NotificationIface } from "@/constants/app";
import { import {
db, db,
logConsoleAndDb, logConsoleAndDb,
@ -218,8 +292,19 @@ import {
getHeaders, getHeaders,
PlanData, PlanData,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { UserProfile } from "@/libs/partnerServer";
import { OnboardPage, retrieveAccountDids } from "@/libs/util"; import { OnboardPage, retrieveAccountDids } from "@/libs/util";
interface Tile {
indexLat: number;
indexLon: number;
minFoundLat: number;
maxFoundLat: number;
minFoundLon: number;
maxFoundLon: number;
recordCount: number;
}
@Component({ @Component({
components: { components: {
InfiniteScroll, InfiniteScroll,
@ -233,25 +318,31 @@ import { OnboardPage, retrieveAccountDids } from "@/libs/util";
}) })
export default class DiscoverView extends Vue { export default class DiscoverView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
$route!: RouteLocationNormalizedLoaded;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
searchTerms = "";
projects: PlanData[] = [];
isLoading = false; isLoading = false;
isLocalActive = true; isLocalActive = true;
isMappedActive = false; isMappedActive = false;
isRemoteActive = false; isAnywhereActive = false;
isProjectsActive = true;
isPeopleActive = false;
isSearchVisible = true; isSearchVisible = true;
localCenterLat = 0; localCenterLat = 0;
localCenterLong = 0; localCenterLong = 0;
localCount = -1; localCount = -1;
markers: { [key: string]: L.Marker } = {}; markers: { [key: string]: L.Marker } = {};
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
projects: PlanData[] = [];
remoteCount = -1; remoteCount = -1;
searchBox: { name: string; bbox: BoundingBox } | null = null; searchBox: { name: string; bbox: BoundingBox } | null = null;
searchTerms = "";
tempSearchBox: BoundingBox | null = null; tempSearchBox: BoundingBox | null = null;
userProfiles: UserProfile[] = [];
zoomedSoDoNotMove = false; zoomedSoDoNotMove = false;
// make this function available to the Vue template // make this function available to the Vue template
@ -261,13 +352,15 @@ export default class DiscoverView extends Vue {
const settings = await retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
this.activeDid = (settings.activeDid as string) || ""; this.activeDid = (settings.activeDid as string) || "";
this.apiServer = (settings.apiServer as string) || ""; this.apiServer = (settings.apiServer as string) || "";
this.partnerApiServer =
(settings.partnerApiServer as string) || this.partnerApiServer;
this.searchBox = settings.searchBoxes?.[0] || null; this.searchBox = settings.searchBoxes?.[0] || null;
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.allMyDids = await retrieveAccountDids(); this.allMyDids = await retrieveAccountDids();
this.searchTerms = (this.$route as Router).query["searchText"] || ""; this.searchTerms = this.$route.query["searchText"]?.toString() || "";
if (!settings.finishedOnboarding) { if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open( (this.$refs.onboardingDialog as OnboardingDialog).open(
@ -284,7 +377,7 @@ export default class DiscoverView extends Vue {
} else { } else {
this.isLocalActive = false; this.isLocalActive = false;
this.isMappedActive = false; this.isMappedActive = false;
this.isRemoteActive = true; this.isAnywhereActive = true;
await this.searchAll(); await this.searchAll();
} }
} }
@ -298,8 +391,8 @@ export default class DiscoverView extends Vue {
if (this.isLocalActive) { if (this.isLocalActive) {
await this.searchLocal(); await this.searchLocal();
} else if (this.isMappedActive) { } else if (this.isMappedActive) {
this.isRemoteActive = true; const mapRef = this.$refs.projectMap as L.Map;
await this.searchAll(); this.requestTiles(mapRef.leafletObject); // not ideal because I found this from experimentation, not documentation
} else { } else {
await this.searchAll(); await this.searchAll();
} }
@ -311,6 +404,7 @@ export default class DiscoverView extends Vue {
if (!beforeId) { if (!beforeId) {
// this was an initial search so clear any previous results // this was an initial search so clear any previous results
this.projects = []; this.projects = [];
this.userProfiles = [];
} }
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms); let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
@ -319,62 +413,62 @@ export default class DiscoverView extends Vue {
queryParams = queryParams + `&beforeId=${beforeId}`; queryParams = queryParams + `&beforeId=${beforeId}`;
} }
const endpoint = this.isProjectsActive
? this.apiServer + "/api/v2/report/plans"
: this.partnerApiServer + "/api/partner/userProfile";
try { try {
this.isLoading = true; this.isLoading = true;
const response = await fetch( const response = await fetch(endpoint + "?" + queryParams, {
this.apiServer + "/api/v2/report/plans?" + queryParams, method: "GET",
{ headers: await getHeaders(this.activeDid),
method: "GET", });
headers: await getHeaders(this.activeDid),
},
);
if (response.status !== 200) { if (response.status !== 200) {
const details = await response.text(); const details = await response.text();
console.error("Problem with full search:", details);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `There was a problem accessing the server.`,
},
3000,
);
throw details; throw details;
} }
const results = await response.json(); const results = await response.json();
const plans: PlanData[] = results.data; if (this.isProjectsActive) {
if (plans) { const plans: PlanData[] = results.data;
for (const plan of plans) { if (plans) {
const { name, description, handleId, image, issuerDid, rowid } = plan; for (const plan of plans) {
this.projects.push({ const { name, description, handleId, image, issuerDid, rowid } = plan;
name, this.projects.push({
description, name,
handleId, description,
image, handleId,
issuerDid, image,
rowid, issuerDid,
}); rowid,
});
}
this.remoteCount = this.projects.length;
} else {
throw JSON.stringify(results);
} }
this.remoteCount = this.projects.length;
} else { } else {
throw JSON.stringify(results); const profiles: UserProfile[] = results.data;
if (profiles) {
this.userProfiles.push(...profiles);
this.remoteCount = this.userProfiles.length;
} else {
throw JSON.stringify(results);
}
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) { } catch (e: any) {
console.error("Error with feed load:", e); console.error("Error with search all:", e);
// this sometimes gives different information // this sometimes gives different information
console.error("Error with feed load (error added): " + e); console.error("Error with search all (error added): " + e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error Searching",
text: e.userMessage || "There was a problem retrieving projects.", text: e.userMessage || "There was a problem retrieving " + (this.isProjectsActive ? "projects" : "profiles") + ".",
}, },
5000, 5000,
); );
@ -392,12 +486,14 @@ export default class DiscoverView extends Vue {
if (!searchBox) { if (!searchBox) {
this.projects = []; this.projects = [];
this.userProfiles = [];
return; return;
} }
if (!beforeId) { if (!beforeId) {
// this was an initial search so clear any previous results // this was an initial search so clear any previous results
this.projects = []; this.projects = [];
this.userProfiles = [];
} }
const claimContents = const claimContents =
@ -407,70 +503,71 @@ export default class DiscoverView extends Vue {
claimContents, claimContents,
"minLocLat=" + searchBox.minLat, "minLocLat=" + searchBox.minLat,
"maxLocLat=" + searchBox.maxLat, "maxLocLat=" + searchBox.maxLat,
"westLocLon=" + searchBox.westLong, "minLocLon=" + searchBox.westLong,
"eastLocLon=" + searchBox.eastLong, "maxLocLon=" + searchBox.eastLong,
].join("&"); ].join("&");
if (beforeId) { if (beforeId) {
queryParams = queryParams + `&beforeId=${beforeId}`; queryParams = queryParams + `&beforeId=${beforeId}`;
} }
const endpoint = this.isProjectsActive
? this.apiServer + "/api/v2/report/plansByLocation"
: this.partnerApiServer + "/api/partner/userProfile";
try { try {
this.isLoading = true; this.isLoading = true;
const response = await fetch( const response = await fetch(endpoint + "?" + queryParams, {
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams, method: "GET",
{ headers: await getHeaders(this.activeDid),
method: "GET", });
headers: await getHeaders(this.activeDid),
},
);
if (response.status !== 200) { if (response.status !== 200) {
const details = await response.text(); const details = await response.text();
console.error("Problem with nearby search:", details); throw details;
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem accessing the server.",
},
3000,
);
throw await response.text();
} }
const results = await response.json(); const results = await response.json();
if (results.data) { if (this.isProjectsActive) {
if (beforeId) { if (results.data) {
const plans: PlanData[] = results.data; if (beforeId) {
for (const plan of plans) { const plans: PlanData[] = results.data;
const { name, description, handleId, issuerDid, rowid } = plan; for (const plan of plans) {
this.projects.push({ const { name, description, handleId, issuerDid, rowid } = plan;
name, this.projects.push({
description, name,
handleId, description,
issuerDid, handleId,
rowid, issuerDid,
}); rowid,
});
}
} else {
this.projects = results.data;
} }
this.localCount = this.projects.length;
} else { } else {
this.projects = results.data; throw JSON.stringify(results);
} }
this.localCount = this.projects.length;
} else { } else {
throw JSON.stringify(results); const profiles: UserProfile[] = results.data;
if (profiles) {
this.userProfiles.push(...profiles);
this.localCount = this.userProfiles.length;
} else {
throw JSON.stringify(results);
}
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) { } catch (e: any) {
console.error("Error with feed load:", e); console.error("Error with search local:", e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: e.userMessage || "There was a problem retrieving projects.", text: e.userMessage || "There was a problem retrieving " + (this.isProjectsActive ? "projects" : "profiles") + ".",
}, },
5000, 5000,
); );
@ -484,14 +581,21 @@ export default class DiscoverView extends Vue {
* @param payload is the flag from the InfiniteScroll indicating if it should load * @param payload is the flag from the InfiniteScroll indicating if it should load
**/ **/
async loadMoreData(payload: boolean) { async loadMoreData(payload: boolean) {
if (this.projects.length > 0 && payload) { if (payload) {
const latestProject = this.projects[this.projects.length - 1]; if (this.isProjectsActive && this.projects.length > 0) {
if (this.isLocalActive) { const latestProject = this.projects[this.projects.length - 1];
this.searchLocal(latestProject["rowid"]); if (this.isLocalActive || this.isMappedActive) {
} else if (this.isMappedActive) { this.searchLocal(latestProject.rowid);
this.searchLocal(latestProject["rowid"]); } else if (this.isAnywhereActive) {
} else if (this.isRemoteActive) { this.searchAll(latestProject.rowid);
this.searchAll(latestProject["rowid"]); }
} else if (!this.isProjectsActive && this.userProfiles.length > 0) {
const latestProfile = this.userProfiles[this.userProfiles.length - 1];
if (this.isLocalActive || this.isMappedActive) {
this.searchLocal(latestProfile.rowid || "");
} else if (this.isAnywhereActive) {
this.searchAll(latestProfile.rowid || "");
}
} }
} }
} }
@ -503,7 +607,7 @@ export default class DiscoverView extends Vue {
} }
// Tried but failed to use other vue-leaflet methods update:zoom and update:bounds // Tried but failed to use other vue-leaflet methods update:zoom and update:bounds
// To access the from this.$refs, use this.$refs.projectMap.mapObject // To access the from this.$refs, use this.$refs.projectMap.leafletObject (or maybe mapObject)
onMoveStart(/* event: L.LocationEvent */) { onMoveStart(/* event: L.LocationEvent */) {
// don't remove markers because they follow the map when moving (and the experience is jarring) // don't remove markers because they follow the map when moving (and the experience is jarring)
@ -540,9 +644,10 @@ export default class DiscoverView extends Vue {
"westLocLon=" + bounds?.getSouthWest().lng, "westLocLon=" + bounds?.getSouthWest().lng,
"eastLocLon=" + bounds?.getNorthEast().lng, "eastLocLon=" + bounds?.getNorthEast().lng,
].join("&"); ].join("&");
const response = await fetch( const endpoint = this.isProjectsActive
this.apiServer + "/api/v2/report/planCountsByBBox?" + queryParams, ? this.apiServer + "/api/v2/report/planCountsByBBox"
); : this.partnerApiServer + "/api/partner/userProfileCountsByBBox";
const response = await fetch(endpoint + "?" + queryParams);
if (response.status === 200) { if (response.status === 200) {
Object.values(this.markers).forEach((marker) => marker.remove()); Object.values(this.markers).forEach((marker) => marker.remove());
this.markers = {}; this.markers = {};
@ -601,14 +706,16 @@ export default class DiscoverView extends Vue {
} }
/** /**
* Handle clicking on a project entry found in the list * Handle clicking on a project or profile entry found in the list
* @param id of the project * @param id of the project or profile
**/ **/
onClickLoadProject(id: string) { onClickLoadItem(id: string) {
const route = { const route = {
path: "/project/" + encodeURIComponent(id), path: this.isProjectsActive
? "/project/" + encodeURIComponent(id)
: "/userProfile/" + encodeURIComponent(id),
}; };
(this.$router as Router).push(route); this.$router.push(route);
} }
public computedLocalTabStyleClassNames() { public computedLocalTabStyleClassNames() {
@ -654,14 +761,50 @@ export default class DiscoverView extends Vue {
"rounded-t-lg": true, "rounded-t-lg": true,
"border-b-2": true, "border-b-2": true,
active: this.isRemoteActive, active: this.isAnywhereActive,
"text-black": this.isRemoteActive, "text-black": this.isAnywhereActive,
"border-black": this.isRemoteActive, "border-black": this.isAnywhereActive,
"font-semibold": this.isRemoteActive, "font-semibold": this.isAnywhereActive,
"text-blue-600": !this.isAnywhereActive,
"border-transparent": !this.isAnywhereActive,
"hover:border-slate-400": !this.isAnywhereActive,
};
}
public computedProjectsTabStyleClassNames() {
return {
"inline-block": true,
"py-3": true,
"rounded-t-lg": true,
"border-b-2": true,
active: this.isProjectsActive,
"text-black": this.isProjectsActive,
"border-black": this.isProjectsActive,
"font-semibold": this.isProjectsActive,
"text-blue-600": !this.isProjectsActive,
"border-transparent": !this.isProjectsActive,
"hover:border-slate-400": !this.isProjectsActive,
};
}
public computedPeopleTabStyleClassNames() {
return {
"inline-block": true,
"py-3": true,
"rounded-t-lg": true,
"border-b-2": true,
active: this.isPeopleActive,
"text-black": this.isPeopleActive,
"border-black": this.isPeopleActive,
"font-semibold": this.isPeopleActive,
"text-blue-600": !this.isRemoteActive, "text-blue-600": !this.isPeopleActive,
"border-transparent": !this.isRemoteActive, "border-transparent": !this.isPeopleActive,
"hover:border-slate-400": !this.isRemoteActive, "hover:border-slate-400": !this.isPeopleActive,
}; };
} }
} }

4
src/views/NewEditProjectView.vue

@ -77,7 +77,9 @@
maxlength="5000" maxlength="5000"
></textarea> ></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4"> <div class="text-xs text-slate-500 italic -mt-3 mb-4">
If you want to be contacted, be sure to include your contact information. If you want to be contacted, be sure to include your contact information
-- just remember that this information is public and saved in a public
history.
</div> </div>
<div class="text-xs text-slate-500 italic -mt-3 mb-4"> <div class="text-xs text-slate-500 italic -mt-3 mb-4">
{{ fullClaim.description?.length }}/5000 max. characters {{ fullClaim.description?.length }}/5000 max. characters

Loading…
Cancel
Save