Browse Source

Merging next-iteration

kb/add-usage-guide
Matthew Aaron Raymer 1 year ago
parent
commit
6233189a49
  1. 5
      README.md
  2. 24
      project.task.yaml
  3. BIN
      public/img/textures/forest-floor.png
  4. BIN
      public/img/textures/leafy-autumn-forest-floor.jpg
  5. 104
      src/components/GiftedDialog.vue
  6. 2
      src/components/InfiniteScroll.vue
  7. 2
      src/components/World/components/objects/terrain.js
  8. 7
      src/db/index.ts
  9. 2
      src/db/tables/accounts.ts
  10. 2
      src/db/tables/settings.ts
  11. 147
      src/libs/endorserServer.ts
  12. 4
      src/main.ts
  13. 31
      src/router/index.ts
  14. 29
      src/views/AccountViewView.vue
  15. 10
      src/views/ContactAmountsView.vue
  16. 5
      src/views/ContactQRScanShowView.vue
  17. 22
      src/views/ContactsView.vue
  18. 8
      src/views/HelpView.vue
  19. 357
      src/views/HomeView.vue
  20. 21
      src/views/NewEditProjectView.vue
  21. 5
      src/views/ProjectViewView.vue
  22. 5
      src/views/ProjectsView.vue
  23. 5
      src/views/StatisticsView.vue

5
README.md

@ -188,6 +188,9 @@ export const createAndStoreIdentifier = async (mnemonicPassword) => {
## Kudos ## Kudos
Gifts make the world go 'round!
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80) * [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
* [Many libraries]() such as Veramo.io, Vuejs.org, threejs * [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439) * [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)

24
project.task.yaml

@ -4,8 +4,9 @@
- add infinite scroll assignee:matthew - add infinite scroll assignee:matthew
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- allow type annotations in World.js & landmarks.js (since we get this error: "Types are not supported by current JavaScript version") - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
- replace user-affecting console.log & console.error with error messages (eg. catches) - replace user-affecting console.log & console.error with error messages (eg. catches)
- if there's no identity, handle it on pages which expect an identity (eg. project -- look for JSON.parse identity calls)
- 8 Move to vue-facing-decorator - 8 Move to vue-facing-decorator
@ -30,9 +31,11 @@
- show pop-up confirming that settings & contacts have been downloaded - show pop-up confirming that settings & contacts have been downloaded
- Ensure each action sent to the server has a confirmation. - Ensure each action sent to the server has a confirmation - registration
- discover screen - Home Feed & Quick Give screen :
- 01 save the feed-viewed status in settings storage ("afterQuery")
- 01 quick action - send action, maybe choose via canvas tool https://github.com/konvajs/vue-konva
- .5 customize favicon - .5 customize favicon
- .5 make advanced features harder to access; advanced build? - .5 make advanced features harder to access; advanced build?
@ -40,14 +43,29 @@
- 24 Move to Vite - 24 Move to Vite
- 40 notifications :
- push
- stats v1 :
- 01 show numeric stats
- 01 link to world for specific stats
- .5 don't load another instance of a bush if it already exists
- Do we want split first name & last name?
- remove 'about' page
- Release Minimum Viable Product : - Release Minimum Viable Product :
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot). - Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
- Add disclaimers. - Add disclaimers.
- Rename DB to TimeSafari.
- Switch default server to the public server. - Switch default server to the public server.
- Deploy to a server. - Deploy to a server.
- Ensure public server has limits that work for group adoption. - Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS. - Test PWA features on Android and iOS.
- 40 notifications v+ :
- pull, w/ scheduled runs
- Stats : - Stats :
- 01 point out user's location on the world - 01 point out user's location on the world
- 01 present a credential selected from the stats - 01 present a credential selected from the stats

BIN
public/img/textures/forest-floor.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

BIN
public/img/textures/leafy-autumn-forest-floor.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

104
src/components/GiftedDialog.vue

