assign boundaries for local search #63

Merged
anomalist merged 18 commits from search-bbox into master 1 year ago
  1. 3
      README.md
  2. 33598
      package-lock.json
  3. 2
      package.json
  4. 10
      project.task.yaml
  5. 47
      src/components/AlertMessage.vue
  6. 2
      src/components/EntityIcon.vue
  7. 12
      src/components/GiftedDialog.vue
  8. 11
      src/db/tables/settings.ts
  9. 221
      src/libs/endorserServer.ts
  10. 23
      src/router/index.ts
  11. 104
      src/views/AccountViewView.vue
  12. 32
      src/views/ContactAmountsView.vue
  13. 130
      src/views/ContactGiftingView.vue
  14. 15
      src/views/ContactQRScanShowView.vue
  15. 88
      src/views/ContactsView.vue
  16. 315
      src/views/DiscoverView.vue
  17. 2
      src/views/HelpView.vue
  18. 119
      src/views/HomeView.vue
  19. 73
      src/views/IdentitySwitcherView.vue
  20. 2
      src/views/ImportAccountView.vue
  21. 12
      src/views/ImportDerivedAccountView.vue
  22. 2
      src/views/NewEditAccountView.vue
  23. 43
      src/views/NewEditProjectView.vue
  24. 4
      src/views/NewIdentifierView.vue
  25. 107
      src/views/ProjectViewView.vue
  26. 46
      src/views/ProjectsView.vue
  27. 30
      src/views/SeedBackupView.vue
  28. 2
      src/views/StartView.vue
  29. 42
      src/views/StatisticsView.vue
  30. 80
      tsconfig.json

3
README.md

