Merging remote master into local

This commit is contained in:
Matthew Raymer
2023-06-27 19:06:25 +08:00
25 changed files with 956 additions and 146 deletions

View File

@@ -66,20 +66,22 @@
</span>
</div>
<!-- Friend referral requirement notice -->
<!-- Registration notice -->
<!-- We won't show any loading indicator; we'll just pop the message in once we know they need it. -->
<div
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
>
<p class="mb-4">
<b>Important:</b> before you can create a new project or commit time to
one, you need a friend to register you.
<b>Note:</b> Before you can publicly announce a new project or time
commitment, a friend needs to register you.
</p>
<button
id="btnShowQR"
<router-link
:to="{ name: 'contact-qr' }"
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
>
Share Your ID
</button>
Share Your Info
</router-link>
</div>
<!-- Identity Details -->
@@ -181,7 +183,7 @@
<div class="text-slate-500 text-center">
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
</div>
<img src="img/sample-qr-code.png" class="w-full mb-3" />
<img src="/img/sample-qr-code.png" class="w-full mb-3" />
<button
value="cancel"
@@ -229,6 +231,13 @@
<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>
@@ -254,10 +263,10 @@
/>
<button
v-if="apiServerInput != apiServer"
class="px-4 rounded bg-slate-200 border border-slate-400"
class="px-4 rounded bg-red-500 border border-slate-400"
@click="onClickSaveApiServer()"
>
<fa icon="floppy-disk" class="fa-fw"></fa>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button>
<button
class="px-4 rounded bg-slate-200 border border-slate-400"
@@ -280,7 +289,7 @@
</div>
<div v-if="numAccounts > 0" class="flex py-2">
Switch Account
Switch Identifier
<span v-for="accountNum in numAccounts" :key="accountNum">
<button class="text-blue-500 px-2" @click="switchAccount(accountNum)">
#{{ accountNum }}
@@ -289,10 +298,10 @@
</div>
<div>
<button class="text-blue-500 px-2">
<button class="text-blue-500">
<router-link
:to="{ name: 'statistics' }"
class="block text-center py-3 px-1"
class="block text-center py-3"
>
See Achievements & Statistics
</router-link>
@@ -323,12 +332,7 @@ import { useClipboard } from "@vueuse/core";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import {
accessToken,
deriveAddress,
generateSeed,
newIdentifier,
} from "@/libs/crypto";
import { accessToken } from "@/libs/crypto";
import { AxiosError } from "axios/index";
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -357,6 +361,8 @@ export default class AccountViewView extends Vue {
publicHex = "";
publicBase64 = "";
limits: RateLimits | null = null;
limitsMessage = "";
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
showContactGives = false;
showDidCopy = false;
@@ -383,11 +389,12 @@ export default class AccountViewView extends Vue {
// 'created' hook runs when the Vue instance is first 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/
// assign this to a class variable, eg. "registerThisUser = testServerRegisterUser",
// select a component in the extension, and enter in the console: $vm.ctx.registerThisUser()
//testServerRegisterUser();
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
@@ -399,34 +406,13 @@ export default class AccountViewView extends Vue {
this.showContactGives = !!settings?.showContactGivesInline;
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
if (this.numAccounts === 0) {
let address = ""; // 0x... ETH address, without "did:eth:"
let privateHex = "";
const mnemonic = generateSeed();
[address, privateHex, this.publicHex, this.derivationPath] =
deriveAddress(mnemonic);
const newId = newIdentifier(
address,
this.publicHex,
privateHex,
this.derivationPath
);
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: this.derivationPath,
did: newId.did,
identity: JSON.stringify(newId),
mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex,
});
this.activeDid = newId.did;
}
const accounts = await accountsDB.accounts.toArray();
this.numAccounts = accounts.length;
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.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta.derivationPath;
@@ -436,11 +422,13 @@ export default class AccountViewView extends Vue {
});
} catch (err) {
this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info.";
console.error("Telling user to clear cache because:", err);
"Clear your cache and start over (after data backup).";
console.error("Telling user to clear cache at page create because:", err);
this.alertTitle = "Error Creating Account";
this.isAlertVisible = true;
}
this.checkLimits();
}
public async updateShowContactAmounts() {
@@ -451,9 +439,12 @@ export default class AccountViewView extends Vue {
});
} catch (err) {
this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info.";
console.error("Telling user to clear cache because:", err);
this.alertTitle = "Error Creating Account";
"Clear your cache and start over (after data backup).";
console.error(
"Telling user to clear cache after contact setting update because:",
err
);
this.alertTitle = "Error Updating Contact Setting";
this.isAlertVisible = true;
}
}
@@ -482,11 +473,17 @@ export default class AccountViewView extends Vue {
}
async checkLimits() {
this.loadingLimits = true;
this.limitsMessage = "";
const url = this.apiServer + "/api/report/rateLimits";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
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 headers = {
"Content-Type": "application/json",
@@ -502,18 +499,14 @@ export default class AccountViewView extends Vue {
} catch (error: unknown) {
const serverError = error as AxiosError;
this.alertTitle = "Error from Server";
console.error("Bad response retrieving limits: ", serverError);
// Anybody know how to access items inside "response.data" without this?
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = serverError.response?.data;
if (data.error.message) {
this.alertMessage = data.error.message;
} else {
this.alertMessage = "Bad server response. See logs for details.";
}
this.isAlertVisible = true;
this.limitsMessage = data?.error?.message || "Bad server response.";
}
this.loadingLimits = false;
}
async switchAccount(accountNum: number) {

View File

@@ -168,7 +168,10 @@ export default class ContactsView extends Vue {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
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
try {
@@ -267,7 +270,10 @@ export default class ContactsView extends Vue {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
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) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!;

View File

@@ -51,7 +51,7 @@
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Contact Info
Your Contact Info
</h1>
<!--
@@ -113,7 +113,10 @@ export default class ContactQRScanShow extends Vue {
if (!account) {
this.alertMessage = "You have no identity yet.";
} 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 publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");

View File

@@ -314,7 +314,7 @@ export default class ContactsView extends Vue {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
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) {
console.error(
@@ -487,7 +487,10 @@ export default class ContactsView extends Vue {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
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
const vcClaim: RegisterVerifiableCredential = {
@@ -495,7 +498,7 @@ export default class ContactsView extends Vue {
"@type": "RegisterAction",
agent: { identifier: identity.did },
object: SERVICE_ID,
recipient: { identifier: contact.did },
participant: { identifier: contact.did },
};
// Make a payload for the claim
const vcPayload = {
@@ -530,7 +533,15 @@ export default class ContactsView extends Vue {
try {
const resp = await this.axios.post(url, payload, { headers });
//console.log("Got resp data:", resp.data);
if (resp.data?.success?.handleId) {
if (resp.data?.success?.embeddedRecordError) {
this.alertTitle = "Registration Still Unknown";
let message = "There was some problem with the registration.";
if (typeof resp.data.success.embeddedRecordError == "string") {
message += " " + resp.data.success.embeddedRecordError;
}
this.alertMessage = message;
this.isAlertVisible = true;
} else if (resp.data?.success?.handleId) {
contact.registered = true;
db.contacts.update(contact.did, { registered: true });
@@ -567,7 +578,10 @@ export default class ContactsView extends Vue {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
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 headers = {
@@ -606,7 +620,10 @@ export default class ContactsView extends Vue {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
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 headers = {
@@ -663,7 +680,10 @@ export default class ContactsView extends Vue {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
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 (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {

View File

@@ -60,14 +60,14 @@
gifts and collaboration.
</p>
<h2 class="text-xl font-semibold">How is this app useful?</h2>
<h2 class="text-xl font-semibold">What is the philosophy here?</h2>
<p>
We are building networks of people who want to grow a gifting society.
First of all, you can record ways you've seen people give, and that
leaves a permanent record... one that they show came from you. This is
personally gratifying, but it extends to broader work: volunteers can
get confirmation of activity and selectively show off their
contributions and network.
leaves a permanent record -- one that came from you, and the recipient
can prove it was for them. This is personally gratifying, but it extends
to broader work: volunteers can get confirmation of activity and
selectively show off their contributions and network.
</p>
<p>
You can also record projects and plans and invite others to collaborate.
@@ -83,10 +83,25 @@
the control; this app gives you the control.
</p>
<h2 class="text-xl font-semibold">How do I take my first action?</h2>
<p>
You need someone to register you -- usually the person who told you
about this app, on the Contacts
<fa icon="circle-user" class="fa-fw" /> page. After they register you,
and after you have contacts, you can select any contact on the home page
and record your appreciation for... whatever. That is a claim recorded
on a custom ledger. The day after being registered, you'll be able to
register others; later, you can create projects, too.
</p>
<p>
Note that there are limits to how many each person can register, so you
may have to wait.
</p>
<h2 class="text-xl font-semibold">How do I backup all my data?</h2>
<p>
There are two parts to backup your data: the identifier secrets and the
other data such as settings, contacts, etc.
There are two sets of data to backup: the identifier secrets and the
other data that isn't quite a secret such as settings, contacts, etc.
</p>
<div class="px-4">
@@ -100,6 +115,10 @@
<li>
Click on "Backup Identifier Seed" and follow the instructions.
</li>
<li>
If you have other identifiers, switch to each one and repeat those
steps.
</li>
</ul>
<h2 class="text-xl font-semibold">
@@ -156,28 +175,6 @@
key.
</p>
<h2 class="text-xl font-semibold">
How do I get permission to store claims on the server?
</h2>
<p>
Get registered by someone else with the app; they can register you on
the Contacts <fa icon="circle-user" class="fa-fw" /> page. There are
limits to how many each person can register, so you may have to wait.
</p>
<h2 class="text-xl font-semibold">What do you mean by "claims"?</h2>
<p>
Certain actions in this app are signed by your private keys, and these
are often called "claims". For example, when you give time to a person
or project, you sign a claim declaring that you gave that time. When you
declare a project, you sign a claim declaring it to the world. When you
confirm someone else's claim, you sign a claim of agreement.
</p>
<p>
Some of the data in this app does not involve claims, such as your
contact list and your identifier.
</p>
<h2 class="text-xl font-semibold">
I know there is a record from someone, so why can't I see that info?
</h2>
@@ -199,8 +196,8 @@
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
<p>
Go
<router-link to="import-account" class="text-blue-500">
import another mnemonic here.
<router-link to="start" class="text-blue-500">
create another identity here.
</router-link>
</p>
@@ -216,6 +213,14 @@
<p>
{{ package.version }}
</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>
</section>
</template>

View File

@@ -1,15 +1,360 @@
<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>
<script lang="ts">
import * as R from "ramda";
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({
components: {
HelloWorld,
},
components: { GiftedDialog },
})
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>

View File

@@ -49,8 +49,9 @@
<span :class="{ hidden: isHiddenSave }">Save Project</span>
<!-- SHOW if in saving state; DISABLE button while in saving state -->
<span :class="{ hidden: isHiddenSpinner }"
><i class="fa-solid fa-spinner fa-spin-pulse"></i>
<span :class="{ hidden: isHiddenSpinner }">
<!-- icon no worky? -->
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
Saving&hellip;</span
>
</button>
@@ -122,11 +123,14 @@ export default class NewEditProjectView extends Vue {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.error("Problem! Should have a profile!");
console.error("Error: no account was found.");
} else {
const accounts = await accountsDB.accounts.toArray();
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);
}
}
@@ -222,7 +226,7 @@ export default class NewEditProjectView extends Vue {
);
}
} 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;
if (serverError) {
this.isAlertVisible = true;
@@ -255,11 +259,14 @@ export default class NewEditProjectView extends Vue {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.error("Problem! Should have a profile!");
console.error("Error: there is no account.");
} else {
const accounts = await accountsDB.accounts.toArray();
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);
}
}

View File

@@ -0,0 +1,126 @@
<template>
<!-- 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 text-slate-500">
<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 bg-slate-400 text-white">
<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">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Identity
</h1>
<div class="flex justify-center py-12">
<span />
<span v-if="loading">
<span class="text-xl">Creating...&nbsp;</span>
<fa
icon="spinner"
class="fa-spin fa-spin-pulse"
color="green"
size="128"
></fa>
</span>
<span v-else>
<span class="text-xl">Created!</span>
<fa
icon="burst"
class="fa-beat px-12"
color="green"
style="
--fa-animation-duration: 1s;
--fa-animation-direction: reverse;
--fa-animation-iteration-count: 1;
--fa-beat-scale: 6;
"
></fa>
</span>
<span />
</div>
</section>
</template>
<script lang="ts">
import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component
export default class AccountViewView extends Vue {
loading = true;
async mounted() {
await accountsDB.open();
const mnemonic = generateSeed();
// address is 0x... ETH address, without "did:eth:"
const [address, privateHex, publicHex, derivationPath] =
deriveAddress(mnemonic);
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
const identity = JSON.stringify(newId);
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: derivationPath,
did: newId.did,
identity: identity,
mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex,
});
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
this.loading = false;
setTimeout(() => {
this.$router.push({ name: "account" });
}, 1000);
}
}
</script>

View File

@@ -240,7 +240,10 @@ export default class ProjectViewView extends Vue {
} else {
const accounts = await accountsDB.accounts.toArray();
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);
}
}

View File

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

View File

@@ -35,7 +35,7 @@ import { Options, Vue } from "vue-class-component";
})
export default class StartView extends Vue {
public onClickYes() {
this.$router.push({ name: "account" });
this.$router.push({ name: "new-identifier" });
}
public onClickNo() {

View File

@@ -61,7 +61,7 @@
<!-- 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>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>
<!-- eslint-enable -->
</ul>
@@ -75,7 +75,7 @@
{{ worldProperties.endTime }}
</div>
<div v-if="worldProperties.animationDurationSeconds">
<label>Animation duration:&nbsp;</label>
<label>Animation Time:&nbsp;</label>
{{ worldProperties.animationDurationSeconds }} seconds
</div>
</div>
@@ -113,6 +113,7 @@ export default class StatisticsView extends Vue {
world: World;
worldProperties: WorldProperties = {};
// 'mounted' hook runs after initial render
mounted() {
const container = document.querySelector("#scene-container");
const newWorld = new World(container, this);