@ -0,0 +1,104 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-lg text-center">
Received from {{ contact?.name || "nobody in particular" }}
</h1>
<p class="py-2">{{ message }}</p>
<input
type="text"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
placeholder="What you received"
v-model="description"
/>
<div class="flex flex-row">
<span class="py-4">Hours</span>
<input
type="text"
class="block w-8 rounded border border-slate-400 ml-4 text-center"
v-model="hours"
/>
<div class="flex flex-col px-1">
<div>
<fa icon="square-caret-up" size="2xl" @click="increment()" />
</div>
<div>
<fa icon="square-caret-down" size="2xl" @click="decrement()" />
</div>
</div>
</div>
<div class="text-right">
<button class="rounded border border-slate-400" @click="confirm">
<span class="m-2">Confirm</span>
</button>
&nbsp;
<button class="rounded border border-slate-400" @click="cancel">
<span class="m-2">Cancel</span>
</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["message"],
data() {
return {
contact: null,
description: "",
hours: "0",
visible: false,
};
},
methods: {
open(contact) {
this.contact = contact;
this.visible = true;
},
close() {
this.visible = false;
},
increment() {
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
},
decrement() {
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
},
confirm() {
this.close();
this.$emit("dialog-result", {
action: "confirm",
contact: this.contact,
hours: parseFloat(this.hours),
description: this.description,
});
},
cancel() {
this.close();
this.$emit("dialog-result", { action: "cancel" });
},
},
};
</script>
<style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 50%;
}
</style>

2
src/components/InfiniteScroll.vue

