Merge changes
This commit is contained in:
@@ -3,4 +3,4 @@ VITE_APP_SERVER=https://timesafari.app
|
|||||||
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H
|
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H
|
||||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.timesafari.app
|
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||||
|
|||||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
|
## [0.3.53] - 2025.01.30
|
||||||
|
### Added
|
||||||
|
- Hints for contacting the creator of a project
|
||||||
|
|
||||||
|
|
||||||
|
## [0.3.52] - 2025.01.22
|
||||||
|
### Fixed
|
||||||
|
- User profile endpoint server for map was broken.
|
||||||
|
|
||||||
|
|
||||||
|
## [0.3.51] - 2025.01.22
|
||||||
|
### Fixed
|
||||||
|
- User profile map jumped on first zoom.
|
||||||
|
|
||||||
|
|
||||||
|
## [0.3.50] - 2025.01.20 - b9fedcd3fd3e34c3fb0fc79150d1a81a76eaeb40
|
||||||
|
### Added
|
||||||
|
- User public profiles
|
||||||
|
|
||||||
|
|
||||||
## [0.3.49] - 2025.01.09 - 36301ed238ff84df25bb11a8d44a295ee7eaf0f8
|
## [0.3.49] - 2025.01.09 - 36301ed238ff84df25bb11a8d44a295ee7eaf0f8
|
||||||
### Changed
|
### Changed
|
||||||
- Make all external contact links direct to the contact-import page.
|
- Make all external contact links direct to the contact-import page.
|
||||||
|
|||||||
37
README.md
37
README.md
@@ -50,35 +50,34 @@ Look below for the "test-all" instructions.
|
|||||||
|
|
||||||
* Put the commit hash in the changelog (which will help you remember to bump the version later).
|
* Put the commit hash in the changelog (which will help you remember to bump the version later).
|
||||||
|
|
||||||
* Record what version is currently on production in docs.
|
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.36` && `git push origin 0.3.36`.
|
||||||
|
|
||||||
* Get on the server and run the correct build:
|
* For test, build the app (because test server is not yet set up to build):
|
||||||
|
|
||||||
* `cd crowd-funder-for-time-pwa && git pull`
|
|
||||||
|
|
||||||
* Previous command: `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
|
|
||||||
|
|
||||||
* Staging
|
|
||||||
|
|
||||||
(Let's replace this with a .env.development or .env.staging file.)
|
|
||||||
|
|
||||||
The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
|
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
* Production
|
... and transfer to the test server: `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
|
||||||
```
|
|
||||||
# This picks up values from .env.production
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
* Back up the time-safari/dist folder.
|
(Let's replace that with a .env.development or .env.staging file.)
|
||||||
|
|
||||||
|
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.)
|
||||||
|
|
||||||
|
* For prod, get on the server and run the correct build:
|
||||||
|
|
||||||
|
... and log onto the server:
|
||||||
|
|
||||||
|
* `pkgx +npm sh`
|
||||||
|
|
||||||
|
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.36 && npm install && npm run build && cd -`
|
||||||
|
|
||||||
|
(The plain `npm run build` uses the .env.production file.)
|
||||||
|
|
||||||
|
* Back up the time-safari/dist folder, then `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/`
|
||||||
|
|
||||||
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
||||||
|
|
||||||
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.36` && `git push origin 0.3.36`.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1925
package-lock.json
generated
1925
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -1,18 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari",
|
||||||
"version": "0.3.51-beta",
|
"version": "0.3.54-beta",
|
||||||
"description": "A cross-platform app for managing time-based crowdfunding.",
|
|
||||||
"author": "Your Name <your.email@example.com>",
|
|
||||||
"main": "src/electron/main.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build",
|
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build",
|
||||||
"build:capacitor": "vite build --mode capacitor",
|
|
||||||
"build:electron": "vite build --mode electron",
|
|
||||||
"electron:dev": "vite build --mode electron && electron .",
|
|
||||||
"electron:build": "electron-builder",
|
|
||||||
"capacitor:sync": "npx cap copy",
|
|
||||||
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
||||||
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
||||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js",
|
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js",
|
||||||
@@ -101,11 +93,11 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vue/eslint-config-typescript": "^11.0.0",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"electron": "^33.2.1",
|
"electron": "^33.2.1",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"eslint": "^8.0.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-vue": "^9.32.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
@@ -114,8 +106,8 @@
|
|||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
"vite": "^6.0.7",
|
"vite": "^5.2.0",
|
||||||
"vite-plugin-pwa": "^0.21.1"
|
"vite-plugin-pwa": "^0.19.8"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.example.app",
|
"appId": "com.example.app",
|
||||||
|
|||||||
182
src/components/HiddenDidDialog.vue
Normal file
182
src/components/HiddenDidDialog.vue
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||||
|
>
|
||||||
|
<div class="bg-white rounded-lg p-6 max-w-2xl w-full mx-4">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-xl font-bold capitalize">{{ roleName }} Details</h2>
|
||||||
|
<button @click="close" class="text-gray-500 hover:text-gray-700">
|
||||||
|
<fa icon="times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<!-- This is somewhat similar to ClaimView.vue and ConfirmGiftView.vue -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<p class="mb-4">
|
||||||
|
<span v-if="R.isEmpty(visibleToDids)">
|
||||||
|
The {{ roleName }} is not visible to you or any of your contacts.
|
||||||
|
</span>
|
||||||
|
<span v-else> The {{ roleName }} is not visible to you. </span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="R.isEmpty(visibleToDids)">
|
||||||
|
<p class="mt-2">
|
||||||
|
You can ask one of your contacts to take a look and see if their
|
||||||
|
contacts can see more details. Someone is connected to people closer
|
||||||
|
to them; if you don't know who to ask, try the person who registered
|
||||||
|
you.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<p class="mb-2">
|
||||||
|
They are visible to some of your contacts. If you'd like an
|
||||||
|
introduction, ask them if they'll tell you more.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="ml-4">
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
v-for="(visDid, idx) of visibleToDids"
|
||||||
|
:key="idx"
|
||||||
|
class="list-disc ml-4 mb-2"
|
||||||
|
>
|
||||||
|
<div class="text-sm">
|
||||||
|
<span>
|
||||||
|
{{ didInfo(visDid) }}
|
||||||
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||||
|
<a
|
||||||
|
:href="`/did/${visDid}`"
|
||||||
|
target="_blank"
|
||||||
|
class="text-blue-500"
|
||||||
|
>
|
||||||
|
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<span v-if="canShare">
|
||||||
|
If you'd like an introduction,
|
||||||
|
<a @click="onClickShareClaim()" class="text-blue-500"
|
||||||
|
>click here to share the information with them and ask if they'll
|
||||||
|
tell you more about the {{ roleName }}.</a
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
If you'd like an introduction,
|
||||||
|
<a
|
||||||
|
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||||
|
class="text-blue-500"
|
||||||
|
>click here to copy this page, paste it into a message, and ask if
|
||||||
|
they'll tell you more about the {{ roleName }}.</a
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
@click="close"
|
||||||
|
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import * as R from "ramda";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
import * as serverUtil from "@/libs/endorserServer";
|
||||||
|
import { NotificationIface } from "@/constants/app";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class HiddenDidDialog extends Vue {
|
||||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
isOpen = false;
|
||||||
|
roleName = "";
|
||||||
|
visibleToDids: string[] = [];
|
||||||
|
allContacts: Array<Contact> = [];
|
||||||
|
activeDid = "";
|
||||||
|
allMyDids: Array<string> = [];
|
||||||
|
canShare = false;
|
||||||
|
windowLocation = window.location.href;
|
||||||
|
|
||||||
|
R = R;
|
||||||
|
serverUtil = serverUtil;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
// When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare
|
||||||
|
// then use this truer check: navigator.canShare && navigator.canShare()
|
||||||
|
this.canShare = !!navigator.share;
|
||||||
|
}
|
||||||
|
|
||||||
|
open(
|
||||||
|
roleName: string,
|
||||||
|
visibleToDids: string[],
|
||||||
|
allContacts: Array<Contact>,
|
||||||
|
activeDid: string,
|
||||||
|
allMyDids: Array<string>,
|
||||||
|
) {
|
||||||
|
this.roleName = roleName;
|
||||||
|
this.visibleToDids = visibleToDids;
|
||||||
|
this.allContacts = allContacts;
|
||||||
|
this.activeDid = activeDid;
|
||||||
|
this.allMyDids = allMyDids;
|
||||||
|
this.isOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
didInfo(did: string) {
|
||||||
|
return serverUtil.didInfo(
|
||||||
|
did,
|
||||||
|
this.activeDid,
|
||||||
|
this.allMyDids,
|
||||||
|
this.allContacts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyToClipboard(name: string, text: string) {
|
||||||
|
useClipboard()
|
||||||
|
.copy(text)
|
||||||
|
.then(() => {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
title: "Copied",
|
||||||
|
text: (name || "That") + " was copied to the clipboard.",
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickShareClaim() {
|
||||||
|
this.copyToClipboard("A link to this page", this.windowLocation);
|
||||||
|
window.navigator.share({
|
||||||
|
title: "Help Connect Me",
|
||||||
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||||
|
url: this.windowLocation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<h1 class="text-xl font-bold text-center mb-4 relative">
|
<h1 class="text-xl font-bold text-center mb-4 relative">
|
||||||
Welcome to Time Safari
|
Welcome to Time Safari
|
||||||
<br />
|
<br />
|
||||||
- Showcasing Gratitude & Magnifing Time
|
- Showcasing Gratitude & Magnifying Time
|
||||||
<div
|
<div
|
||||||
class="text-lg text-center leading-none absolute right-0 -top-1"
|
class="text-lg text-center leading-none absolute right-0 -top-1"
|
||||||
@click="onClickClose(true)"
|
@click="onClickClose(true)"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
* (Maybe we should use PlanSummaryRecord instead, either by adding rowId or by iterating with jwtId.)
|
||||||
**/
|
**/
|
||||||
export interface PlanData {
|
export interface PlanData {
|
||||||
/**
|
|
||||||
* Name of the project
|
|
||||||
**/
|
|
||||||
name: string;
|
|
||||||
/**
|
/**
|
||||||
* Description of the project
|
* Description of the project
|
||||||
**/
|
**/
|
||||||
@@ -212,9 +208,14 @@ export interface PlanData {
|
|||||||
*/
|
*/
|
||||||
issuerDid: string;
|
issuerDid: string;
|
||||||
/**
|
/**
|
||||||
* The identifier of the project -- different from jwtId, needs to be fixed
|
* Name of the project
|
||||||
**/
|
**/
|
||||||
rowid?: string;
|
name: string;
|
||||||
|
/**
|
||||||
|
* The identifier of the project record -- different from jwtId
|
||||||
|
* (Maybe we should use the jwtId to iterate through the records instead.)
|
||||||
|
**/
|
||||||
|
rowId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EndorserRateLimits {
|
export interface EndorserRateLimits {
|
||||||
@@ -459,7 +460,7 @@ export function didInfoForContact(
|
|||||||
} else if (contact) {
|
} else if (contact) {
|
||||||
return {
|
return {
|
||||||
displayName: contact.name || "Contact With No Name",
|
displayName: contact.name || "Contact With No Name",
|
||||||
known: !!contact,
|
known: true,
|
||||||
profileImageUrl: contact.profileImageUrl,
|
profileImageUrl: contact.profileImageUrl,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -477,6 +478,19 @@ export function didInfoForContact(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns full contact info object (never undefined), where did is searched in contacts and allMyDids
|
||||||
|
*/
|
||||||
|
export function didInfoObject(
|
||||||
|
did: string | undefined,
|
||||||
|
activeDid: string | undefined,
|
||||||
|
allMyDids: string[],
|
||||||
|
contacts: Contact[],
|
||||||
|
): { known: boolean; displayName: string; profileImageUrl?: string } {
|
||||||
|
const contact = contactForDid(did, contacts);
|
||||||
|
return didInfoForContact(did, activeDid, contact, allMyDids);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
always returns text, maybe something like "unnamed" or "unknown"
|
always returns text, maybe something like "unnamed" or "unknown"
|
||||||
|
|
||||||
|
|||||||
9
src/libs/partnerServer.ts
Normal file
9
src/libs/partnerServer.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface UserProfile {
|
||||||
|
description: string;
|
||||||
|
locLat?: number;
|
||||||
|
locLon?: number;
|
||||||
|
locLat2?: number;
|
||||||
|
locLon2?: number;
|
||||||
|
issuerDid: string;
|
||||||
|
rowId?: string; // set on profile retrieved from server
|
||||||
|
}
|
||||||
@@ -262,6 +262,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "test",
|
name: "test",
|
||||||
component: () => import("../views/TestView.vue"),
|
component: () => import("../views/TestView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/userProfile/:id?",
|
||||||
|
name: "userProfile",
|
||||||
|
component: () => import("../views/UserProfileView.vue"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const isElectron = window.location.protocol === "file:"; // Check if running in Electron
|
const isElectron = window.location.protocol === "file:"; // Check if running in Electron
|
||||||
@@ -292,9 +297,7 @@ const errorHandler = (
|
|||||||
) => {
|
) => {
|
||||||
// Handle the error here
|
// Handle the error here
|
||||||
console.error("Caught in top level error handler:", error, to, from);
|
console.error("Caught in top level error handler:", error, to, from);
|
||||||
alert(
|
alert("Something is very wrong. Try reloading or restarting the app.");
|
||||||
"Something is very wrong. We'd love if you contacted us and let us know how you got here. Thank you!",
|
|
||||||
);
|
|
||||||
|
|
||||||
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -159,7 +158,7 @@
|
|||||||
We'll just pop the message in only if we discover that they need it.
|
We'll just pop the message in only if we discover that they need it.
|
||||||
-->
|
-->
|
||||||
<div
|
<div
|
||||||
v-if="!loadingLimits && !endorserLimits?.nextWeekBeginDateTime"
|
v-if="!isRegistered"
|
||||||
id="noticeBeforeAnnounce"
|
id="noticeBeforeAnnounce"
|
||||||
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
|
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
|
||||||
>
|
>
|
||||||
@@ -176,6 +175,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
v-if="isRegistered"
|
||||||
id="sectionNotifications"
|
id="sectionNotifications"
|
||||||
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
||||||
>
|
>
|
||||||
@@ -250,19 +250,111 @@
|
|||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
|
<!-- User Profile -->
|
||||||
|
<div
|
||||||
|
v-if="isRegistered"
|
||||||
|
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
||||||
|
>
|
||||||
|
<div v-if="loadingProfile" class="text-center mb-2">
|
||||||
|
<fa icon="spinner" class="fa-spin text-slate-400"></fa> Loading
|
||||||
|
profile...
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center mb-2">
|
||||||
|
<span class="font-bold">Public Profile</span>
|
||||||
|
<fa
|
||||||
|
icon="circle-info"
|
||||||
|
class="text-slate-400 fa-fw ml-2 cursor-pointer"
|
||||||
|
@click="showProfileInfo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
v-model="userProfileDesc"
|
||||||
|
class="w-full h-32 p-2 border border-slate-300 rounded-md"
|
||||||
|
placeholder="Write something about yourself for the public..."
|
||||||
|
:readonly="loadingProfile || savingProfile"
|
||||||
|
:class="{ 'bg-slate-100': loadingProfile || savingProfile }"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-4" @click="toggleUserProfileLocation">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="mr-2"
|
||||||
|
v-model="includeUserProfileLocation"
|
||||||
|
/>
|
||||||
|
<label for="includeUserProfileLocation">Include Location</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
|
||||||
|
<p class="text-sm mb-2 text-slate-500">
|
||||||
|
For your security, choose a location nearby but not exactly at your
|
||||||
|
place.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<l-map
|
||||||
|
ref="profileMap"
|
||||||
|
class="!z-40 rounded-md"
|
||||||
|
@click="
|
||||||
|
(event: LeafletMouseEvent) => {
|
||||||
|
userProfileLatitude = event.latlng.lat;
|
||||||
|
userProfileLongitude = event.latlng.lng;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@ready="onMapReady"
|
||||||
|
>
|
||||||
|
<l-tile-layer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
layer-type="base"
|
||||||
|
name="OpenStreetMap"
|
||||||
|
/>
|
||||||
|
<l-marker
|
||||||
|
v-if="userProfileLatitude && userProfileLongitude"
|
||||||
|
:lat-lng="[userProfileLatitude, userProfileLongitude]"
|
||||||
|
@click="confirmEraseLatLong()"
|
||||||
|
/>
|
||||||
|
</l-map>
|
||||||
|
</div>
|
||||||
|
<div v-if="!loadingProfile && !savingProfile">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<button
|
||||||
|
@click="saveProfile"
|
||||||
|
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
||||||
|
:disabled="loadingProfile || savingProfile"
|
||||||
|
:class="{
|
||||||
|
'opacity-50 cursor-not-allowed': loadingProfile || savingProfile,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Save Profile
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="confirmDeleteProfile"
|
||||||
|
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
||||||
|
:disabled="loadingProfile || savingProfile"
|
||||||
|
:class="{
|
||||||
|
'opacity-50 cursor-not-allowed':
|
||||||
|
loadingProfile ||
|
||||||
|
savingProfile ||
|
||||||
|
(!userProfileDesc && !includeUserProfileLocation),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Delete Profile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="loadingProfile">Loading...</div>
|
||||||
|
<div v-else>Saving...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="activeDid"
|
v-if="activeDid"
|
||||||
id="sectionUsageLimits"
|
id="sectionUsageLimits"
|
||||||
@@ -599,7 +691,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"
|
||||||
@@ -676,7 +768,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>
|
||||||
|
|
||||||
<span class="text-sm">{{ DEFAULT_IMAGE_API_SERVER }}</span>
|
<span class="text-sm">{{ DEFAULT_IMAGE_API_SERVER }}</span>
|
||||||
@@ -791,17 +883,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { Buffer } from "buffer/";
|
import { Buffer } from "buffer/";
|
||||||
import Dexie from "dexie";
|
import Dexie from "dexie";
|
||||||
import "dexie-export-import";
|
import "dexie-export-import";
|
||||||
import { ImportProgress } from "dexie-export-import/dist/import";
|
import { ImportProgress } from "dexie-export-import/dist/import";
|
||||||
|
import { LeafletMouseEvent } from "leaflet";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
|
|
||||||
import EntityIcon from "../components/EntityIcon.vue";
|
import EntityIcon from "../components/EntityIcon.vue";
|
||||||
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
|
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
|
||||||
@@ -819,6 +915,7 @@ import {
|
|||||||
} from "../constants/app";
|
} from "../constants/app";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
|
logConsoleAndDb,
|
||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount,
|
||||||
updateAccountSettings,
|
updateAccountSettings,
|
||||||
} from "../db/index";
|
} from "../db/index";
|
||||||
@@ -830,8 +927,9 @@ import {
|
|||||||
} from "../db/tables/settings";
|
} from "../db/tables/settings";
|
||||||
import {
|
import {
|
||||||
clearPasskeyToken,
|
clearPasskeyToken,
|
||||||
ErrorResponse,
|
|
||||||
EndorserRateLimits,
|
EndorserRateLimits,
|
||||||
|
ErrorResponse,
|
||||||
|
errorStringForLog,
|
||||||
fetchEndorserRateLimits,
|
fetchEndorserRateLimits,
|
||||||
fetchImageRateLimits,
|
fetchImageRateLimits,
|
||||||
getHeaders,
|
getHeaders,
|
||||||
@@ -850,6 +948,10 @@ const inputImportFileNameRef = ref<Blob>();
|
|||||||
components: {
|
components: {
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
ImageMethodDialog,
|
ImageMethodDialog,
|
||||||
|
LeafletMouseEvent,
|
||||||
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LTileLayer,
|
||||||
PushNotificationPermission,
|
PushNotificationPermission,
|
||||||
QuickNav,
|
QuickNav,
|
||||||
TopMessage,
|
TopMessage,
|
||||||
@@ -873,23 +975,26 @@ export default class AccountViewView extends Vue {
|
|||||||
givenName = "";
|
givenName = "";
|
||||||
hideRegisterPromptOnNewContact = false;
|
hideRegisterPromptOnNewContact = false;
|
||||||
imageLimits: ImageRateLimits | null = null;
|
imageLimits: ImageRateLimits | null = null;
|
||||||
imageServer = "";
|
includeUserProfileLocation = false;
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
|
isSearchAreasSet = false;
|
||||||
limitsMessage = "";
|
limitsMessage = "";
|
||||||
loadingLimits = false;
|
loadingLimits = false;
|
||||||
|
loadingProfile = true;
|
||||||
notifyingNewActivity = false;
|
notifyingNewActivity = false;
|
||||||
notifyingNewActivityTime = "";
|
notifyingNewActivityTime = "";
|
||||||
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;
|
||||||
profileImageUrl?: string;
|
profileImageUrl?: string;
|
||||||
publicHex = "";
|
publicHex = "";
|
||||||
publicBase64 = "";
|
publicBase64 = "";
|
||||||
|
savingProfile = false;
|
||||||
showAdvanced = false;
|
showAdvanced = false;
|
||||||
showB64Copy = false;
|
showB64Copy = false;
|
||||||
showContactGives = false;
|
showContactGives = false;
|
||||||
@@ -903,8 +1008,12 @@ 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 = "";
|
||||||
|
userProfileLatitude = 0;
|
||||||
|
userProfileLongitude = 0;
|
||||||
|
zoom = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Async function executed when the component is mounted.
|
* Async function executed when the component is mounted.
|
||||||
@@ -918,6 +1027,49 @@ export default class AccountViewView extends Vue {
|
|||||||
// Initialize component state with values from the database or defaults
|
// Initialize component state with values from the database or defaults
|
||||||
await this.initializeState();
|
await this.initializeState();
|
||||||
await this.processIdentity();
|
await this.processIdentity();
|
||||||
|
|
||||||
|
// Load the user profile
|
||||||
|
if (this.isRegistered) {
|
||||||
|
try {
|
||||||
|
const headers = await getHeaders(this.activeDid);
|
||||||
|
const response = await this.axios.get(
|
||||||
|
this.apiServer +
|
||||||
|
"/api/partner/userProfileForIssuer/" +
|
||||||
|
this.activeDid,
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.userProfileDesc = response.data.data.description || "";
|
||||||
|
this.userProfileLatitude = response.data.data.locLat || 0;
|
||||||
|
this.userProfileLongitude = response.data.data.locLon || 0;
|
||||||
|
if (this.userProfileLatitude && this.userProfileLongitude) {
|
||||||
|
this.includeUserProfileLocation = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// won't get here because axios throws an error instead
|
||||||
|
throw Error("Unable to load profile.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
// this is ok: the profile is not yet created
|
||||||
|
} else {
|
||||||
|
logConsoleAndDb(
|
||||||
|
"Error loading profile: " + errorStringForLog(error),
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Loading Profile",
|
||||||
|
text: "Your server profile is not available.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loadingProfile = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// this can happen when running automated tests in dev mode because notifications don't work
|
// this can happen when running automated tests in dev mode because notifications don't work
|
||||||
console.error(
|
console.error(
|
||||||
@@ -992,14 +1144,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 =
|
||||||
@@ -1009,8 +1162,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
|
||||||
@@ -1498,13 +1651,19 @@ export default class AccountViewView extends Vue {
|
|||||||
*/
|
*/
|
||||||
private handleRateLimitsError(error: unknown) {
|
private handleRateLimitsError(error: unknown) {
|
||||||
if (error instanceof AxiosError) {
|
if (error instanceof AxiosError) {
|
||||||
|
if (error.status == 400 || error.status == 404) {
|
||||||
|
// no worries: they probably just aren't registered and don't have any limits
|
||||||
|
console.log(
|
||||||
|
"Got 400 or 404 response retrieving limits which probably means they're not registered:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
this.limitsMessage = "No limits were found, so no actions are allowed.";
|
||||||
|
} else {
|
||||||
const data = error.response?.data as ErrorResponse;
|
const data = error.response?.data as ErrorResponse;
|
||||||
this.limitsMessage =
|
this.limitsMessage =
|
||||||
(data?.error?.message as string) || "Bad server response.";
|
(data?.error?.message as string) || "Bad server response.";
|
||||||
console.error(
|
console.error("Got bad response retrieving limits:", error);
|
||||||
"Got bad response retrieving limits, which usually means user isn't registered.",
|
}
|
||||||
error,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
this.limitsMessage = "Got an error retrieving limits.";
|
this.limitsMessage = "Got an error retrieving limits.";
|
||||||
console.error("Got some error retrieving limits:", error);
|
console.error("Got some error retrieving limits:", error);
|
||||||
@@ -1635,5 +1794,174 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMapReady(map: L.Map) {
|
||||||
|
// doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
|
||||||
|
const zoom = this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
|
||||||
|
map.setView([this.userProfileLatitude, this.userProfileLongitude], zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
showProfileInfo() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "info",
|
||||||
|
title: "Public Profile Information",
|
||||||
|
text: "This data will be published for all to see, so be careful what your write. Your ID will only be shared with people who you allow to see your activity.",
|
||||||
|
},
|
||||||
|
7000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveProfile() {
|
||||||
|
this.savingProfile = true;
|
||||||
|
try {
|
||||||
|
const headers = await getHeaders(this.activeDid);
|
||||||
|
const payload: UserProfile = {
|
||||||
|
description: this.userProfileDesc,
|
||||||
|
};
|
||||||
|
if (this.userProfileLatitude && this.userProfileLongitude) {
|
||||||
|
payload.locLat = this.userProfileLatitude;
|
||||||
|
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(
|
||||||
|
this.apiServer + "/api/partner/userProfile",
|
||||||
|
payload,
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
if (response.status === 201) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Profile Saved",
|
||||||
|
text: "Your profile has been updated successfully.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// won't get here because axios throws an error on non-success
|
||||||
|
throw Error("Profile not saved");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleAndDb("Error saving profile: " + errorStringForLog(error));
|
||||||
|
const errorMessage: string =
|
||||||
|
error.response?.data?.error?.message ||
|
||||||
|
error.response?.data?.error ||
|
||||||
|
error.message ||
|
||||||
|
"There was an error saving your profile.";
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Saving Profile",
|
||||||
|
text: errorMessage,
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.savingProfile = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleUserProfileLocation() {
|
||||||
|
this.includeUserProfileLocation = !this.includeUserProfileLocation;
|
||||||
|
if (!this.includeUserProfileLocation) {
|
||||||
|
this.userProfileLatitude = 0;
|
||||||
|
this.userProfileLongitude = 0;
|
||||||
|
this.zoom = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmEraseLatLong() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "modal",
|
||||||
|
type: "confirm",
|
||||||
|
title: "Erase Marker",
|
||||||
|
text: "Are you sure you don't want to mark a location? This will erase the current location.",
|
||||||
|
onYes: async () => {
|
||||||
|
this.eraseLatLong();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
eraseLatLong() {
|
||||||
|
this.userProfileLatitude = 0;
|
||||||
|
this.userProfileLongitude = 0;
|
||||||
|
this.zoom = 2;
|
||||||
|
this.includeUserProfileLocation = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmDeleteProfile() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "modal",
|
||||||
|
type: "confirm",
|
||||||
|
title: "Delete Profile",
|
||||||
|
text: "Are you sure you want to delete your public profile? This will remove your description and location from the server, and it cannot be undone.",
|
||||||
|
onYes: this.deleteProfile,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteProfile() {
|
||||||
|
this.savingProfile = true;
|
||||||
|
try {
|
||||||
|
const headers = await getHeaders(this.activeDid);
|
||||||
|
const response = await this.axios.delete(
|
||||||
|
this.apiServer + "/api/partner/userProfile",
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
if (response.status === 204) {
|
||||||
|
this.userProfileDesc = "";
|
||||||
|
this.userProfileLatitude = 0;
|
||||||
|
this.userProfileLongitude = 0;
|
||||||
|
this.includeUserProfileLocation = false;
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Profile Deleted",
|
||||||
|
text: "Your profile has been deleted successfully.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw Error("Profile not deleted");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleAndDb("Error deleting profile: " + errorStringForLog(error));
|
||||||
|
const errorMessage: string =
|
||||||
|
error.response?.data?.error?.message ||
|
||||||
|
error.response?.data?.error ||
|
||||||
|
error.message ||
|
||||||
|
"There was an error deleting your profile.";
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Deleting Profile",
|
||||||
|
text: errorMessage,
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.savingProfile = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -148,8 +148,36 @@ export default class ClaimCertificateView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// alternatively, show some offer details
|
||||||
|
if (claimData.claimType === "Offer") {
|
||||||
|
const presentedText = "To";
|
||||||
|
ctx.font = "14px Arial";
|
||||||
|
const presentedWidth = ctx.measureText(presentedText).width;
|
||||||
|
ctx.fillText(
|
||||||
|
presentedText,
|
||||||
|
(CANVAS_WIDTH - presentedWidth) / 2, // Center horizontally
|
||||||
|
CANVAS_HEIGHT * 0.37,
|
||||||
|
);
|
||||||
|
// fulfills
|
||||||
|
const agentDid =
|
||||||
|
claimData.claim.agent.identifier || claimData.claim.agent;
|
||||||
|
const agentText = serverUtil.didInfoForCertificate(
|
||||||
|
agentDid,
|
||||||
|
allContacts,
|
||||||
|
);
|
||||||
|
ctx.font = "bold 20px Arial";
|
||||||
|
const agentWidth = ctx.measureText(agentText).width;
|
||||||
|
ctx.fillText(
|
||||||
|
agentText,
|
||||||
|
(CANVAS_WIDTH - agentWidth) / 2, // Center horizontally
|
||||||
|
CANVAS_HEIGHT * 0.41,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const descriptionText =
|
const descriptionText =
|
||||||
claimData.claim.name || claimData.claim.description;
|
claimData.claim.name ||
|
||||||
|
claimData.claim.description ||
|
||||||
|
claimData.claim.itemOffered?.description; // for Offers
|
||||||
if (descriptionText) {
|
if (descriptionText) {
|
||||||
const descriptionLine =
|
const descriptionLine =
|
||||||
descriptionText.length > 50
|
descriptionText.length > 50
|
||||||
@@ -164,12 +192,12 @@ export default class ClaimCertificateView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const possibleObject =
|
||||||
claimData.claim.object?.amountOfThisGood &&
|
claimData.claim.object || // for GiveActions
|
||||||
claimData.claim.object?.unitCode
|
claimData.claim.includesObject; // for Offers
|
||||||
) {
|
if (possibleObject?.amountOfThisGood && possibleObject?.unitCode) {
|
||||||
const amount = claimData.claim.object.amountOfThisGood;
|
const amount = possibleObject.amountOfThisGood;
|
||||||
const unit = claimData.claim.object.unitCode;
|
const unit = possibleObject.unitCode;
|
||||||
const amountText = serverUtil.displayAmount(unit, amount);
|
const amountText = serverUtil.displayAmount(unit, amount);
|
||||||
const amountWidth = ctx.measureText(amountText).width;
|
const amountWidth = ctx.measureText(amountText).width;
|
||||||
// if there was no description then put this in that spot, otherwise put it below the description
|
// if there was no description then put this in that spot, otherwise put it below the description
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
<button
|
<button
|
||||||
title="Copy Link"
|
title="Copy Link"
|
||||||
@click="
|
@click="
|
||||||
copyToClipboard('Current page link', window.location.href)
|
copyToClipboard('A link to this page', window.location.href)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<fa icon="link" class="text-slate-500" />
|
<fa icon="link" class="text-slate-500" />
|
||||||
@@ -270,16 +270,13 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
{{ didInfo(confirmerId) }}
|
{{ didInfo(confirmerId) }}
|
||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
|
||||||
<button
|
<a
|
||||||
@click="
|
:href="`/did/${confirmerId}`"
|
||||||
copyToClipboard(
|
target="_blank"
|
||||||
'The DID of ' + confirmerId,
|
class="text-blue-500"
|
||||||
confirmerId,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<fa icon="copy" class="text-slate-400 fa-fw" />
|
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||||
</button>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,16 +308,13 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
{{ didInfo(confsVisibleTo) }}
|
{{ didInfo(confsVisibleTo) }}
|
||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(confsVisibleTo)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(confsVisibleTo)">
|
||||||
<button
|
<a
|
||||||
@click="
|
:href="`/did/${confsVisibleTo}`"
|
||||||
copyToClipboard(
|
target="_blank"
|
||||||
'The DID of ' + confsVisibleTo,
|
class="text-blue-500"
|
||||||
confsVisibleTo,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<fa icon="copy" class="text-slate-400 fa-fw" />
|
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||||
</button>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -344,7 +338,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Note that a similar section is found in ConfirmGiftView.vue -->
|
<!-- Note that a similar section is found in ConfirmGiftView.vue, and kinda in HiddenDidDialog.vue -->
|
||||||
<h2
|
<h2
|
||||||
class="font-bold uppercase text-xl text-blue-500 mt-8 cursor-pointer"
|
class="font-bold uppercase text-xl text-blue-500 mt-8 cursor-pointer"
|
||||||
@click="showVeriClaimDump = !showVeriClaimDump"
|
@click="showVeriClaimDump = !showVeriClaimDump"
|
||||||
@@ -364,24 +358,26 @@
|
|||||||
Some of the details are not visible to you; they show as "HIDDEN". They
|
Some of the details are not visible to you; they show as "HIDDEN". They
|
||||||
are not visible to any of your direct contacts, either.
|
are not visible to any of your direct contacts, either.
|
||||||
<span v-if="canShare">
|
<span v-if="canShare">
|
||||||
If you'd like to ask any of your contacts to take a look and see if
|
You can ask one of your contacts to take a look and see if their
|
||||||
their contacts can see more details,
|
contacts can see more details:
|
||||||
<a @click="onClickShareClaim()" class="text-blue-500"
|
<a @click="onClickShareClaim()" class="text-blue-500"
|
||||||
>click to send them this info</a
|
>click to send them this page info</a
|
||||||
>
|
>
|
||||||
and see if they are willing to make an introduction. They are surely
|
and see if they can make an introduction. Someone is connected to
|
||||||
connected to someone; if you don't know who to ask, you might try the
|
people closer to them; if you don't know who to ask, try the person
|
||||||
person who registered you.
|
who registered you.
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
If you'd like to ask any of your contacts to take a look and see if
|
You can ask one of your contacts to take a look and see if their
|
||||||
their contacts can see more details,
|
contacts can see more details:
|
||||||
<a
|
<a
|
||||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>share this page with them</a
|
>click to copy this page info</a
|
||||||
>
|
>
|
||||||
and see if they are willing to make an introduction.
|
and see if they can make an introduction. Someone is connected to
|
||||||
|
people closer to them; if you don't know who to ask, try the person
|
||||||
|
who registered you.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -425,18 +421,21 @@
|
|||||||
<span>
|
<span>
|
||||||
{{ didInfo(visDid) }}
|
{{ didInfo(visDid) }}
|
||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||||
<button
|
<a
|
||||||
@click="copyToClipboard('The DID of ' + visDid, visDid)"
|
:href="`/did/${visDid}`"
|
||||||
|
target="_blank"
|
||||||
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
<fa icon="copy" class="text-slate-400 fa-fw" />
|
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||||
</button>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="veriClaim.publicUrls?.[visDid]"
|
<span v-if="veriClaim.publicUrls?.[visDid]"
|
||||||
>, found at <a
|
>, found at <a
|
||||||
:href="veriClaim.publicUrls?.[visDid]"
|
:href="veriClaim.publicUrls?.[visDid]"
|
||||||
|
target="_blank"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
<fa icon="globe" class="fa-fw text-slate-400" />
|
<fa icon="globe" class="fa-fw" />
|
||||||
{{
|
{{
|
||||||
veriClaim.publicUrls[visDid].substring(
|
veriClaim.publicUrls[visDid].substring(
|
||||||
veriClaim.publicUrls[visDid].indexOf("//") + 2,
|
veriClaim.publicUrls[visDid].indexOf("//") + 2,
|
||||||
@@ -452,7 +451,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="isEditedGlobalId" class="mt-2">
|
<span v-if="isEditedGlobalId" class="mt-2">
|
||||||
This record is an edited version. The latest version is here.
|
This record is an edited version. The latest version is shown.
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
|
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
|
||||||
@@ -963,9 +962,10 @@ export default class ClaimView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClickShareClaim() {
|
onClickShareClaim() {
|
||||||
|
this.copyToClipboard("A link to this page", this.windowLocation);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the full details of this claim. Can you help me?",
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||||
url: this.windowLocation,
|
url: this.windowLocation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,7 +254,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Note that a similar section is found in ClaimView.vue -->
|
<!-- Note that a similar section is found in ClaimView.vue, and kinda in HiddenDidDialog.vue -->
|
||||||
<h2
|
<h2
|
||||||
class="font-bold uppercase text-xl text-blue-500 mt-8 cursor-pointer"
|
class="font-bold uppercase text-xl text-blue-500 mt-8 cursor-pointer"
|
||||||
@click="showVeriClaimDump = !showVeriClaimDump"
|
@click="showVeriClaimDump = !showVeriClaimDump"
|
||||||
@@ -274,24 +274,26 @@
|
|||||||
Some of the details are not visible to you; they show as "HIDDEN".
|
Some of the details are not visible to you; they show as "HIDDEN".
|
||||||
They are not visible to any of your direct contacts, either.
|
They are not visible to any of your direct contacts, either.
|
||||||
<span v-if="canShare">
|
<span v-if="canShare">
|
||||||
If you'd like to ask any of your contacts to take a look and see if
|
You can ask one of your contacts to take a look and see if their
|
||||||
their contacts can see more details,
|
contacts can see more details:
|
||||||
<a @click="onClickShareClaim()" class="text-blue-500"
|
<a @click="onClickShareClaim()" class="text-blue-500"
|
||||||
>click to send them this info</a
|
>click to send them this page info</a
|
||||||
>
|
>
|
||||||
and see if they are willing to make an introduction. They are surely
|
and see if they can make an introduction. Someone is connected to
|
||||||
connected to someone; if you don't know who to ask, you might try
|
people closer to them; if you don't know who to ask, try the person
|
||||||
the person who registered you.
|
who registered you.
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
If you'd like to ask any of your contacts to take a look and see if
|
You can ask one of your contacts to take a look and see if their
|
||||||
their contacts can see more details,
|
contacts can see more details:
|
||||||
<a
|
<a
|
||||||
@click="copyToClipboard('Location', windowLocation.href)"
|
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>share this page with them</a
|
>click to copy this page info</a
|
||||||
>
|
>
|
||||||
and see if they are willing to make an introduction.
|
and see if they can make an introduction. Someone is connected to
|
||||||
|
people closer to them; if you don't know who to ask, try the person
|
||||||
|
who registered you.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -308,9 +310,7 @@
|
|||||||
<span v-else>
|
<span v-else>
|
||||||
If you'd like an introduction,
|
If you'd like an introduction,
|
||||||
<a
|
<a
|
||||||
@click="
|
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||||
copyToClipboard('A link to this page', windowLocation.href)
|
|
||||||
"
|
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>share this page with them and ask if they'll tell you more about
|
>share this page with them and ask if they'll tell you more about
|
||||||
about the participants.</a
|
about the participants.</a
|
||||||
@@ -448,7 +448,7 @@ export default class ClaimView extends Vue {
|
|||||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||||
veriClaimDump = "";
|
veriClaimDump = "";
|
||||||
veriClaimDidsVisible = {};
|
veriClaimDidsVisible = {};
|
||||||
windowLocation = window.location;
|
windowLocation = window.location.href;
|
||||||
|
|
||||||
R = R;
|
R = R;
|
||||||
yaml = yaml;
|
yaml = yaml;
|
||||||
@@ -856,10 +856,11 @@ export default class ClaimView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClickShareClaim() {
|
onClickShareClaim() {
|
||||||
|
this.copyToClipboard("A link to this page", this.windowLocation);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the full details of this claim. Can you help me?",
|
text: "I'm trying to find the full details of this claim. Can you help me?",
|
||||||
url: this.windowLocation.href,
|
url: this.windowLocation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 id="ViewHeading" class="text-lg text-center font-light relative px-7">
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
<button
|
<button
|
||||||
@click="$router.go(-1)"
|
@click="$router.go(-1)"
|
||||||
|
|||||||
@@ -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,16 @@
|
|||||||
>
|
>
|
||||||
<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,13 +202,15 @@
|
|||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<InfiniteScroll @reached-bottom="loadMoreData">
|
<InfiniteScroll @reached-bottom="loadMoreData">
|
||||||
<ul id="listDiscoverResults">
|
<ul id="listDiscoverResults">
|
||||||
|
<!-- Projects List -->
|
||||||
|
<template v-if="isProjectsActive">
|
||||||
<li
|
<li
|
||||||
class="border-b border-slate-300"
|
class="border-b border-slate-300"
|
||||||
v-for="project in projects"
|
v-for="project in projects"
|
||||||
:key="project.handleId"
|
:key="project.handleId"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@click="onClickLoadProject(project.handleId)"
|
@click="onClickLoadItem(project.handleId)"
|
||||||
class="block py-4 flex gap-4 cursor-pointer"
|
class="block py-4 flex gap-4 cursor-pointer"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -181,12 +227,64 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
{{
|
{{
|
||||||
didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
|
didInfo(
|
||||||
|
project.issuerDid,
|
||||||
|
activeDid,
|
||||||
|
allMyDids,
|
||||||
|
allContacts,
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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?.rowId || '')"
|
||||||
|
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="isAnywhereActive && profile.locLat && profile.locLon"
|
||||||
|
class="mt-1 text-xs text-slate-500"
|
||||||
|
>
|
||||||
|
<fa icon="location-dot" class="fa-fw"></fa>
|
||||||
|
{{
|
||||||
|
(profile.locLat > 0 ? "North" : "South") +
|
||||||
|
" in " +
|
||||||
|
(profile.locLon > 0 ? "Eastern" : "Western") +
|
||||||
|
" Hemisphere"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
</section>
|
</section>
|
||||||
@@ -197,7 +295,7 @@ 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";
|
||||||
@@ -220,6 +318,16 @@ import {
|
|||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
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 +341,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 +375,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 +400,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 +414,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 +427,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 +436,58 @@ 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",
|
method: "GET",
|
||||||
headers: await getHeaders(this.activeDid),
|
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();
|
||||||
|
|
||||||
|
if (this.isProjectsActive) {
|
||||||
|
this.userProfiles = [];
|
||||||
const plans: PlanData[] = results.data;
|
const plans: PlanData[] = results.data;
|
||||||
if (plans) {
|
if (plans) {
|
||||||
for (const plan of plans) {
|
this.projects.push(...plans);
|
||||||
const { name, description, handleId, image, issuerDid, rowid } = plan;
|
|
||||||
this.projects.push({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
handleId,
|
|
||||||
image,
|
|
||||||
issuerDid,
|
|
||||||
rowid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.remoteCount = this.projects.length;
|
this.remoteCount = this.projects.length;
|
||||||
} else {
|
} else {
|
||||||
throw JSON.stringify(results);
|
throw JSON.stringify(results);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.projects = [];
|
||||||
|
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 +505,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 +522,64 @@ 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",
|
method: "GET",
|
||||||
headers: await getHeaders(this.activeDid),
|
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) {
|
this.userProfiles = [];
|
||||||
const plans: PlanData[] = results.data;
|
const plans: PlanData[] = results.data;
|
||||||
for (const plan of plans) {
|
if (plans) {
|
||||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
this.projects.push(...plans);
|
||||||
this.projects.push({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
handleId,
|
|
||||||
issuerDid,
|
|
||||||
rowid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.projects = results.data;
|
|
||||||
}
|
|
||||||
this.localCount = this.projects.length;
|
this.localCount = this.projects.length;
|
||||||
} else {
|
} else {
|
||||||
throw JSON.stringify(results);
|
throw JSON.stringify(results);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.projects = [];
|
||||||
|
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,26 +593,38 @@ 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) {
|
||||||
|
if (this.isProjectsActive && this.projects.length > 0) {
|
||||||
const latestProject = this.projects[this.projects.length - 1];
|
const latestProject = this.projects[this.projects.length - 1];
|
||||||
if (this.isLocalActive) {
|
if (this.isLocalActive || this.isMappedActive) {
|
||||||
this.searchLocal(latestProject["rowid"]);
|
this.searchLocal(latestProject.rowId);
|
||||||
} else if (this.isMappedActive) {
|
} else if (this.isAnywhereActive) {
|
||||||
this.searchLocal(latestProject["rowid"]);
|
this.searchAll(latestProject.rowId);
|
||||||
} else if (this.isRemoteActive) {
|
}
|
||||||
this.searchAll(latestProject["rowid"]);
|
} else if (this.isPeopleActive && 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 || "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearMarkers() {
|
||||||
|
Object.values(this.markers).forEach((marker) => marker.remove());
|
||||||
|
this.markers = {};
|
||||||
|
}
|
||||||
|
|
||||||
async onMapReady(map: L.Map) {
|
async onMapReady(map: L.Map) {
|
||||||
// doing this here instead of the l-map element avoids a recentering after the first drag
|
// doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
|
||||||
map.setView([this.localCenterLat, this.localCenterLong], 2);
|
map.setView([this.localCenterLat, this.localCenterLong], 2);
|
||||||
this.requestTiles(map);
|
this.requestTiles(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -521,8 +642,7 @@ export default class DiscoverView extends Vue {
|
|||||||
|
|
||||||
onZoomStart(/* event: L.LocationEvent */) {
|
onZoomStart(/* event: L.LocationEvent */) {
|
||||||
// remove markers because otherwise they jump around at zoom end
|
// remove markers because otherwise they jump around at zoom end
|
||||||
Object.values(this.markers).forEach((marker) => marker.remove());
|
this.clearMarkers();
|
||||||
this.markers = {};
|
|
||||||
|
|
||||||
this.zoomedSoDoNotMove = true;
|
this.zoomedSoDoNotMove = true;
|
||||||
}
|
}
|
||||||
@@ -540,15 +660,15 @@ 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());
|
this.clearMarkers();
|
||||||
this.markers = {};
|
|
||||||
const results = await response.json();
|
const results = await response.json();
|
||||||
if (results.data?.tiles?.length > 0) {
|
if (results.data?.tiles?.length > 0) {
|
||||||
for (const tile of results.data.tiles) {
|
for (const tile: Tile of results.data.tiles) {
|
||||||
const pinLat = (tile.minFoundLat + tile.maxFoundLat) / 2;
|
const pinLat = (tile.minFoundLat + tile.maxFoundLat) / 2;
|
||||||
const pinLon = (tile.minFoundLon + tile.maxFoundLon) / 2;
|
const pinLon = (tile.minFoundLon + tile.maxFoundLon) / 2;
|
||||||
const numberIcon = L.divIcon({
|
const numberIcon = L.divIcon({
|
||||||
@@ -569,10 +689,22 @@ export default class DiscoverView extends Vue {
|
|||||||
};
|
};
|
||||||
this.searchLocal();
|
this.searchLocal();
|
||||||
});
|
});
|
||||||
this.markers["" + tile.indexLat + "X" + tile.indexLon] = marker;
|
this.markers[
|
||||||
|
"" +
|
||||||
|
tile.indexLat +
|
||||||
|
"X" +
|
||||||
|
tile.indexLon +
|
||||||
|
"_" +
|
||||||
|
tile.minFoundLat +
|
||||||
|
"X" +
|
||||||
|
tile.minFoundLon +
|
||||||
|
"-" +
|
||||||
|
tile.maxFoundLat +
|
||||||
|
"X" +
|
||||||
|
tile.maxFoundLon
|
||||||
|
] = marker;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.searchLocal();
|
|
||||||
} else {
|
} else {
|
||||||
throw {
|
throw {
|
||||||
message: "Got an error loading projects on the map.",
|
message: "Got an error loading projects on the map.",
|
||||||
@@ -601,14 +733,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 +788,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.isRemoteActive,
|
"text-blue-600": !this.isAnywhereActive,
|
||||||
"border-transparent": !this.isRemoteActive,
|
"border-transparent": !this.isAnywhereActive,
|
||||||
"hover:border-slate-400": !this.isRemoteActive,
|
"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.isPeopleActive,
|
||||||
|
"border-transparent": !this.isPeopleActive,
|
||||||
|
"hover:border-slate-400": !this.isPeopleActive,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<fa
|
<fa
|
||||||
v-if="dids[0] == selectedArrayFirstDid"
|
v-if="dids[0] == selectedArrayFirstDid"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
class="fa-fw text-blue-400 text-xl mr-3"
|
class="fa-fw text-blue-500 text-xl mr-3"
|
||||||
></fa>
|
></fa>
|
||||||
<fa
|
<fa
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -152,11 +154,17 @@
|
|||||||
<div class="flex" @click="sendToTrustroots = !sendToTrustroots">
|
<div class="flex" @click="sendToTrustroots = !sendToTrustroots">
|
||||||
<input type="checkbox" class="mr-2" v-model="sendToTrustroots" />
|
<input type="checkbox" class="mr-2" v-model="sendToTrustroots" />
|
||||||
<label>Send to Trustroots</label>
|
<label>Send to Trustroots</label>
|
||||||
|
<fa
|
||||||
|
icon="circle-info"
|
||||||
|
class="text-blue-500 ml-2 cursor-pointer"
|
||||||
|
@click.stop="showNostrPartnerInfo"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!--
|
<!--
|
||||||
<div class="flex" @click="sendToTripHopping = !sendToTripHopping">
|
<div class="flex" @click="sendToTripHopping = !sendToTripHopping">
|
||||||
<input type="checkbox" class="mr-2" v-model="sendToTripHopping" />
|
<input type="checkbox" class="mr-2" v-model="sendToTripHopping" />
|
||||||
<label>Send to TripHopping</label>
|
<label>Send to TripHopping</label>
|
||||||
|
<fa icon="circle-info" class="text-blue-500 ml-2 cursor-pointer" @click.stop="showNostrPartnerInfo" />
|
||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
</div>
|
</div>
|
||||||
@@ -195,8 +203,12 @@ import "leaflet/dist/leaflet.css";
|
|||||||
import { AxiosError, AxiosRequestHeaders } from "axios";
|
import { AxiosError, AxiosRequestHeaders } from "axios";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { hexToBytes } from "@noble/hashes/utils";
|
import { hexToBytes } from "@noble/hashes/utils";
|
||||||
import type { EventTemplate, VerifiedEvent } from "nostr-tools/lib/types/core";
|
// these core imports could also be included as "import type ..."
|
||||||
import { accountFromSeedWords } from "nostr-tools/nip06";
|
import { EventTemplate, UnsignedEvent, VerifiedEvent } from "nostr-tools/core";
|
||||||
|
import {
|
||||||
|
accountFromExtendedKey,
|
||||||
|
extendedKeysFromSeedWords,
|
||||||
|
} from "nostr-tools/nip06";
|
||||||
import { finalizeEvent, serializeEvent } from "nostr-tools/pure";
|
import { finalizeEvent, serializeEvent } from "nostr-tools/pure";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
@@ -217,7 +229,6 @@ import {
|
|||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import {
|
import {
|
||||||
retrieveAccountCount,
|
retrieveAccountCount,
|
||||||
retrieveAccountMetadata,
|
|
||||||
retrieveFullyDecryptedAccount,
|
retrieveFullyDecryptedAccount,
|
||||||
} from "../libs/util";
|
} from "../libs/util";
|
||||||
|
|
||||||
@@ -408,6 +419,18 @@ export default class NewEditProjectView extends Vue {
|
|||||||
delete vcClaim.image;
|
delete vcClaim.image;
|
||||||
}
|
}
|
||||||
if (this.includeLocation) {
|
if (this.includeLocation) {
|
||||||
|
if (!this.latitude || !this.longitude) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Location Error",
|
||||||
|
text: "The location was invalid so it was not set.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
delete vcClaim.location;
|
||||||
|
} else {
|
||||||
vcClaim.location = {
|
vcClaim.location = {
|
||||||
geo: {
|
geo: {
|
||||||
"@type": "GeoCoordinates",
|
"@type": "GeoCoordinates",
|
||||||
@@ -415,6 +438,7 @@ export default class NewEditProjectView extends Vue {
|
|||||||
longitude: this.longitude,
|
longitude: this.longitude,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
delete vcClaim.location;
|
delete vcClaim.location;
|
||||||
}
|
}
|
||||||
@@ -431,7 +455,7 @@ export default class NewEditProjectView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Date Error",
|
||||||
text: "The date was invalid so it was not set.",
|
text: "The date was invalid so it was not set.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
@@ -451,31 +475,59 @@ export default class NewEditProjectView extends Vue {
|
|||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
if (resp.data?.success?.handleId) {
|
if (resp.data?.success?.handleId) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Saved",
|
||||||
|
text: "The project was saved successfully.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
|
||||||
this.errorMessage = "";
|
this.errorMessage = "";
|
||||||
|
|
||||||
const projectPath = encodeURIComponent(resp.data.success.handleId);
|
const projectPath = encodeURIComponent(resp.data.success.handleId);
|
||||||
|
|
||||||
let signedPayload: VerifiedEvent | undefined; // sign something to prove ownership of pubkey
|
if (this.sendToTrustroots || this.sendToTripHopping) {
|
||||||
|
if (this.latitude && this.longitude) {
|
||||||
|
let payloadAndKey; // sign something to prove ownership of pubkey
|
||||||
if (this.sendToTrustroots) {
|
if (this.sendToTrustroots) {
|
||||||
signedPayload = await this.signPayload();
|
payloadAndKey = await this.signSomePayload();
|
||||||
|
// not going to await... the save was successful, so we'll continue to the next page
|
||||||
this.sendToNostrPartner(
|
this.sendToNostrPartner(
|
||||||
"NOSTR-EVENT-TRUSTROOTS",
|
"NOSTR-EVENT-TRUSTROOTS",
|
||||||
"Trustroots",
|
"Trustroots",
|
||||||
resp.data.success.claimId,
|
resp.data.success.claimId,
|
||||||
signedPayload,
|
payloadAndKey.signedEvent,
|
||||||
|
payloadAndKey.publicExtendedKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.sendToTripHopping) {
|
if (this.sendToTripHopping) {
|
||||||
if (!signedPayload) {
|
if (!payloadAndKey) {
|
||||||
signedPayload = await this.signPayload();
|
payloadAndKey = await this.signSomePayload();
|
||||||
}
|
}
|
||||||
|
// not going to await... the save was successful, so we'll continue to the next page
|
||||||
this.sendToNostrPartner(
|
this.sendToNostrPartner(
|
||||||
"NOSTR-EVENT-TRIPHOPPING",
|
"NOSTR-EVENT-TRIPHOPPING",
|
||||||
"TripHopping",
|
"TripHopping",
|
||||||
resp.data.success.claimId,
|
resp.data.success.claimId,
|
||||||
signedPayload,
|
payloadAndKey.signedEvent,
|
||||||
|
payloadAndKey.publicExtendedKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Partner Error",
|
||||||
|
text: "A partner was selected but the location was not set, so it was not sent to any partner.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(this.$router as Router).push({ path: "/project/" + projectPath });
|
(this.$router as Router).push({ path: "/project/" + projectPath });
|
||||||
} else {
|
} else {
|
||||||
@@ -541,19 +593,28 @@ export default class NewEditProjectView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async signPayload(): Promise<VerifiedEvent> {
|
/**
|
||||||
|
* @return a signed payload and an extended public key for later transmission
|
||||||
|
*/
|
||||||
|
private async signSomePayload(): Promise<{
|
||||||
|
signedEvent: VerifiedEvent;
|
||||||
|
publicExtendedKey: string;
|
||||||
|
}> {
|
||||||
const account = await retrieveFullyDecryptedAccount(this.activeDid);
|
const account = await retrieveFullyDecryptedAccount(this.activeDid);
|
||||||
// get the last number of the derivationPath
|
// get the last number of the derivationPath
|
||||||
const finalDerNum = account?.derivationPath?.split?.("/")?.reverse()[0];
|
const finalDerNum = account?.derivationPath?.split?.("/")?.reverse()[0];
|
||||||
// remove any trailing '
|
// remove any trailing '
|
||||||
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
|
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
|
||||||
const accountNum = Number(finalDerNumNoApostrophe || 0);
|
const accountNum = Number(finalDerNumNoApostrophe || 0);
|
||||||
const pubPri = accountFromSeedWords(
|
const extPubPri = extendedKeysFromSeedWords(
|
||||||
account?.mnemonic as string,
|
account?.mnemonic as string,
|
||||||
"",
|
"",
|
||||||
accountNum,
|
accountNum,
|
||||||
);
|
);
|
||||||
const privateBytes = hexToBytes(pubPri?.privateKey);
|
const publicExtendedKey: string = extPubPri?.publicExtendedKey;
|
||||||
|
const privateExtendedKey = extPubPri?.privateExtendedKey;
|
||||||
|
const privateKey = accountFromExtendedKey(privateExtendedKey).privateKey;
|
||||||
|
const privateBytes = hexToBytes(privateKey);
|
||||||
// No real content is necessary, we just want something signed,
|
// No real content is necessary, we just want something signed,
|
||||||
// so we might as well use nostr libs for nostr functions.
|
// so we might as well use nostr libs for nostr functions.
|
||||||
// Besides: someday we may create real content that we can relay.
|
// Besides: someday we may create real content that we can relay.
|
||||||
@@ -563,9 +624,12 @@ export default class NewEditProjectView extends Vue {
|
|||||||
content: "",
|
content: "",
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
};
|
};
|
||||||
|
const signedEvent: VerifiedEvent = finalizeEvent(
|
||||||
// Why does IntelliJ not see matching types?
|
// Why does IntelliJ not see matching types?
|
||||||
const signedEvent = finalizeEvent(event, privateBytes);
|
event as EventTemplate,
|
||||||
return signedEvent;
|
privateBytes,
|
||||||
|
) as VerifiedEvent;
|
||||||
|
return { signedEvent, publicExtendedKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendToNostrPartner(
|
private async sendToNostrPartner(
|
||||||
@@ -573,21 +637,9 @@ export default class NewEditProjectView extends Vue {
|
|||||||
serviceName: string,
|
serviceName: string,
|
||||||
jwtId: string,
|
jwtId: string,
|
||||||
signedPayload: VerifiedEvent,
|
signedPayload: VerifiedEvent,
|
||||||
|
publicExtendedKey: string,
|
||||||
) {
|
) {
|
||||||
// first, get the public key for nostr
|
try {
|
||||||
const account = await retrieveAccountMetadata(this.activeDid);
|
|
||||||
// get the last number of the derivationPath
|
|
||||||
const finalDerNum = account?.derivationPath?.split?.("/")?.reverse()[0];
|
|
||||||
// remove any trailing '
|
|
||||||
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
|
|
||||||
const accountNum = Number(finalDerNumNoApostrophe || 0);
|
|
||||||
const pubPri = accountFromSeedWords(
|
|
||||||
account?.mnemonic as string,
|
|
||||||
"",
|
|
||||||
accountNum,
|
|
||||||
);
|
|
||||||
const nostrPubKey = pubPri?.publicKey;
|
|
||||||
|
|
||||||
let partnerServer = DEFAULT_PARTNER_API_SERVER;
|
let partnerServer = DEFAULT_PARTNER_API_SERVER;
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
if (settings.partnerApiServer) {
|
if (settings.partnerApiServer) {
|
||||||
@@ -596,18 +648,26 @@ export default class NewEditProjectView extends Vue {
|
|||||||
const endorserPartnerUrl = partnerServer + "/api/partner/link";
|
const endorserPartnerUrl = partnerServer + "/api/partner/link";
|
||||||
const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
|
const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
|
||||||
const content = this.fullClaim.name + " - see " + timeSafariUrl;
|
const content = this.fullClaim.name + " - see " + timeSafariUrl;
|
||||||
|
const publicKeyHex = accountFromExtendedKey(publicExtendedKey).publicKey;
|
||||||
|
const unsignedPayload: UnsignedEvent = {
|
||||||
|
// why doesn't "...signedPayload" work?
|
||||||
|
kind: signedPayload.kind,
|
||||||
|
tags: signedPayload.tags,
|
||||||
|
content: signedPayload.content,
|
||||||
|
created_at: signedPayload.created_at,
|
||||||
|
pubkey: publicKeyHex,
|
||||||
|
};
|
||||||
// Why does IntelliJ not see matching types?
|
// Why does IntelliJ not see matching types?
|
||||||
const payload = serializeEvent(signedPayload);
|
const payload = serializeEvent(unsignedPayload as UnsignedEvent);
|
||||||
const partnerParams = {
|
const partnerParams = {
|
||||||
jwtId: jwtId,
|
jwtId: jwtId,
|
||||||
linkCode: linkCode,
|
linkCode: linkCode,
|
||||||
inputJson: JSON.stringify(content),
|
inputJson: JSON.stringify(content),
|
||||||
pubKeyHex: nostrPubKey,
|
pubKeyHex: publicKeyHex,
|
||||||
pubKeyImage: payload,
|
pubKeyImage: payload,
|
||||||
pubKeySigHex: signedPayload.sig,
|
pubKeySigHex: signedPayload.sig,
|
||||||
};
|
};
|
||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
try {
|
|
||||||
const linkResp = await this.axios.post(
|
const linkResp = await this.axios.post(
|
||||||
endorserPartnerUrl,
|
endorserPartnerUrl,
|
||||||
partnerParams,
|
partnerParams,
|
||||||
@@ -689,5 +749,17 @@ export default class NewEditProjectView extends Vue {
|
|||||||
public onCancelClick() {
|
public onCancelClick() {
|
||||||
(this.$router as Router).back();
|
(this.$router as Router).back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public showNostrPartnerInfo() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "info",
|
||||||
|
title: "About Nostr Events",
|
||||||
|
text: "This will submit this project to a partner on the nostr network. It will contain your public key data which may allow correlation, so don't allow this if you're not comfortable with that.",
|
||||||
|
},
|
||||||
|
7000,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
<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">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb">
|
<div id="ViewBreadcrumb">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<div>
|
||||||
|
<h1 class="text-center text-lg font-light relative px-7">
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
<button
|
<button
|
||||||
@click="$router.go(-1)"
|
@click="$router.go(-1)"
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
</button>
|
</button>
|
||||||
Project Idea
|
Project Idea
|
||||||
</h1>
|
</h1>
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-center text-xl font-semibold">
|
||||||
{{ name }}
|
{{ name }}
|
||||||
<button
|
<button
|
||||||
v-if="activeDid === issuer || activeDid === agentDid"
|
v-if="activeDid === issuer || activeDid === agentDid"
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Project Details -->
|
<!-- Project Details -->
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4">
|
||||||
@@ -47,22 +49,22 @@
|
|||||||
<div class="text-sm mb-3">
|
<div class="text-sm mb-3">
|
||||||
<div class="truncate">
|
<div class="truncate">
|
||||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
{{
|
{{ issuerInfoObject?.displayName }}
|
||||||
serverUtil.didInfo(issuer, activeDid, allMyDids, allContacts)
|
|
||||||
}}
|
|
||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
|
||||||
<button
|
<a
|
||||||
@click="
|
:href="`/did/${issuer}`"
|
||||||
libsUtil.doCopyTwoSecRedo(
|
target="_blank"
|
||||||
issuer,
|
class="text-blue-500"
|
||||||
() => (showDidCopy = !showDidCopy),
|
|
||||||
)
|
|
||||||
"
|
|
||||||
class="ml-2 mr-2"
|
|
||||||
>
|
>
|
||||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||||
</button>
|
</a>
|
||||||
<span v-show="showDidCopy">Copied DID</span>
|
</span>
|
||||||
|
<span v-else-if="serverUtil.isHiddenDid(issuer)">
|
||||||
|
<fa
|
||||||
|
icon="info-circle"
|
||||||
|
class="fa-fw text-blue-500 cursor-pointer"
|
||||||
|
@click="openHiddenDidDialog()"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="startTime">
|
<div v-if="startTime">
|
||||||
@@ -74,14 +76,21 @@
|
|||||||
<a
|
<a
|
||||||
:href="getOpenStreetMapUrl()"
|
:href="getOpenStreetMapUrl()"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline"
|
class="underline text-blue-500"
|
||||||
>Map View
|
>Map View
|
||||||
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
<fa
|
||||||
|
icon="arrow-up-right-from-square"
|
||||||
|
class="fa-fw text-blue-500"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="url">
|
<div v-if="url">
|
||||||
<fa icon="globe" class="fa-fw text-slate-400"></fa>
|
<fa icon="globe" class="fa-fw text-slate-400"></fa>
|
||||||
<a :href="addScheme(url)" target="_blank" class="underline">
|
<a
|
||||||
|
:href="addScheme(url)"
|
||||||
|
target="_blank"
|
||||||
|
class="underline text-blue-500"
|
||||||
|
>
|
||||||
{{ domainForWebsite(this.url) }}
|
{{ domainForWebsite(this.url) }}
|
||||||
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||||
</a>
|
</a>
|
||||||
@@ -475,6 +484,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<HiddenDidDialog ref="hiddenDidDialog" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -506,11 +517,13 @@ import {
|
|||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
import * as serverUtil from "../libs/endorserServer";
|
||||||
import { retrieveAccountDids } from "../libs/util";
|
import { retrieveAccountDids } from "../libs/util";
|
||||||
|
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
GiftedDialog,
|
GiftedDialog,
|
||||||
|
HiddenDidDialog,
|
||||||
OfferDialog,
|
OfferDialog,
|
||||||
ProjectIcon,
|
ProjectIcon,
|
||||||
QuickNav,
|
QuickNav,
|
||||||
@@ -522,6 +535,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
agentDid = "";
|
agentDid = "";
|
||||||
|
agentDidVisibleToDids: Array<string> = [];
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
@@ -538,6 +552,12 @@ export default class ProjectViewView extends Vue {
|
|||||||
imageUrl = "";
|
imageUrl = "";
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
issuer = "";
|
issuer = "";
|
||||||
|
issuerInfoObject: {
|
||||||
|
known: boolean;
|
||||||
|
displayName: string;
|
||||||
|
profileImageUrl?: string;
|
||||||
|
} | null = null;
|
||||||
|
issuerVisibleToDids: Array<string> = [];
|
||||||
latitude = 0;
|
latitude = 0;
|
||||||
longitude = 0;
|
longitude = 0;
|
||||||
name = "";
|
name = "";
|
||||||
@@ -545,7 +565,6 @@ export default class ProjectViewView extends Vue {
|
|||||||
offersHitLimit = false;
|
offersHitLimit = false;
|
||||||
projectId = ""; // handle ID
|
projectId = ""; // handle ID
|
||||||
recentlyCheckedAndUnconfirmableJwts: string[] = [];
|
recentlyCheckedAndUnconfirmableJwts: string[] = [];
|
||||||
showDidCopy = false;
|
|
||||||
startTime = "";
|
startTime = "";
|
||||||
truncatedDesc = "";
|
truncatedDesc = "";
|
||||||
truncateLength = 40;
|
truncateLength = 40;
|
||||||
@@ -623,8 +642,17 @@ export default class ProjectViewView extends Vue {
|
|||||||
startDateTime.toLocaleTimeString();
|
startDateTime.toLocaleTimeString();
|
||||||
}
|
}
|
||||||
this.agentDid = resp.data.claim?.agent?.identifier;
|
this.agentDid = resp.data.claim?.agent?.identifier;
|
||||||
|
this.agentDidVisibleToDids =
|
||||||
|
resp.data.claim?.agent?.identifierVisibleToDids || [];
|
||||||
this.imageUrl = resp.data.claim?.image;
|
this.imageUrl = resp.data.claim?.image;
|
||||||
this.issuer = resp.data.issuer;
|
this.issuer = resp.data.issuer;
|
||||||
|
this.issuerInfoObject = serverUtil.didInfoObject(
|
||||||
|
this.issuer,
|
||||||
|
this.activeDid,
|
||||||
|
this.allMyDids,
|
||||||
|
this.allContacts,
|
||||||
|
);
|
||||||
|
this.issuerVisibleToDids = resp.data.issuerVisibleToDids || [];
|
||||||
this.name = resp.data.claim?.name || "(no name)";
|
this.name = resp.data.claim?.name || "(no name)";
|
||||||
this.description = resp.data.claim?.description || "(no description)";
|
this.description = resp.data.claim?.description || "(no description)";
|
||||||
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
||||||
@@ -1156,5 +1184,15 @@ export default class ProjectViewView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openHiddenDidDialog() {
|
||||||
|
(this.$refs.hiddenDidDialog as HiddenDidDialog).open(
|
||||||
|
"creator",
|
||||||
|
this.issuerVisibleToDids,
|
||||||
|
this.allContacts,
|
||||||
|
this.activeDid,
|
||||||
|
this.allMyDids,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -361,14 +361,14 @@ export default class ProjectsView extends Vue {
|
|||||||
if (resp.status === 200 && resp.data.data) {
|
if (resp.status === 200 && resp.data.data) {
|
||||||
const plans: PlanData[] = resp.data.data;
|
const plans: PlanData[] = resp.data.data;
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, image, issuerDid, rowid } = plan;
|
const { name, description, handleId, image, issuerDid, rowId } = plan;
|
||||||
this.projects.push({
|
this.projects.push({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
image,
|
image,
|
||||||
handleId,
|
handleId,
|
||||||
issuerDid,
|
issuerDid,
|
||||||
rowid,
|
rowId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -395,7 +395,7 @@ export default class ProjectsView extends Vue {
|
|||||||
async loadMoreProjectData(payload: boolean) {
|
async loadMoreProjectData(payload: boolean) {
|
||||||
if (this.projects.length > 0 && payload) {
|
if (this.projects.length > 0 && payload) {
|
||||||
const latestProject = this.projects[this.projects.length - 1];
|
const latestProject = this.projects[this.projects.length - 1];
|
||||||
await this.loadProjects(`beforeId=${latestProject.rowid}`);
|
await this.loadProjects(`beforeId=${latestProject.rowId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
184
src/views/UserProfileView.vue
Normal file
184
src/views/UserProfileView.vue
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav selected="Discover" />
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
|
<h1 id="ViewHeading" class="text-lg text-center font-light relative px-7">
|
||||||
|
<!-- Back -->
|
||||||
|
<button
|
||||||
|
@click="$router.go(-1)"
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
Individual Profile
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Animation -->
|
||||||
|
<div
|
||||||
|
class="fixed left-6 mt-16 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||||
|
v-if="isLoading"
|
||||||
|
>
|
||||||
|
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="profile">
|
||||||
|
<!-- Profile Info -->
|
||||||
|
<div class="mt-8">
|
||||||
|
<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-4 text-slate-600">
|
||||||
|
{{ profile.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map for first coordinates -->
|
||||||
|
<div v-if="profile?.locLat && profile?.locLon" class="mt-4">
|
||||||
|
<h2 class="text-lg font-semibold">Location</h2>
|
||||||
|
<div class="h-96 mt-2 w-full">
|
||||||
|
<l-map
|
||||||
|
ref="profileMap"
|
||||||
|
:center="[profile.locLat, profile.locLon]"
|
||||||
|
:zoom="12"
|
||||||
|
>
|
||||||
|
<l-tile-layer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
layer-type="base"
|
||||||
|
name="OpenStreetMap"
|
||||||
|
/>
|
||||||
|
<l-marker :lat-lng="[profile.locLat, profile.locLon]">
|
||||||
|
<l-popup>{{
|
||||||
|
didInfo(profile.issuerDid, activeDid, allMyDids, allContacts)
|
||||||
|
}}</l-popup>
|
||||||
|
</l-marker>
|
||||||
|
</l-map>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map for second coordinates -->
|
||||||
|
<div v-if="profile?.locLat2 && profile?.locLon2" class="mt-4">
|
||||||
|
<h2 class="text-lg font-semibold">Second Location</h2>
|
||||||
|
<div class="h-96 mt-2 w-full">
|
||||||
|
<l-map
|
||||||
|
ref="profileMap"
|
||||||
|
:center="[profile.locLat2, profile.locLon2]"
|
||||||
|
:zoom="12"
|
||||||
|
>
|
||||||
|
<l-tile-layer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
layer-type="base"
|
||||||
|
name="OpenStreetMap"
|
||||||
|
/>
|
||||||
|
<l-marker :lat-lng="[profile.locLat2, profile.locLon2]">
|
||||||
|
<l-popup>{{
|
||||||
|
didInfo(profile.issuerDid, activeDid, allMyDids, allContacts)
|
||||||
|
}}</l-popup>
|
||||||
|
</l-marker>
|
||||||
|
</l-map>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="text-center mt-8">
|
||||||
|
<p class="text-lg text-slate-500">Profile not found.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { LMap, LTileLayer, LMarker, LPopup } from "@vue-leaflet/vue-leaflet";
|
||||||
|
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
||||||
|
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
|
import { DEFAULT_PARTNER_API_SERVER, NotificationIface } from "@/constants/app";
|
||||||
|
import { db } from "@/db/index";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
import { didInfo, getHeaders } from "@/libs/endorserServer";
|
||||||
|
import { UserProfile } from "@/libs/partnerServer";
|
||||||
|
import { retrieveAccountDids } from "@/libs/util";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LPopup,
|
||||||
|
LTileLayer,
|
||||||
|
QuickNav,
|
||||||
|
TopMessage,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class UserProfileView extends Vue {
|
||||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
$router!: Router;
|
||||||
|
$route!: RouteLocationNormalizedLoaded;
|
||||||
|
|
||||||
|
activeDid = "";
|
||||||
|
allContacts: Array<Contact> = [];
|
||||||
|
allMyDids: Array<string> = [];
|
||||||
|
isLoading = true;
|
||||||
|
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
||||||
|
profile: UserProfile | null = null;
|
||||||
|
|
||||||
|
// make this function available to the Vue template
|
||||||
|
didInfo = didInfo;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
const settings = await db.settings.toArray();
|
||||||
|
this.activeDid = settings[0]?.activeDid || "";
|
||||||
|
this.partnerApiServer =
|
||||||
|
settings[0]?.partnerApiServer || this.partnerApiServer;
|
||||||
|
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
this.allMyDids = await retrieveAccountDids();
|
||||||
|
|
||||||
|
await this.loadProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadProfile() {
|
||||||
|
const profileId: string = this.$route.params.id as string;
|
||||||
|
if (!profileId) {
|
||||||
|
this.isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.partnerApiServer}/api/partner/userProfile/${encodeURIComponent(profileId)}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: await getHeaders(this.activeDid),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
const result = await response.json();
|
||||||
|
this.profile = result.data;
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to load profile");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading profile:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "There was a problem loading the profile.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -23,6 +23,6 @@ test('Check usage limits', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Set Your Name' }).click();
|
await page.getByRole('button', { name: 'Set Your Name' }).click();
|
||||||
const name = 'User ' + did.slice(11, 14);
|
const name = 'User ' + did.slice(11, 14);
|
||||||
await page.getByPlaceholder('Name').fill(name);
|
await page.getByPlaceholder('Name').fill(name);
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -2,6 +2,8 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { importUser, createUniqueStringsArray } from './testUtils';
|
import { importUser, createUniqueStringsArray } from './testUtils';
|
||||||
|
|
||||||
test('Create 10 new projects', async ({ page }) => {
|
test('Create 10 new projects', async ({ page }) => {
|
||||||
|
test.setTimeout(40000); // Set timeout longer since it often fails at 30 seconds
|
||||||
|
|
||||||
const projectCount = 10;
|
const projectCount = 10;
|
||||||
|
|
||||||
// Standard texts
|
// Standard texts
|
||||||
|
|||||||
Reference in New Issue
Block a user