@ -99,6 +99,9 @@ See https://tea.xyz
### Reference Material
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
```
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83

33598
package-lock.json

File diff suppressed because it is too large

2
package.json

@ -52,11 +52,11 @@
"vue": "^3.3.4",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "^2.1.20",
"vue-property-decorator": "^9.1.2",
"vue-router": "^4.2.3",
"web-did-resolver": "^2.0.27"
},
"devDependencies": {
"@types/leaflet": "^1.9.4",
"@types/ramda": "^0.29.3",
"@types/three": "^0.152.1",
"@typescript-eslint/eslint-plugin": "^5.61.0",

10
project.task.yaml

@ -1,14 +1,17 @@
tasks:
- fix "any" warnings
- fix missing updateAllFeed in ContactGiftingView page
- check that Anonymous users jdenticon are nulls (not "Anonymous")
- test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView)
- .2 bug - on contacts view, click on "to" & "from" and nothing happens
- 40 notifications :
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
- 01 add my bounding box(es) of interest for searches on Nearby part of Discovery page
- .5 search by a bounding box(s) of interest for local projects (see API by clicking on "Nearby")
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
- 01 fix the Discovery map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
- 08 Scan QR code to import into contacts assignee:matthew
- SEE: https://github.com/gruhn/vue-qrcode-reader
@ -24,6 +27,7 @@ tasks:
- 24 Move to Vite assignee:matthew
- .2 Edit Plan does not have icons across the bottom assignee-group:ui
- .5 include the hash of the latest commit, and maybe a version
- .5 add link to further project / people when a project pays ahead
- .5 add project ID to the URL, to make a project publicly-accessible
@ -102,6 +106,8 @@ tasks:
- 40 notifications v+ :
- pull, w/ scheduled runs
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
log:
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27

47
src/components/AlertMessage.vue

@ -1,47 +0,0 @@
<template>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-amber-400 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
@Component
export default class AlertMessage extends Vue {
@Prop alertTitle = "";
@Prop alertMessage = "";
isAlertVisible = this.alertMessage;
public onClickClose() {
this.isAlertVisible = false;
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-amber-200": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

2
src/components/EntityIcon.vue

@ -8,7 +8,7 @@ import { toSvg } from "jdenticon";
@Component
export default class EntityIcon extends Vue {
@Prop entityId = "";
@Prop iconSize = "";
@Prop iconSize = 0;
generateIdenticon() {
const svgString = toSvg(this.entityId, this.iconSize);

12
src/components/GiftedDialog.vue

@ -52,18 +52,18 @@
<script lang="ts">
import { Vue, Component, Prop, Emit } from "vue-facing-decorator";
import { GiverInputInfo, GiverOutputInfo } from "@/libs/endorserServer";
@Component
export default class GiftedDialog extends Vue {
@Prop message = "";
giver = null;
giver?: GiverInputInfo;
description = "";
hours = "0";
visible = false;
open(giver) {
// giver: GiverInputInfo
open(giver: GiverInputInfo) {
this.giver = giver;
this.visible = true;
}
@ -81,7 +81,7 @@ export default class GiftedDialog extends Vue {
}
@Emit("dialog-result")
confirm() {
confirm(): GiverOutputInfo {
const result = {
action: "confirm",
giver: this.giver,
@ -90,14 +90,14 @@ export default class GiftedDialog extends Vue {
};
this.close();
this.description = "";
this.giver = null;
this.giver = undefined;
this.hours = "0";
return result;
}
@Emit("dialog-result")
cancel() {
cancel(): GiverOutputInfo {
const result = { action: "cancel" };
this.close();
return result;

11
src/db/tables/settings.ts

@ -1,3 +1,10 @@
export type BoundingBox = {
eastLong: number;
maxLat: number;
minLat: number;
westLong: number;
};
// a singleton
export type Settings = {
id: number; // there's only one entry: MASTER_SETTINGS_KEY
@ -7,6 +14,10 @@ export type Settings = {
firstName?: string;
lastName?: string;
lastViewedClaimId?: string;
searchBoxes?: Array<{
name: string;
bbox: BoundingBox;
}>;
showContactGivesInline?: boolean;
};

221
src/libs/endorserServer.ts

@ -21,6 +21,13 @@ export interface GiverInputInfo {
name?: string;
}
export interface GiverOutputInfo {
action: string;
giver?: GiverInputInfo;
description?: string;
hours?: number;
}
export interface ClaimResult {
success: { claimId: string; handleId: string };
error: { code: string; message: string };
@ -55,7 +62,30 @@ export interface GiveVerifiableCredential {
fulfills?: { "@type": string; identifier: string };
identifier?: string;
object?: { amountOfThisGood: number; unitCode: string };
recipient: { identifier: string };
recipient?: { identifier: string };
}
export interface PlanVerifiableCredential {
"@context": "https://schema.org";
"@type": "PlanAction";
name: string;
description: string;
identifier?: string;
location?: {
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
};
}
export interface PlanServerRecord {
agentDid?: string; // optional, if the issuer wants someone else to manage as well
description: string;
endTime?: string;
issuerDid: string;
handleId: string;
locLat?: number;
locLon?: number;
startTime?: string;
url?: string;
}
export interface RegisterVerifiableCredential {
@ -63,7 +93,7 @@ export interface RegisterVerifiableCredential {
"@type": string;
agent: { identifier: string };
object: string;
recipient: { identifier: string };
participant: { identifier: string };
}
export interface InternalError {
@ -75,7 +105,7 @@ export interface InternalError {
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN";
export function isHiddenDid(did) {
export function isHiddenDid(did: string) {
return did === HIDDEN_DID;
}
@ -88,7 +118,7 @@ export function didInfo(
allMyDids: Array<string>,
contacts: Array<Contact>,
): string {
const myId: string | undefined = R.find(R.equals(did), allMyDids, did);
const myId: string | undefined = R.find(R.equals(did), allMyDids);
if (myId) {
return "You" + (myId !== activeDid ? " (Alt ID)" : "");
} else {
@ -105,6 +135,22 @@ export function didInfo(
}
}
export interface ResultWithType {
type: string;
}
export interface SuccessResult extends ResultWithType {
type: "success";
response: AxiosResponse<ClaimResult>;
}
export interface ErrorResult {
type: "error";
error: InternalError;
}
export type CreateAndSubmitGiveResult = SuccessResult | ErrorResult;
/**
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
*
@ -118,76 +164,115 @@ export async function createAndSubmitGive(
axios: Axios,
apiServer: string,
identity: IIdentifier,
fromDid: string,
toDid: string,
description: string,
hours: number,
fromDid?: string,
toDid?: string,
description?: string,
hours?: number,
fulfillsProjectHandleId?: string,
): Promise<AxiosResponse<ClaimResult> | InternalError> {
// Make a claim
const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org",
"@type": "GiveAction",
recipient: { identifier: toDid },
};
if (fromDid) {
vcClaim.agent = { identifier: fromDid };
}
if (description) {
vcClaim.description = description;
}
if (hours) {
vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
}
if (fulfillsProjectHandleId) {
vcClaim.fulfills = {
"@type": "PlanAction",
identifier: fulfillsProjectHandleId,
): Promise<CreateAndSubmitGiveResult> {
try {
// Make a claim
const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org",
"@type": "GiveAction",
};
}
// Make a payload for the claim
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// Create a signature using private key of identity
if (identity.keys[0].privateKeyHex == null) {
return new Promise<InternalError>((resolve, reject) => {
reject({
if (toDid) {
vcClaim.recipient = { identifier: toDid };
}
if (fromDid) {
vcClaim.agent = { identifier: fromDid };
}
if (description) {
vcClaim.description = description;
}
if (hours) {
vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
}
if (fulfillsProjectHandleId) {
vcClaim.fulfills = {
"@type": "PlanAction",
identifier: fulfillsProjectHandleId,
};
}
// Make a payload for the claim
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// Create a signature using private key of identity
const firstKey = identity.keys[0];
if (!firstKey || !firstKey.privateKeyHex) {
throw {
error: "No private key",
message:
"Your identifier " +
identity.did +
" is not configured correctly. Use a different identifier.",
});
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
};
}
const privateKeyHex = firstKey.privateKeyHex;
if (!privateKeyHex) {
throw {
error: "No private key",
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
};
}
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
// Make the xhr request payload
return axios.post(url, payload, { headers });
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const response = await axios.post(url, payload, { headers });
return {
type: "success",
response,
};
} catch (error: unknown) {
let errorMessage: string;
if (error instanceof Error) {
// If it's a JavaScript Error object
errorMessage = error.message;
} else if (
typeof error === "object" &&
error !== null &&
"message" in error
) {
// If it's an object that has a 'message' property
errorMessage = (error as { message: string }).message;
} else {
// Unknown error shape, default message
errorMessage = "Unknown error";
}
return {
type: "error",
error: {
error: errorMessage,
userMessage: "Failed to create and submit the claim.",
},
};
}
}
// from https://stackoverflow.com/a/175787/845494

23
src/router/index.ts

@ -1,5 +1,11 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { accountsDB } from "@/db";
import {
createRouter,
createWebHistory,
NavigationGuardNext,
RouteLocationNormalized,
RouteRecordRaw,
} from "vue-router";
import { accountsDB } from "@/db/index";
/**
*
@ -7,7 +13,11 @@ import { accountsDB } from "@/db";
* @param from :RouteLocationNormalized
* @param next :NavigationGuardNext
*/
const enterOrStart = async (to, from, next) => {
const enterOrStart = async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext,
) => {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
@ -190,7 +200,12 @@ const router = createRouter({
routes,
});
const errorHandler = (error, to, from) => {
const errorHandler = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any,
to: RouteLocationNormalized,
from: RouteLocationNormalized,
) => {
// Handle the error here
console.error("Caught in top level error handler:", error, to, from);

104
src/views/AccountViewView.vue

@ -170,6 +170,33 @@
</button>
</dialog>
<div class="flex py-2">
<button class="text-center text-md text-blue-500" @click="checkLimits()">
Check Limits
</button>
<!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="ml-2">
Checking... <fa icon="spinner" class="fa-spin"></fa>
</div>
<div class="ml-2">
{{ limitsMessage }}
</div>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<span class="font-bold">Rate Limits</span>
<p>
You have done {{ limits.doneClaimsThisWeek }} claims out of
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
</p>
<p>
You have done {{ limits.doneRegistrationsThisMonth }} registrations
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
registrations counter resets at
{{ readableTime(limits.nextMonthBeginDateTime) }}
</p>
</div>
</div>
<h3
class="text-sm uppercase font-semibold mb-3"
@click="showAdvanced = !showAdvanced"
@ -202,36 +229,6 @@
<div class="ml-2">Show amounts given with contacts</div>
</label>
<div class="flex py-2">
<button
class="text-center text-md text-blue-500"
@click="checkLimits()"
>
Check Limits
</button>
<!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="ml-2">
Checking... <fa icon="spinner" class="fa-spin"></fa>
</div>
<div class="ml-2">
{{ limitsMessage }}
</div>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<span class="font-bold">Rate Limits</span>
<p>
You have done {{ limits.doneClaimsThisWeek }} claims out of
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
</p>
<p>
You have done {{ limits.doneRegistrationsThisMonth }} registrations
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
registrations counter resets at
{{ readableTime(limits.nextMonthBeginDateTime) }}
</p>
</div>
</div>
<div class="flex py-2">
<router-link
:to="{ name: 'identity-switcher' }"
@ -286,32 +283,37 @@
</button>
</div>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { AxiosError } from "axios/index";
import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db";
import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { AxiosError } from "axios/index";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import QuickNav from "@/components/QuickNav.vue";
import { IIdentifier } from "@veramo/core";
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
@Component({ components: { AlertMessage, QuickNav } })
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } })
export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
Constants = AppString;
activeDid = "";
@ -337,7 +339,7 @@ export default class AccountViewView extends Vue {
alertMessage = "";
alertTitle = "";
public async getIdentity(activeDid) {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
@ -347,7 +349,7 @@ export default class AccountViewView extends Vue {
return identity;
}
public async getHeaders(identity) {
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@ -357,7 +359,7 @@ export default class AccountViewView extends Vue {
}
// call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text, fn) {
doCopyTwoSecRedo(text: string, fn: () => void) {
fn();
useClipboard()
.copy(text)
@ -409,7 +411,8 @@ export default class AccountViewView extends Vue {
});
this.checkLimitsFor(identity);
}
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
if (
err.message ===
"Attempted to load account records with no identity available."
@ -512,7 +515,8 @@ export default class AccountViewView extends Vue {
if (resp.status === 200) {
this.limits = resp.data;
}
} catch (error: unknown) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if (
error.message ===
"Attempted to load Give records with no identity available."
@ -523,13 +527,9 @@ export default class AccountViewView extends Vue {
const serverError = error as AxiosError;
console.error("Bad response retrieving limits: ", serverError);
const data: ErrorResponse | undefined =
serverError.response && serverError.response.data;
if (data && data.error && data.error.message) {
this.limitsMessage = data.error.message;
} else {
this.limitsMessage = "Bad server response.";
}
const data = (serverError.response &&
serverError.response.data) as ErrorResponse;
this.limitsMessage = data?.error?.message || "Bad server response.";
}
}
@ -579,7 +579,7 @@ export default class AccountViewView extends Vue {
this.apiServer = this.apiServerInput;
}
setApiServerInput(value) {
setApiServerInput(value: string) {
this.apiServerInput = value;
}
}

32
src/views/ContactAmountsView.vue

@ -90,10 +90,6 @@
</tr>
</tbody>
</table>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@ -101,7 +97,7 @@
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
@ -113,17 +109,24 @@ import {
} from "@/libs/endorserServer";
import * as didJwt from "did-jwt";
import { AxiosError } from "axios";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import QuickNav from "@/components/QuickNav.vue";
import { IIdentifier } from "@veramo/core";
@Component({ components: { AlertMessage, QuickNav } })
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } })
export default class ContactsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = "";
apiServer = "";
contact: Contact | null = null;
giveRecords: Array<GiveServerRecord> = [];
alertTitle = "";
alertMessage = "";
numAccounts = 0;
async beforeCreate() {
@ -131,7 +134,7 @@ export default class ContactsView extends Vue {
this.numAccounts = await accountsDB.accounts.count();
}
public async getIdentity(activeDid) {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
@ -147,7 +150,7 @@ export default class ContactsView extends Vue {
return identity;
}
public async getHeaders(identity) {
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@ -169,7 +172,8 @@ export default class ContactsView extends Vue {
if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact);
}
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
this.$notify(
{
group: "alert",
@ -187,7 +191,7 @@ export default class ContactsView extends Vue {
async loadGives(activeDid: string, contact: Contact) {
try {
const identity = await this.getIdentity(this.activeDid);
let result = [];
let result: Array<GiveServerRecord> = [];
const url =
this.apiServer +
"/api/v2/report/gives?agentDid=" +

130
src/views/ContactGiftingView.vue

@ -76,48 +76,53 @@
message="Received from"
>
</GiftedDialog>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db";
import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { createAndSubmitGive } from "@/libs/endorserServer";
import { Account } from "@/db/tables/accounts";
import {
createAndSubmitGive,
CreateAndSubmitGiveResult,
ErrorResult,
GiverInputInfo,
GiverOutputInfo,
} from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { IIdentifier } from "@veramo/core";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
components: { GiftedDialog, QuickNav, EntityIcon },
})
export default class HomeView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = "";
allAccounts: Array<Account> = [];
allContacts: Array<Contact> = [];
apiServer = "";
isHiddenSpinner = true;
alertTitle = "";
alertMessage = "";
accounts: AccountsSchema;
accounts: typeof AccountsSchema;
numAccounts = 0;
async beforeCreate() {
accountsDB.open();
this.accounts = accountsDB.accounts;
this.numAccounts = await this.accounts.count();
this.numAccounts = await accountsDB.accounts.count();
}
public async getIdentity(activeDid) {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
@ -133,7 +138,7 @@ export default class HomeView extends Vue {
return identity;
}
public async getHeaders(identity) {
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@ -144,23 +149,20 @@ export default class HomeView extends Vue {
async created() {
try {
await accountsDB.open();
this.allAccounts = await accountsDB.accounts.toArray();
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId;
this.updateAllFeed();
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
err.userMessage ||
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
},
-1,
@ -168,37 +170,20 @@ export default class HomeView extends Vue {
}
}
public async buildHeaders() {
const headers = { "Content-Type": "application/json" };
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
openDialog(giver) {
this.$refs.customDialog.open(giver);
openDialog(giver: GiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(giver);
}
handleDialogResult(result) {
handleDialogResult(result: GiverOutputInfo) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(result.contact?.did, result.description, result.hours);
resolve();
this.recordGive(
result.giver?.did,
result.description,
result.hours,
).then(() => {
resolve(null);
});
});
} else {
// action was "cancel" so do nothing
@ -211,7 +196,11 @@ export default class HomeView extends Vue {
* @param description may be an empty string
* @param hours may be 0
*/
public async recordGive(giverDid, description, hours) {
public async recordGive(
giverDid?: string,
description?: string,
hours?: number,
) {
if (!this.activeDid) {
this.$notify(
{
@ -250,8 +239,8 @@ export default class HomeView extends Vue {
hours,
);
if (isGiveCreationError(result)) {
const errorMessage = getGiveCreationErrorMessage(result);
if (this.isGiveCreationError(result)) {
const errorMessage = this.getGiveCreationErrorMessage(result);
console.log("Error with give result:", result);
this.$notify(
{
@ -273,39 +262,34 @@ export default class HomeView extends Vue {
-1,
);
}
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log("Error with give caught:", error);
const message =
error.userMessage ||
error.response?.data?.error?.message ||
"There was an error recording the Give.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
getGiveErrorMessage(error) ||
"There was an error recording the give.",
text: message,
},
-1,
);
}
}
private setAlert(title, message) {
this.alertTitle = title;
this.alertMessage = message;
}
// Helper functions for readability
isGiveCreationError(result) {
return result.status !== 201 || result.data?.error;
}
getGiveCreationErrorMessage(result) {
return result.data?.error?.message;
isGiveCreationError(result: CreateAndSubmitGiveResult) {
return result.type == "error";
}
getGiveErrorMessage(error) {
return error.userMessage || error.response?.data?.error?.message;
getGiveCreationErrorMessage(result: CreateAndSubmitGiveResult) {
return (result as ErrorResult).error?.userMessage;
}
}
</script>

15
src/views/ContactQRScanShowView.vue

@ -23,17 +23,24 @@
<script lang="ts">
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import * as R from "ramda";
import { SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import QuickNav from "@/components/QuickNav";
import QuickNav from "@/components/QuickNav.vue";
import { Account } from "@/db/tables/accounts";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({
components: {
QRCodeVue3,
@ -41,11 +48,13 @@ const Buffer = require("buffer/").Buffer;
},
})
export default class ContactQRScanShow extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = "";
apiServer = "";
qrValue = "";
public async getIdentity(activeDid) {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account: Account | undefined = R.find(

88
src/views/ContactsView.vue

@ -121,19 +121,21 @@
</button>
<button
v-if="contact.registered"
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
title="Registered"
>
<fa icon="person-circle-check" class="fa-fw" />
</button>
<button
v-else
@click="register(contact)"
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
title="Registration unknown"
>
<fa icon="person-circle-question" class="fa-fw" />
<fa
v-if="contact.registered"
icon="person-circle-check"
class="fa-fw"
title="Registered"
/>
<fa
v-else
icon="person-circle-question"
class="fa-fw"
title="Registration Unknown"
/>
</button>
<button
@ -202,10 +204,6 @@
</li>
</ul>
<p v-else>This identity has no contacts.</p>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@ -214,27 +212,36 @@ import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { accountsDB, db } from "@/db";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import {
GiveServerRecord,
GiveVerifiableCredential,
RegisterVerifiableCredential,
SERVICE_ID,
} from "@/libs/endorserServer";
import { Component, Vue } from "vue-facing-decorator";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({
components: { AlertMessage, QuickNav, EntityIcon },
components: { QuickNav, EntityIcon },
})
export default class ContactsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = "";
apiServer = "";
contacts: Array<Contact> = [];
@ -256,8 +263,6 @@ export default class ContactsView extends Vue {
showGiveNumbers = false;
showGiveTotals = true;
showGiveConfirmed = true;
alertTitle = "";
alertMessage = "";
async created() {
await db.open();
@ -276,7 +281,7 @@ export default class ContactsView extends Vue {
);
}
public async getIdentity(activeDid) {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
@ -290,7 +295,7 @@ export default class ContactsView extends Vue {
return identity;
}
public async getHeaders(identity) {
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@ -299,7 +304,7 @@ export default class ContactsView extends Vue {
return headers;
}
public async getHeadersAndIdentity(activeDid) {
public async getHeadersAndIdentity(activeDid: string) {
const identity = await this.getIdentity(activeDid);
const headers = await this.getHeaders(identity);
@ -308,11 +313,11 @@ export default class ContactsView extends Vue {
async loadGives() {
const handleResponse = (
resp,
descriptions,
confirmed,
unconfirmed,
useRecipient,
resp: { status: number; data: { data: GiveServerRecord[] } },
descriptions: Record<string, string>,
confirmed: Record<string, number>,
unconfirmed: Record<string, number>,
useRecipient: boolean,
) => {
if (resp.status === 200) {
const allData = resp.data.data;
@ -344,9 +349,8 @@ export default class ContactsView extends Vue {
title: "Error With Server",
text:
"Got an error retrieving your " +
resp.config.url.includes("recipientDid")
? "received"
: "given" + " time from the server.",
(useRecipient ? "given" : "received") +
" time from the server.",
},
-1,
);
@ -457,6 +461,9 @@ export default class ContactsView extends Vue {
confirm(
"Are you sure you want to use one of your registrations for " +
this.nameForDid(this.contacts, contact.did) +
(contact.registered
? " -- especially since they are already marked as registered"
: "") +
"?",
)
) {
@ -608,6 +615,8 @@ export default class ContactsView extends Vue {
this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did);
const identity = await this.getIdentity(this.activeDid);
const headers = await this.getHeaders(identity);
try {
const resp = await this.axios.get(url, { headers });
@ -685,11 +694,17 @@ export default class ContactsView extends Vue {
// if they have unconfirmed amounts, ask to confirm those first
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
const isare = this.givenToMeUnconfirmed[fromDid] == 1 ? "is" : "are";
const hours = this.givenToMeUnconfirmed[fromDid] == 1 ? "hour" : "hours";
if (
confirm(
"There are " +
"There " +
isare +
" " +
this.givenToMeUnconfirmed[fromDid] +
" unconfirmed hours from them." +
" unconfirmed " +
hours +
" from them." +
" Would you like to confirm some of those hours?",
)
) {
@ -697,6 +712,7 @@ export default class ContactsView extends Vue {
name: "contact-amounts",
query: { contactDid: fromDid },
});
return;
}
}
if (!this.isNumeric(this.hourInput)) {
@ -747,7 +763,9 @@ export default class ContactsView extends Vue {
confirm(
"Are you sure you want to record " +
this.hourInput +
" hours " +
" hour" +
(this.hourInput == "1" ? "" : "s") +
" " +
toFrom +
description +
"?",

315
src/views/DiscoverView.vue

@ -9,7 +9,7 @@
</h1>
<!-- Quick Search -->
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="search()">
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchAll()">
<input
type="text"
v-model="searchTerms"
@ -17,7 +17,7 @@
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
/>
<button
@click="search()"
@click="searchAll()"
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
@ -32,6 +32,8 @@
href="#"
@click="
projects = [];
isLocalActive = true;
isRemoteActive = false;
searchLocal();
"
v-bind:class="computedLocalTabClassNames()"
@ -49,7 +51,9 @@
v-bind:class="computedRemoteTabClassNames()"
@click="
projects = [];
search();
isRemoteActive = true;
isLocalActive = false;
searchAll();
"
>
Remote
@ -62,6 +66,49 @@
</ul>
</div>
<div v-if="isLocalActive">
<div v-if="!isChoosingSearchBox">
<button
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="isChoosingSearchBox = true"
>
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
</button>
</div>
<div v-else>
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
Choose Location Below for Nearby Search
</button>
<button
v-if="isNewMarkerSet"
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="storeSearchBox"
>
Store This Location for Nearby Search
</button>
<button
v-if="searchBox"
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="forgetSearchBox"
>
Delete Stored Location
</button>
<button
v-if="isNewMarkerSet"
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="resetLatLong"
>
Reset Marker
</button>
<button
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="cancelSearchBoxSelect"
>
Cancel
</button>
</div>
</div>
<!-- Loading Animation -->
<div
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
@ -71,7 +118,7 @@
</div>
<!-- Results List -->
<InfiniteScroll @reached-bottom="loadMoreData">
<InfiniteScroll @reached-bottom="loadMoreData" v-if="!isChoosingSearchBox">
<ul>
<li
class="border-b border-slate-300"
@ -103,42 +150,103 @@
</li>
</ul>
</InfiniteScroll>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
<div
v-if="isLocalActive && isChoosingSearchBox"
style="height: 600px; width: 800px"
>
<l-map
ref="map"
:center="[localCenterLat, localCenterLong]"
v-model:zoom="localZoom"
@click="setMapPoint"
>
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layer-type="base"
name="OpenStreetMap"
/>
<l-marker
v-if="isNewMarkerSet"
:lat-lng="[localCenterLat, localCenterLong]"
@click="isNewMarkerSet = false"
/>
<l-rectangle
v-if="isNewMarkerSet"
:bounds="[
[localCenterLat - localLatDiff, localCenterLong - localLongDiff],
[localCenterLat + localLatDiff, localCenterLong + localLongDiff],
]"
:weight="1"
/>
</l-map>
</div>
</section>
</template>
<script lang="ts">
import { LeafletMouseEvent } from "leaflet";
import "leaflet/dist/leaflet.css";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import {
LMap,
LMarker,
LRectangle,
LTileLayer,
} from "@vue-leaflet/vue-leaflet";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { didInfo } from "@/libs/endorserServer";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import InfiniteScroll from "@/components/InfiniteScroll";
import EntityIcon from "@/components/EntityIcon";
import { didInfo, ProjectData } from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import EntityIcon from "@/components/EntityIcon.vue";
const DEFAULT_LAT_LONG_DIFF = 0.01;
const WORLD_ZOOM = 2;
const DEFAULT_ZOOM = 2;
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({
components: { AlertMessage, QuickNav, InfiniteScroll, EntityIcon },
components: {
LRectangle,
QuickNav,
InfiniteScroll,
EntityIcon,
LMap,
LMarker,
LTileLayer,
},
})
export default class DiscoverView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = "";
searchTerms = "";
alertMessage = "";
alertTitle = "";
projects: ProjectData[] = [];
isChoosingSearchBox = false;
isLocalActive = true;
isRemoteActive = false;
isNewMarkerSet = false;
localCenterLat = 0;
localCenterLong = 0;
localLatDiff = DEFAULT_LAT_LONG_DIFF;
localLongDiff = DEFAULT_LAT_LONG_DIFF;
localCount = 0;
localZoom = DEFAULT_ZOOM;
remoteCount = 0;
searchBox: { name: string; bbox: BoundingBox } | null = null;
isLoading = false;
// make this function available to the Vue template
@ -149,6 +257,9 @@ export default class DiscoverView extends Vue {
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.searchBox = settings?.searchBoxes?.[0] || null;
this.resetLatLong();
this.allContacts = await db.contacts.toArray();
await accountsDB.open();
@ -158,8 +269,10 @@ export default class DiscoverView extends Vue {
this.searchLocal();
}
public async buildHeaders() {
const headers = { "Content-Type": "application/json" };
public async buildHeaders(): Promise<HeadersInit> {
const headers: HeadersInit = {
"Content-Type": "application/json",
};
if (this.activeDid) {
await accountsDB.open();
@ -180,16 +293,13 @@ export default class DiscoverView extends Vue {
return headers;
}
public async search(beforeId?: string) {
public async searchAll(beforeId?: string) {
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
if (beforeId) {
queryParams = queryParams + `&beforeId=${beforeId}`;
}
this.isRemoteActive = true;
this.isLocalActive = false;
try {
this.isLoading = true;
const response = await fetch(
@ -202,12 +312,13 @@ export default class DiscoverView extends Vue {
if (response.status !== 200) {
const details = await response.text();
console.log("Problem with full search:", details);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `There was a problem accessing the server. Please try again later. (${details})`,
text: `There was a problem accessing the server. Try again later.`,
},
-1,
);
@ -220,14 +331,15 @@ export default class DiscoverView extends Vue {
const plans: ProjectData[] = results.data;
if (plans) {
for (const plan of plans) {
const { name, description, handleId, rowid, issuerDid } = plan;
this.projects.push({ name, description, handleId, rowid, issuerDid });
const { name, description, handleId, rowid } = plan;
this.projects.push({ name, description, handleId, rowid });
}
this.remoteCount = this.projects.length;
} else {
throw JSON.stringify(results);
}
} catch (e) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
console.log("Error with feed load:", e);
this.$notify(
{
@ -244,14 +356,19 @@ export default class DiscoverView extends Vue {
}
public async searchLocal(beforeId?: string) {
if (!this.searchBox) {
this.projects = [];
return;
}
const claimContents =
"claimContents=" + encodeURIComponent(this.searchTerms);
let queryParams = [
claimContents,
"minLocLat=40.901000",
"maxLocLat=40.904000",
"westLocLon=-111.914000",
"eastLocLon=-111.909000",
"minLocLat=" + this.searchBox.bbox.minLat,
"maxLocLat=" + this.searchBox.bbox.maxLat,
"westLocLon=" + this.searchBox.bbox.westLong,
"eastLocLon=" + this.searchBox.bbox.eastLong,
].join("&");
if (beforeId) {
@ -260,8 +377,6 @@ export default class DiscoverView extends Vue {
try {
this.isLoading = true;
this.isLocalActive = true;
this.isRemoteActive = false;
const response = await fetch(
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
{
@ -272,12 +387,13 @@ export default class DiscoverView extends Vue {
if (response.status !== 200) {
const details = await response.text();
console.log("Problem with nearby search:", details);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `There was a problem accessing the server. Please try again later. (${details})`,
text: "There was a problem accessing the server. Try again later.",
},
-1,
);
@ -302,7 +418,8 @@ export default class DiscoverView extends Vue {
} else {
throw JSON.stringify(results);
}
} catch (e) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
console.log("Error with feed load:", e);
this.$notify(
{
@ -328,7 +445,7 @@ export default class DiscoverView extends Vue {
if (this.isLocalActive) {
this.searchLocal(latestProject["rowid"]);
} else if (this.isRemoteActive) {
this.search(latestProject["rowid"]);
this.searchAll(latestProject["rowid"]);
}
}
}
@ -345,6 +462,128 @@ export default class DiscoverView extends Vue {
this.$router.push(route);
}
setMapPoint(event: LeafletMouseEvent) {
if (this.isNewMarkerSet) {
this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat);
this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong);
} else {
// marker is not set
this.localCenterLat = event.latlng.lat;
this.localCenterLong = event.latlng.lng;
let latDiff = DEFAULT_LAT_LONG_DIFF;
let longDiff = DEFAULT_LAT_LONG_DIFF;
// Guess at a size for the bounding box.
// This doesn't seem like the right approach but it's the only way I can find to get the screen bounds.
const bounds = event.target.boxZoom?._map?.getBounds();
if (bounds) {
latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8;
longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8;
}
this.localLatDiff = latDiff;
this.localLongDiff = longDiff;
this.isNewMarkerSet = true;
}
}
public resetLatLong() {
if (this.searchBox?.bbox) {
const bbox = this.searchBox.bbox;
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2;
this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2;
this.localZoom = WORLD_ZOOM;
this.isNewMarkerSet = true;
} else {
this.isNewMarkerSet = false;
}
}
public async storeSearchBox() {
if (this.localCenterLong || this.localCenterLat) {
try {
const newSearchBox = {
name: "Local",
bbox: {
eastLong: this.localCenterLong + this.localLongDiff,
maxLat: this.localCenterLat + this.localLatDiff,
minLat: this.localCenterLat - this.localLatDiff,
westLong: this.localCenterLong - this.localLongDiff,
},
};
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: [newSearchBox],
});
this.searchBox = newSearchBox;
this.isChoosingSearchBox = false;
this.searchLocal();
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Search Settings",
text: "Try going to a different page and then coming back.",
},
-1,
);
console.error(
"Telling user to retry the location search setting because:",
err,
);
}
} else {
this.$notify(
{
group: "alert",
type: "warning",
title: "No Location Selected",
text: "Select a location on the map.",
},
-1,
);
}
}
public async forgetSearchBox() {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: [],
});
this.searchBox = null;
this.localCenterLat = 0;
this.localCenterLong = 0;
this.localLatDiff = DEFAULT_LAT_LONG_DIFF;
this.localLongDiff = DEFAULT_LAT_LONG_DIFF;
this.localZoom = DEFAULT_ZOOM;
this.isChoosingSearchBox = false;
this.isNewMarkerSet = false;
this.searchLocal();
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Search Settings",
text: "Try going to a different page and then coming back.",
},
-1,
);
console.error(
"Telling user to retry the location search setting because:",
err,
);
}
}
public cancelSearchBoxSelect() {
this.isChoosingSearchBox = false;
this.localZoom = WORLD_ZOOM;
}
public computedLocalTabClassNames() {
return {
"inline-block": true,

2
src/views/HelpView.vue

@ -181,7 +181,7 @@
<script lang="ts">
import * as Package from "../../package.json";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav";
import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { QuickNav } })
export default class Help extends Vue {

119
src/views/HomeView.vue

@ -196,40 +196,49 @@
</li>
</ul>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db";
import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
import {
createAndSubmitGive,
didInfo,
GiverInputInfo,
GiverOutputInfo,
GiveServerRecord,
} from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { IIdentifier } from "@veramo/core";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
components: { GiftedDialog, QuickNav, EntityIcon },
})
export default class HomeView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = "";
feedAllLoaded = false;
feedData = [];
feedPreviousOldestId = null;
feedLastViewedId = null;
feedPreviousOldestId?: string;
feedLastViewedId?: string;
isHiddenSpinner = true;
alertTitle = "";
alertMessage = "";
numAccounts = 0;
async beforeCreate() {
@ -237,7 +246,7 @@ export default class HomeView extends Vue {
this.numAccounts = await accountsDB.accounts.count();
}
public async getIdentity(activeDid) {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
@ -253,7 +262,7 @@ export default class HomeView extends Vue {
return identity;
}
public async getHeaders(identity) {
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@ -275,7 +284,8 @@ export default class HomeView extends Vue {
this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId;
this.updateAllFeed();
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
this.$notify(
{
group: "alert",
@ -291,7 +301,9 @@ export default class HomeView extends Vue {
}
public async buildHeaders() {
const headers = { "Content-Type": "application/json" };
const headers: HeadersInit = {
"Content-Type": "application/json",
};
if (this.activeDid) {
await accountsDB.open();
@ -314,7 +326,7 @@ export default class HomeView extends Vue {
public async updateAllFeed() {
this.isHiddenSpinner = false;
await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId)
await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId)
.then(async (results) => {
if (results.data.length > 0) {
this.feedData = this.feedData.concat(results.data);
@ -349,7 +361,7 @@ export default class HomeView extends Vue {
this.isHiddenSpinner = true;
}
public async retrieveClaims(endorserApiServer, identifier, beforeId) {
public async retrieveClaims(endorserApiServer: string, beforeId?: string) {
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const response = await fetch(
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
@ -372,13 +384,13 @@ export default class HomeView extends Vue {
}
}
giveDescription(giveRecord) {
let claim = giveRecord.fullClaim;
if (claim.claim) {
claim = claim.claim;
}
giveDescription(giveRecord: GiveServerRecord) {
// claim.claim happen for some claims wrapped in a Verifiable Credential
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim;
// agent.did is for legacy data, before March 2023
const giverDid = claim.agent?.identifier || claim.agent?.did;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const giverDid = claim.agent?.identifier || (claim.agent as any)?.did;
const giverInfo = didInfo(
giverDid,
this.activeDid,
@ -389,7 +401,9 @@ export default class HomeView extends Vue {
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: claim.description || "something unknown";
// recipient.did is for legacy data, before March 2023
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
const gaveRecipientId =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
claim.recipient?.identifier || (claim.recipient as any)?.did;
const gaveRecipientInfo = gaveRecipientId
? " to " +
didInfo(
@ -402,23 +416,28 @@ export default class HomeView extends Vue {
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
}
displayAmount(code, amt) {
displayAmount(code: string, amt: number) {
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
}
currencyShortWordForCode(unitCode, single) {
currencyShortWordForCode(unitCode: string, single: boolean) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
}
openDialog(giver) {
this.$refs.customDialog.open(giver);
openDialog(giver: GiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(giver);
}
handleDialogResult(result) {
handleDialogResult(result: GiverOutputInfo) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(result.giver?.did, result.description, result.hours);
resolve();
this.recordGive(
result.giver?.did,
result.description,
result.hours,
).then(() => {
resolve(null);
});
});
} else {
// action was "cancel" so do nothing
@ -431,7 +450,11 @@ export default class HomeView extends Vue {
* @param description may be an empty string
* @param hours may be 0
*/
public async recordGive(giverDid, description, hours) {
public async recordGive(
giverDid?: string,
description?: string,
hours?: number,
) {
if (!this.activeDid) {
this.$notify(
{
@ -493,39 +516,35 @@ export default class HomeView extends Vue {
-1,
);
}
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log("Error with give caught:", error);
const message =
error.userMessage ||
error.response?.data?.error?.message ||
"There was an error recording the give.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
this.getGiveErrorMessage(error) ||
"There was an error recording the give.",
text: message,
},
-1,
);
}
}
private setAlert(title, message) {
this.alertTitle = title;
this.alertMessage = message;
}
// Helper functions for readability
isGiveCreationError(result) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isGiveCreationError(result: any) {
return result.status !== 201 || result.data?.error;
}
getGiveCreationErrorMessage(result) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getGiveCreationErrorMessage(result: any) {
return result.data?.error?.message;
}
getGiveErrorMessage(error) {
return error.userMessage || error.response?.data?.error?.message;
}
}
</script>

73
src/views/IdentitySwitcherView.vue

@ -62,34 +62,38 @@
>
No Identity
</a>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db";
import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import QuickNav from "@/components/QuickNav.vue";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { AlertMessage, QuickNav } })
@Component({ components: { QuickNav } })
export default class IdentitySwitcherView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
Constants = AppString;
public accounts: AccountsSchema;
public activeDid;
public firstName;
public lastName;
public alertTitle;
public alertMessage;
public otherIdentities = [];
public accounts: typeof AccountsSchema;
public activeDid = "";
public apiServer = "";
public apiServerInput = "";
public firstName = "";
public lastName = "";
public otherIdentities: Array<{ did: string }> = [];
public showContactGives = false;
public async getIdentity(activeDid) {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
@ -126,31 +130,20 @@ export default class IdentitySwitcherView extends Vue {
}
}
} catch (err) {
if (
err.message ===
"Attempted to load account records with no identity available."
) {
this.limitsMessage = "No identity.";
this.loadingLimits = false;
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Creating Account",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
console.error(
"Telling user to clear cache at page create because:",
err,
);
}
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Loading Accounts",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
console.error("Telling user to clear cache at page create because:", err);
}
}
async switchAccount(did: string) {
async switchAccount(did?: string) {
// 0 means none
if (did === "0") {
did = undefined;
@ -159,7 +152,7 @@ export default class IdentitySwitcherView extends Vue {
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: did,
});
this.activeDid = did;
this.activeDid = did || "";
this.otherIdentities = [];
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();

2
src/views/ImportAccountView.vue

@ -67,7 +67,7 @@ import {
deriveAddress,
newIdentifier,
} from "../libs/crypto";
import { accountsDB, db } from "@/db";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component({

12
src/views/ImportDerivedAccountView.vue

@ -73,7 +73,7 @@ import {
deriveAddress,
newIdentifier,
} from "../libs/crypto";
import { accountsDB, db } from "@/db";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component({
@ -87,9 +87,9 @@ export default class ImportAccountView extends Vue {
async mounted() {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const seedDids = {};
const seedDids: Record<string, Array<string>> = {};
accounts.forEach((account) => {
const prevDids = seedDids[account.mnemonic] || [];
const prevDids: Array<string> = seedDids[account.mnemonic] || [];
seedDids[account.mnemonic] = prevDids.concat([account.did]);
});
this.didArrays = Object.values(seedDids);
@ -107,9 +107,9 @@ export default class ImportAccountView extends Vue {
public async incrementDerivation() {
await accountsDB.open();
// find the maximum derivation path for the selected DIDs
const selectedArray: Array<string> = this.didArrays.find(
(dids) => dids[0] === this.selectedArrayFirstDid,
);
const selectedArray: Array<string> =
this.didArrays.find((dids) => dids[0] === this.selectedArrayFirstDid) ||
[];
const allMatchingAccounts = await accountsDB.accounts
.where("did")
.anyOf(...selectedArray)

2
src/views/NewEditAccountView.vue

@ -49,7 +49,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { db } from "@/db";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component({

43
src/views/NewEditProjectView.vue

@ -97,10 +97,6 @@
Cancel
</button>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@ -111,20 +107,27 @@ import * as didJwt from "did-jwt";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { accountsDB, db } from "@/db";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import { useAppStore } from "@/store/app";
import { IIdentifier } from "@veramo/core";
import AlertMessage from "@/components/AlertMessage";
import { PlanVerifiableCredential } from "@/libs/endorserServer";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({
components: { AlertMessage, LMap, LMarker, LTileLayer },
components: { LMap, LMarker, LTileLayer },
})
export default class NewEditProjectView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = "";
alertTitle = "";
alertMessage = "";
apiServer = "";
description = "";
errorMessage = "";
@ -140,7 +143,7 @@ export default class NewEditProjectView extends Vue {
this.numAccounts = await accountsDB.accounts.count();
}
public async getIdentity(activeDid) {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
@ -156,7 +159,7 @@ export default class NewEditProjectView extends Vue {
return identity;
}
public async getHeaders(identity) {
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@ -215,7 +218,7 @@ export default class NewEditProjectView extends Vue {
private async SaveProject(identity: IIdentifier) {
// Make a claim
const vcClaim: VerifiableCredential = {
const vcClaim: PlanVerifiableCredential = {
"@context": "https://schema.org",
"@type": "PlanAction",
name: this.projectName,
@ -270,19 +273,15 @@ export default class NewEditProjectView extends Vue {
// version shows up here: https://api.endorser.ch/api-docs/
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
this.errorMessage = "";
this.alertTitle = "";
this.alertMessage = "";
// handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://api.endorser.ch/api-docs/
useAppStore().setProjectId(
resp.data.success.handleId || resp.data.success.fullIri,
);
setTimeout(
function (that: Vue) {
const route = {
name: "project",
};
that.$router.push(route);
function (that: NewEditProjectView) {
that.$router.push({ name: "project" });
},
2000,
this,
@ -290,11 +289,13 @@ export default class NewEditProjectView extends Vue {
}
} catch (error) {
let userMessage = "There was an error saving the project.";
const serverError = error as AxiosError;
const serverError = error as AxiosError<{
error?: { message?: string };
}>;
if (serverError) {
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
console.log(serverError);
userMessage = serverError.response.data.error.message; // This is info for the user.
userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user.
this.$notify(
{
group: "alert",

4
src/views/NewIdentifierView.vue

@ -40,10 +40,10 @@
<script lang="ts">
import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { accountsDB, db } from "@/db/index";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav";
import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { QuickNav } })
export default class AccountViewView extends Vue {

107
src/views/ProjectViewView.vue

@ -200,40 +200,45 @@
message="Received from"
>
</GiftedDialog>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { AxiosError } from "axios";
import { AxiosError, RawAxiosRequestHeaders } from "axios";
import * as moment from "moment";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { accountsDB, db } from "@/db";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
createAndSubmitGive,
didInfo,
GiverInputInfo,
GiverOutputInfo,
GiveServerRecord,
ResultWithType,
} from "@/libs/endorserServer";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
components: { GiftedDialog, QuickNav, EntityIcon },
})
export default class ProjectViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = "";
alertMessage = "";
alertTitle = "";
allMyDids: Array<string> = [];
allContacts: Array<Contact> = [];
apiServer = "";
@ -266,7 +271,7 @@ export default class ProjectViewView extends Vue {
this.LoadProject(identity);
}
public async getIdentity(activeDid) {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
@ -282,7 +287,7 @@ export default class ProjectViewView extends Vue {
return identity;
}
public async getHeaders(identity) {
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@ -300,7 +305,12 @@ export default class ProjectViewView extends Vue {
}
// Isn't there a better way to make this available to the template?
didInfo(did, activeDid, dids, contacts) {
didInfo(
did: string,
activeDid: string,
dids: Array<string>,
contacts: Array<Contact>,
) {
return didInfo(did, activeDid, dids, contacts);
}
@ -317,7 +327,7 @@ export default class ProjectViewView extends Vue {
this.apiServer +
"/api/claim/byHandle/" +
encodeURIComponent(this.projectId);
const headers = {
const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
if (identity) {
@ -451,8 +461,9 @@ export default class ProjectViewView extends Vue {
}
}
openDialog(contact) {
this.$refs.customDialog.open(contact);
openDialog(contact: GiverInputInfo) {
const dialog: GiftedDialog = this.$refs.customDialog as GiftedDialog;
dialog.open(contact);
}
getOpenStreetMapUrl() {
@ -469,11 +480,16 @@ export default class ProjectViewView extends Vue {
);
}
handleDialogResult(result) {
handleDialogResult(result: GiverOutputInfo) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(result.contact?.did, result.description, result.hours);
resolve();
this.recordGive(
result.giver?.did,
result.description,
result.hours,
).then(() => {
resolve(null);
});
});
} else {
// action was not "confirm" so do nothing
@ -486,7 +502,7 @@ export default class ProjectViewView extends Vue {
* @param description may be an empty string
* @param hours may be 0
*/
async recordGive(giverDid, description, hours) {
async recordGive(giverDid?: string, description?: string, hours?: number) {
if (!this.activeDid) {
this.$notify(
{
@ -510,10 +526,7 @@ export default class ProjectViewView extends Vue {
},
-1,
);
return;
}
try {
} else {
const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
@ -525,45 +538,37 @@ export default class ProjectViewView extends Vue {
hours,
this.projectId,
);
if (result.status !== 201 || result.data?.error) {
console.log("Error with give result:", result);
if (result.type == "success") {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
result.data?.error?.message ||
"There was an error recording the give.",
type: "success",
title: "Success",
text: "That gift was recorded.",
},
-1,
);
} else {
console.log("Error with give creation:", result);
if (result.type != "error") {
console.log(
"... and it has an unexpected result type of",
(result as ResultWithType).type,
);
}
const message =
result?.error?.userMessage ||
"There was an error recording the Give.";
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "That gift was recorded.",
type: "danger",
title: "Error",
text: message,
},
-1,
);
}
} catch (e) {
console.log("Error with give caught:", e);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
e.userMessage ||
e.response?.data?.error?.message ||
"There was an error recording the give.",
},
-1,
);
}
}
}

46
src/views/ProjectsView.vue

@ -67,28 +67,33 @@
</li>
</ul>
</InfiniteScroll>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import InfiniteScroll from "@/components/InfiniteScroll";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { ProjectData } from "@/libs/endorserServer";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({
components: { InfiniteScroll, AlertMessage, QuickNav, EntityIcon },
components: { InfiniteScroll, QuickNav, EntityIcon },
})
export default class ProjectsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
apiServer = "";
projects: ProjectData[] = [];
current: IIdentifier;
@ -119,21 +124,30 @@ export default class ProjectsView extends Vue {
if (resp.status === 200 || !resp.data.data) {
const plans: ProjectData[] = resp.data.data;
for (const plan of plans) {
const { name, description, handleId = plan.fullIri, rowid } = plan;
const { name, description, handleId, rowid } = plan;
this.projects.push({ name, description, handleId, rowid });
}
} else {
console.log("Bad server response & data:", resp.status, resp.data);
throw Error("Failed to get projects from the server.");
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to get projects from the server. Try again later.",
},
-1,
);
}
} catch (error) {
console.error("Got error loading projects:", error.message);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Got error loading projects:", error.message || error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Got an error loading projects: " + error.message,
text: "Got an error loading projects.",
},
-1,
);
@ -177,7 +191,7 @@ export default class ProjectsView extends Vue {
await this.dataLoader(url, token);
}
public async getIdentity(activeDid) {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")

30
src/views/SeedBackupView.vue

@ -50,28 +50,34 @@
</div>
</div>
<div v-else>You do not have an active identity.</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { accountsDB, db } from "@/db/index";
import * as R from "ramda";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { AlertMessage, QuickNav } })
interface Account {
mnemonic: string;
}
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } })
export default class SeedBackupView extends Vue {
activeAccount = null;
$notify!: (notification: Notification, timeout?: number) => void;
activeAccount: Account | null | undefined = null;
numAccounts = 0;
showSeed = false;
alertMessage = "";
alertTitle = "";
// 'created' hook runs when the Vue instance is first created
async created() {
@ -84,7 +90,7 @@ export default class SeedBackupView extends Vue {
const accounts = await accountsDB.accounts.toArray();
this.numAccounts = accounts.length;
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
} catch (err) {
} catch (err: unknown) {
console.error("Got an error loading an identity:", err);
this.$notify(
{

2
src/views/StartView.vue

@ -37,7 +37,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB } from "@/db";
import { accountsDB } from "@/db/index";
@Component({
components: {},

42
src/views/StatisticsView.vue

@ -34,25 +34,35 @@
</div>
<button class="float-right" @click="captureGraphics()">Screenshot</button>
<div id="scene-container" class="h-screen"></div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js";
import { Component, Vue } from "vue-facing-decorator";
import { World } from "@/components/World/World.js";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { AlertMessage, World, QuickNav } })
interface RendererSVGType {
domElement: Element;
}
interface Dictionary<T> {
[key: string]: T;
}
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { World, QuickNav } })
export default class StatisticsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
world: World;
worldProperties: WorldProperties = {};
alertTitle = "";
alertMessage = "";
worldProperties: Dictionary<number> = {};
mounted() {
try {
@ -60,14 +70,14 @@ export default class StatisticsView extends Vue {
const newWorld = new World(container, this);
newWorld.start();
this.world = newWorld;
} catch (err) {
console.log(err);
} catch (err: unknown) {
const error = err as Error;
this.$notify(
{
group: "alert",
type: "danger",
title: "Mounting Error",
text: err.message,
text: error.message,
},
-1,
);
@ -85,12 +95,12 @@ export default class StatisticsView extends Vue {
ExportToSVG(rendererSVG, "test.svg");
}
public setWorldProperty(propertyName, propertyValue) {
public setWorldProperty(propertyName: string, propertyValue: number) {
this.worldProperties[propertyName] = propertyValue;
}
}
function ExportToSVG(rendererSVG, filename) {
function ExportToSVG(rendererSVG: RendererSVGType, filename: string) {
const XMLS = new XMLSerializer();
const svgfile = XMLS.serializeToString(rendererSVG.domElement);
const svgData = svgfile;

80
tsconfig.json

@ -1,41 +1,47 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
"compilerOptions": {
"allowJs": true,
"resolveJsonModule": true,
"target": "esnext",
"module": "esnext",
"strict": true,
"strictPropertyInitialization": false,
"jsx": "preserve",
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": "./src",
"types": [
"webpack-env"
],
"paths": {
"@/components/*": ["components/*"],
"@/views/*": ["views/*"],
"@/db/*": ["db/*"],
"@/libs/*": ["libs/*"],
"@/constants/*": ["constants/*"],
"@/store/*": ["store/*"],
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

Loading…
Cancel
Save