@ -14,6 +14,7 @@ export default class InfiniteScroll extends Vue {
readonly distance!: number; readonly distance!: number;
private observer!: IntersectionObserver; private observer!: IntersectionObserver;
// 'mounted' hook runs after initial render
mounted() { mounted() {
const options = { const options = {
root: this.$refs.scrollContainer as HTMLElement, root: this.$refs.scrollContainer as HTMLElement,
@ -24,6 +25,7 @@ export default class InfiniteScroll extends Vue {
this.observer.observe(this.$refs.sentinel as HTMLElement); this.observer.observe(this.$refs.sentinel as HTMLElement);
} }
// 'beforeUnmount' hook runs before unmounting the component
beforeUnmount() { beforeUnmount() {
this.observer.disconnect(); this.observer.disconnect();
} }

2
src/components/World/components/objects/terrain.js

@ -2,7 +2,7 @@ import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from "three";
export function createTerrain(props) { export function createTerrain(props) {
const loader = new TextureLoader(); const loader = new TextureLoader();
const height = loader.load("img/textures/forest-floor.png"); const height = loader.load("img/textures/leafy-autumn-forest-floor.jpg");
// w h // w h
const geometry = new PlaneGeometry(props.width, props.height, 64, 64); const geometry = new PlaneGeometry(props.width, props.height, 64, 64);

7
src/db/index.ts

@ -41,7 +41,12 @@ const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw * https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
*/ */
// create password and place password in localStorage /**
* Create password and place password in localStorage.
*
* It's good practice to keep the data encrypted at rest, so we'll do that even
* if the secret is stored right next to the app.
*/
const secret = const secret =
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey(); localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();

2
src/db/tables/accounts.ts

@ -3,6 +3,8 @@ export type Account = {
dateCreated: string; dateCreated: string;
derivationPath: string; derivationPath: string;
did: string; did: string;
// stringified JSON containing underlying key material of type IIdentifier
// https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts
identity: string; identity: string;
publicKeyHex: string; publicKeyHex: string;
mnemonic: string; mnemonic: string;

2
src/db/tables/settings.ts

@ -1,10 +1,12 @@
// a singleton // a singleton
export type Settings = { export type Settings = {
id: number; // there's only one entry: MASTER_SETTINGS_KEY id: number; // there's only one entry: MASTER_SETTINGS_KEY
activeDid?: string; activeDid?: string;
apiServer?: string; apiServer?: string;
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
lastViewedClaimId?: string;
showContactGivesInline?: boolean; showContactGivesInline?: boolean;
}; };

147
src/libs/endorserServer.ts

@ -1,21 +1,32 @@
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import { Axios, AxiosResponse } from "axios";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
export const SERVICE_ID = "endorser.ch"; export const SERVICE_ID = "endorser.ch";
export interface GenericClaim { export interface AgreeVerifiableCredential {
"@context": string; "@context": string;
"@type": string; "@type": string;
issuedAt: string;
// "any" because arbitrary objects can be subject of agreement // "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
claim: Record<any, any>; object: Record<any, any>;
} }
export interface AgreeVerifiableCredential { export interface ClaimResult {
success: { claimId: string; handleId: string };
error: { code: string; message: string };
}
export interface GenericClaim {
"@context": string; "@context": string;
"@type": string; "@type": string;
issuedAt: string;
// "any" because arbitrary objects can be subject of agreement // "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
object: Record<any, any>; claim: Record<any, any>;
} }
export interface GiveServerRecord { export interface GiveServerRecord {
@ -33,10 +44,10 @@ export interface GiveServerRecord {
export interface GiveVerifiableCredential { export interface GiveVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree "@context"?: string; // optional when embedded, eg. in an Agree
"@type": string; "@type": string;
agent: { identifier: string }; agent?: { identifier: string };
description?: string; description?: string;
identifier?: string; identifier?: string;
object: { amountOfThisGood: number; unitCode: string }; object?: { amountOfThisGood: number; unitCode: string };
recipient: { identifier: string }; recipient: { identifier: string };
} }
@ -47,3 +58,125 @@ export interface RegisterVerifiableCredential {
object: string; object: string;
recipient: { identifier: string }; recipient: { identifier: string };
} }
export interface InternalError {
error: string; // for system logging
userMessage?: string; // for user display
}
// This is used to check for hidden info.
// 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) {
return did === HIDDEN_DID;
}
/**
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
**/
export function didInfo(did, identifiers, contacts) {
const myId = R.find((i) => i.did === did, identifiers);
if (myId) {
return "You";
} else {
const contact = R.find((c) => c.did === did, contacts);
if (contact) {
return contact.name || "Someone Unnamed in Contacts";
} else if (!did) {
return "Unpecified Person";
} else if (isHiddenDid(did)) {
return "Someone Not In Network";
} else {
return "Someone Not In Contacts";
}
}
}
/**
* For result, see https://endorser.ch:3000/api-docs/#/claims/post_api_v2_claim
* @param identity
* @param fromDid may be null
* @param toDid
* @param description may be null; should have this or hours
* @param hours may be null; should have this or description
*/
export async function createAndSubmitGive(
axios: Axios,
apiServer: string,
identity: IIdentifier,
fromDid: string,
toDid: string,
description: string,
hours: number
): 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" };
}
// 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({
error: "No private key",
message:
"Your identifier " +
identity.did +
" is not configured correctly. Use a different identifier.",
});
});
}
// 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,
};
return axios.post(url, payload, { headers });
}
// from https://stackoverflow.com/a/175787/845494
//
export function isNumeric(str: string): boolean {
return !isNaN(+str);
}
export function numberOrZero(str: string): number {
return isNumeric(str) ? +str : 0;
}

4
src/main.ts

@ -37,6 +37,8 @@ import {
faRotate, faRotate,
faShareNodes, faShareNodes,
faSpinner, faSpinner,
faSquareCaretDown,
faSquareCaretUp,
faTrashCan, faTrashCan,
faUser, faUser,
faUsers, faUsers,
@ -71,6 +73,8 @@ library.add(
faRotate, faRotate,
faShareNodes, faShareNodes,
faSpinner, faSpinner,
faSquareCaretDown,
faSquareCaretUp,
faTrashCan, faTrashCan,
faUser, faUser,
faUsers, faUsers,

31
src/router/index.ts

@ -1,21 +1,29 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { accountsDB } from "@/db"; import { accountsDB } from "@/db";
/**
*
* @param to :RouteLocationNormalized
* @param from :RouteLocationNormalized
* @param next :NavigationGuardNext
*/
const enterOrStart = async (to, from, next) => {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
next();
} else {
next({ name: "start" });
}
};
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
path: "/", path: "/",
name: "home", name: "home",
component: () => component: () =>
import(/* webpackChunkName: "start" */ "../views/DiscoverView.vue"), import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
beforeEnter: async (to, from, next) => { beforeEnter: enterOrStart,
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
next();
} else {
next({ name: "start" });
}
},
}, },
{ {
path: "/about", path: "/about",
@ -28,6 +36,7 @@ const routes: Array<RouteRecordRaw> = [
name: "account", name: "account",
component: () => component: () =>
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"), import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
beforeEnter: enterOrStart,
}, },
{ {
path: "/confirm-contact", path: "/confirm-contact",
@ -58,6 +67,7 @@ const routes: Array<RouteRecordRaw> = [
name: "contacts", name: "contacts",
component: () => component: () =>
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"), import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
beforeEnter: enterOrStart,
}, },
{ {
path: "/scan-contact", path: "/scan-contact",
@ -130,6 +140,7 @@ const routes: Array<RouteRecordRaw> = [
name: "projects", name: "projects",
component: () => component: () =>
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"), import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
beforeEnter: enterOrStart,
}, },
{ {
path: "/seed-backup", path: "/seed-backup",

29
src/views/AccountViewView.vue

@ -298,10 +298,10 @@
</div> </div>
<div> <div>
<button class="text-blue-500 px-2"> <button class="text-blue-500">
<router-link <router-link
:to="{ name: 'statistics' }" :to="{ name: 'statistics' }"
class="block text-center py-3 px-1" class="block text-center py-3"
> >
See Achievements & Statistics See Achievements & Statistics
</router-link> </router-link>
@ -389,7 +389,7 @@ export default class AccountViewView extends Vue {
// 'created' hook runs when the Vue instance is first created // 'created' hook runs when the Vue instance is first created
async created() { async created() {
// Uncomment to register this user on the test server. // Uncomment this to register this user on the test server.
// To manage within the vue devtools browser extension https://devtools.vuejs.org/ // To manage within the vue devtools browser extension https://devtools.vuejs.org/
// assign this to a class variable, eg. "registerThisUser = testServerRegisterUser", // assign this to a class variable, eg. "registerThisUser = testServerRegisterUser",
// select a component in the extension, and enter in the console: $vm.ctx.registerThisUser() // select a component in the extension, and enter in the console: $vm.ctx.registerThisUser()
@ -409,7 +409,10 @@ export default class AccountViewView extends Vue {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
this.numAccounts = accounts.length; this.numAccounts = accounts.length;
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
this.publicHex = identity.keys[0].publicKeyHex; this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta.derivationPath; this.derivationPath = identity.keys[0].meta.derivationPath;
@ -419,8 +422,8 @@ export default class AccountViewView extends Vue {
}); });
} catch (err) { } catch (err) {
this.alertMessage = this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info."; "Clear your cache and start over (after data backup).";
console.error("Telling user to clear cache because:", err); console.error("Telling user to clear cache at page create because:", err);
this.alertTitle = "Error Creating Account"; this.alertTitle = "Error Creating Account";
this.isAlertVisible = true; this.isAlertVisible = true;
} }
@ -436,9 +439,12 @@ export default class AccountViewView extends Vue {
}); });
} catch (err) { } catch (err) {
this.alertMessage = this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info."; "Clear your cache and start over (after data backup).";
console.error("Telling user to clear cache because:", err); console.error(
this.alertTitle = "Error Creating Account"; "Telling user to clear cache after contact setting update because:",
err
);
this.alertTitle = "Error Updating Contact Setting";
this.isAlertVisible = true; this.isAlertVisible = true;
} }
} }
@ -474,7 +480,10 @@ export default class AccountViewView extends Vue {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",

10
src/views/ContactAmountsView.vue

@ -168,7 +168,10 @@ export default class ContactsView extends Vue {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts); const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
// load all the time I have given to them // load all the time I have given to them
try { try {
@ -267,7 +270,10 @@ export default class ContactsView extends Vue {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
if (identity.keys[0].privateKeyHex !== null) { if (identity.keys[0].privateKeyHex !== null) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!; const privateKeyHex: string = identity.keys[0].privateKeyHex!;

5
src/views/ContactQRScanShowView.vue

@ -113,7 +113,10 @@ export default class ContactQRScanShow extends Vue {
if (!account) { if (!account) {
this.alertMessage = "You have no identity yet."; this.alertMessage = "You have no identity yet.";
} else { } else {
const identity = JSON.parse(account.identity); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
const publicKeyHex = identity.keys[0].publicKeyHex; const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64"); const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");

22
src/views/ContactsView.vue

@ -314,7 +314,7 @@ export default class ContactsView extends Vue {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
console.error( console.error(
@ -487,7 +487,10 @@ export default class ContactsView extends Vue {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
// Make a claim // Make a claim
const vcClaim: RegisterVerifiableCredential = { const vcClaim: RegisterVerifiableCredential = {
@ -575,7 +578,10 @@ export default class ContactsView extends Vue {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
@ -614,7 +620,10 @@ export default class ContactsView extends Vue {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
@ -671,7 +680,10 @@ export default class ContactsView extends Vue {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
// if they have unconfirmed amounts, ask to confirm those first // if they have unconfirmed amounts, ask to confirm those first
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) { if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {

8
src/views/HelpView.vue

@ -213,6 +213,14 @@
<p> <p>
{{ package.version }} {{ package.version }}
</p> </p>
<h2 class="text-xl font-semibold">
For any other questions, including remove your data:
</h2>
<p>
Contact us through
<a href="https://communitycred.org">CommunityCred.org</a>.
</p>
</div> </div>
</section> </section>
</template> </template>

357
src/views/HomeView.vue

@ -1,15 +1,360 @@
<template> <template>
<section></section> <!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
><fa icon="users" class="fa-fw"></fa
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></router-link>
</li>
</ul>
</nav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Time Safari
</h1>
<div>
<h1 class="text-2xl">Quick Action</h1>
<p>Choose a contact to whom to show appreciation:</p>
<div class="px-4">
<button
v-for="contact in allContacts"
:key="contact.did"
@click="openDialog(contact)"
class="text-blue-500"
>
&nbsp;{{ contact.name }},
</button>
or
<button @click="openDialog()" class="text-blue-500">
nobody in particular
</button>
</div>
</div>
<GiftedDialog
ref="customDialog"
@dialog-result="handleDialogResult"
message="Confirm to publish to the world."
>
</GiftedDialog>
<div class="py-4">
<h1 class="text-2xl">Latest Activity</h1>
<span :class="{ hidden: isHiddenSpinner }">
<fa icon="spinner" class="fa-fw"></fa>
Loading&hellip;
</span>
<ul class="">
<li
class="border-b border-slate-300"
v-for="record in feedData"
:key="record.jwtId"
>
<div
class="border-b text-orange-400 px-8 py-4"
v-if="record.jwtId == feedLastViewedId"
>
You've seen all claims below.
</div>
{{ this.giveDescription(record) }}
</li>
</ul>
</div>
</section>
<!-- This same popup code is in many files. -->
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 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> </template>
<script lang="ts"> <script lang="ts">
import * as R from "ramda";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
@Options({ @Options({
components: { components: { GiftedDialog },
HelloWorld,
},
}) })
export default class HomeView extends Vue {} export default class HomeView extends Vue {
activeDid = "";
allAccounts: Array<Account> = [];
allContacts: Array<Contact> = [];
apiServer = "";
feedAllLoaded = false;
feedData = [];
feedPreviousOldestId = null;
feedLastViewedId = null;
isHiddenSpinner = true;
// 'created' hook runs when the Vue instance is first created
async created() {
await accountsDB.open();
this.allAccounts = await accountsDB.accounts.toArray();
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId;
}
// 'mounted' hook runs after initial render
async mounted() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.apiServer = settings?.apiServer || "";
this.updateAllFeed();
} catch (err) {
console.log("Error in mounted():", err);
this.alertTitle = "Error";
this.alertMessage =
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.";
}
}
updateAllFeed = async () => {
this.isHiddenSpinner = false;
await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId)
.then(async (results) => {
if (results.data.length > 0) {
this.feedData = this.feedData.concat(results.data);
//console.log("Feed data:", this.feedData);
this.feedAllLoaded = results.hitLimit;
this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId;
if (
this.feedLastViewedId == null ||
this.feedLastViewedId < results.data[0].jwtId
) {
// save it to storage
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
lastViewedClaimId: results.data[0].jwtId,
});
// but not for this page because we need to remember what it was before
}
}
})
.catch((e) => {
console.log("Error with feed load:", e);
this.alertMessage =
e.userMessage || "There was an error retrieving feed data.";
});
this.isHiddenSpinner = true;
};
retrieveClaims = async (endorserApiServer, identifier, beforeId) => {
//const afterQuery = afterId == null ? "" : "&afterId=" + afterId;
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const headers = { "Content-Type": "application/json" };
if (this.activeDid) {
const account = R.find(
(acc) => acc.did === this.activeDid,
this.allAccounts
);
//console.log("about to parse from", this.activeDid, account?.identity);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
} else {
// it's OK without auth... we just won't get any identifiers
}
return fetch(this.apiServer + "/api/v2/report/gives?" + beforeQuery, {
method: "GET",
headers: headers,
})
.then(async (response) => {
if (response.status !== 200) {
const details = await response.text();
throw details;
}
return response.json();
})
.then((results) => {
if (results.data) {
return results;
} else {
throw JSON.stringify(results);
}
});
};
giveDescription(giveRecord) {
let claim = giveRecord.fullClaim;
if (claim.claim) {
// it's probably a Verified Credential
claim = claim.claim;
}
// agent.did is for legacy data, before March 2023
const giver =
claim.agent?.identifier || claim.agent?.did || giveRecord.issuer;
const giverInfo = didInfo(giver, this.allAccounts, this.allContacts);
const gaveAmount = claim.object?.amountOfThisGood
? 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 gaveRecipientInfo = gaveRecipientId
? " to " + didInfo(gaveRecipientId, this.allAccounts, this.allContacts)
: "";
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
}
displayAmount(code, amt) {
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
}
currencyShortWordForCode(unitCode, single) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
}
openDialog(contact) {
this.$refs.customDialog.open(contact);
}
handleDialogResult(result) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(result.contact, result.description, result.hours);
resolve();
});
} else {
// action was "cancel" so do nothing
}
}
/**
*
* @param contact may be null
* @param description may be an empty string
* @param hours may be 0
*/
recordGive(contact, description, hours) {
if (this.activeDid == null) {
this.alertTitle = "Error";
this.alertMessage =
"You must select an identity before you can record a give.";
return;
}
const account = R.find(
(acc) => acc.did === this.activeDid,
this.allAccounts
);
//console.log("about to parse from", this.activeDid, account?.identity);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
createAndSubmitGive(
this.axios,
this.apiServer,
identity,
contact?.did,
this.activeDid,
description,
hours
)
.then((result) => {
if (result.status != 201 || result.data?.error) {
console.log("Error with give result:", result);
this.alertTitle = "Error";
this.alertMessage =
result.data?.message || "There was an error recording the give.";
} else {
this.alertTitle = "Success";
this.alertMessage = "That gift was recorded.";
//this.updateAllFeed(); // full update is overkill but we should show something
}
})
.catch((e) => {
console.log("Error with give caught:", e);
this.alertTitle = "Error";
this.alertMessage =
e.userMessage || "There was an error recording the give.";
});
}
// This same popup code is in many files.
alertMessage = "";
alertTitle = "";
public onClickClose() {
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.alertMessage,
"dismissable-alert": true,
"bg-slate-100": 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> </script>

21
src/views/NewEditProjectView.vue

@ -49,8 +49,9 @@
<span :class="{ hidden: isHiddenSave }">Save Project</span> <span :class="{ hidden: isHiddenSave }">Save Project</span>
<!-- SHOW if in saving state; DISABLE button while in saving state --> <!-- SHOW if in saving state; DISABLE button while in saving state -->
<span :class="{ hidden: isHiddenSpinner }" <span :class="{ hidden: isHiddenSpinner }">
><i class="fa-solid fa-spinner fa-spin-pulse"></i> <!-- icon no worky? -->
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
Saving&hellip;</span Saving&hellip;</span
> >
</button> </button>
@ -122,11 +123,14 @@ export default class NewEditProjectView extends Vue {
await accountsDB.open(); await accountsDB.open();
const num_accounts = await accountsDB.accounts.count(); const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) { if (num_accounts === 0) {
console.error("Problem! Should have a profile!"); console.error("Error: no account was found.");
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
this.LoadProject(identity); this.LoadProject(identity);
} }
} }
@ -222,7 +226,7 @@ export default class NewEditProjectView extends Vue {
); );
} }
} catch (error) { } catch (error) {
let userMessage = "There was an error. See logs for more info."; let userMessage = "There was an error saving the project.";
const serverError = error as AxiosError; const serverError = error as AxiosError;
if (serverError) { if (serverError) {
this.isAlertVisible = true; this.isAlertVisible = true;
@ -254,11 +258,14 @@ export default class NewEditProjectView extends Vue {
await accountsDB.open(); await accountsDB.open();
const num_accounts = await accountsDB.accounts.count(); const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) { if (num_accounts === 0) {
console.error("Problem! Should have a profile!"); console.error("Error: there is no account.");
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
this.SaveProject(identity); this.SaveProject(identity);
} }
} }

5
src/views/ProjectViewView.vue

@ -240,7 +240,10 @@ export default class ProjectViewView extends Vue {
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts); const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
this.LoadProject(identity); this.LoadProject(identity);
} }
} }

5
src/views/ProjectsView.vue

@ -167,7 +167,10 @@ export default class ProjectsView extends Vue {
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts); const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
this.LoadProjects(identity); this.LoadProjects(identity);
} }
} }

5
src/views/StatisticsView.vue

@ -61,7 +61,7 @@
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. --> <!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
<li>Each will show at their time of appearance relative to all others.</li> <li>Each will show at their time of appearance relative to all others.</li>
<li>Note that the ones on the left and right edges are randomized <li>Note that the ones on the left and right edges are randomized
because not all their positional data is visible to you. because their data isn't all visible to you.
</li> </li>
<!-- eslint-enable --> <!-- eslint-enable -->
</ul> </ul>
@ -75,7 +75,7 @@
{{ worldProperties.endTime }} {{ worldProperties.endTime }}
</div> </div>
<div v-if="worldProperties.animationDurationSeconds"> <div v-if="worldProperties.animationDurationSeconds">
<label>Animation duration:&nbsp;</label> <label>Animation Time:&nbsp;</label>
{{ worldProperties.animationDurationSeconds }} seconds {{ worldProperties.animationDurationSeconds }} seconds
</div> </div>
</div> </div>
@ -113,6 +113,7 @@ export default class StatisticsView extends Vue {
world: World; world: World;
worldProperties: WorldProperties = {}; worldProperties: WorldProperties = {};
// 'mounted' hook runs after initial render
mounted() { mounted() {
const container = document.querySelector("#scene-container"); const container = document.querySelector("#scene-container");
const newWorld = new World(container, this); const newWorld = new World(container, this);

Loading…
Cancel
Save