add more contact management, including registration & visibility #14

Merged
anomalist merged 11 commits from separate-dbs into master 2 years ago
  1. 7
      package-lock.json
  2. 1
      package.json
  3. 7
      project.yaml
  4. 60
      src/main.ts
  5. 126
      src/views/AccountViewView.vue
  6. 511
      src/views/ContactsView.vue
  7. 12
      src/views/HelpView.vue
  8. 6
      src/views/ProjectsView.vue

7
package-lock.json

@ -23,6 +23,7 @@
"@vueuse/core": "^9.6.0", "@vueuse/core": "^9.6.0",
"@zxing/text-encoding": "^0.9.0", "@zxing/text-encoding": "^0.9.0",
"axios": "^1.2.2", "axios": "^1.2.2",
"buffer": "^6.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"core-js": "^3.26.1", "core-js": "^3.26.1",
"dexie": "^3.2.2", "dexie": "^3.2.2",
@ -7510,9 +7511,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "18.11.9", "version": "18.15.5",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-18.11.9.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.5.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" "integrity": "sha512-Ark2WDjjZO7GmvsyFFf81MXuGTA/d6oP38anyxWOL6EREyBKAxKoFHwBhaZxCfLRLpO8JgVXwqOwSwa7jRcjew=="
}, },
"node_modules/@types/normalize-package-data": { "node_modules/@types/normalize-package-data": {
"version": "2.4.1", "version": "2.4.1",

1
package.json

@ -23,6 +23,7 @@
"@vueuse/core": "^9.6.0", "@vueuse/core": "^9.6.0",
"@zxing/text-encoding": "^0.9.0", "@zxing/text-encoding": "^0.9.0",
"axios": "^1.2.2", "axios": "^1.2.2",
"buffer": "^6.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"core-js": "^3.26.1", "core-js": "^3.26.1",
"dexie": "^3.2.2", "dexie": "^3.2.2",

7
project.yaml

@ -12,12 +12,15 @@
- replace user-affecting console.logs with error messages (eg. catches) - replace user-affecting console.logs with error messages (eg. catches)
- contacts v1 : - contacts v1 :
- .2 show gives with new setting - test confirmed vs unconfirmed amounts
- remove 'copy' until it works
- switch to prod server
- 01 show gives with confirmations - 01 show gives with confirmations
- .5 Add page to show seed. - .5 Add page to show seed.
- 01 Provide a way to import the non-sensitive data. - 01 Provide a way to import the non-sensitive data.
- 01 Provide way to share your contact info. - 01 Provide way to share your contact info.
- .1 remove "scan new contact" - .2 move all "identity" references to temporary account access
- get 'copy' to work on account page
- contacts v+ : - contacts v+ :
- .5 make advanced "show/hide amounts" button into a nice UI toggle - .5 make advanced "show/hide amounts" button into a nice UI toggle

60
src/main.ts

@ -10,46 +10,58 @@ import "./assets/styles/tailwind.css";
import { library } from "@fortawesome/fontawesome-svg-core"; import { library } from "@fortawesome/fontawesome-svg-core";
import { import {
faCalendar,
faChevronLeft, faChevronLeft,
faHouseChimney, faCircleCheck,
faMagnifyingGlass, faCircleQuestion,
faFolderOpen,
faHand,
faCircleUser, faCircleUser,
faCopy, faCopy,
faShareNodes, faEllipsisVertical,
faQrcode, faEye,
faUser, faEyeSlash,
faUsers, faFolderOpen,
faHand,
faHouseChimney,
faMagnifyingGlass,
faPen, faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus, faPlus,
faTrashCan, faQrcode,
faCalendar, faRotate,
faEllipsisVertical, faShareNodes,
faSpinner, faSpinner,
faCircleCheck, faTrashCan,
faUser,
faUsers,
faXmark, faXmark,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
library.add( library.add(
faCalendar,
faChevronLeft, faChevronLeft,
faHouseChimney, faCircleCheck,
faMagnifyingGlass, faCircleQuestion,
faFolderOpen,
faHand,
faCircleUser, faCircleUser,
faCopy, faCopy,
faShareNodes, faEllipsisVertical,
faQrcode, faEye,
faUser, faEyeSlash,
faUsers, faFolderOpen,
faHand,
faHouseChimney,
faMagnifyingGlass,
faPen, faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus, faPlus,
faTrashCan, faQrcode,
faCalendar, faRotate,
faEllipsisVertical, faShareNodes,
faSpinner, faSpinner,
faCircleCheck, faTrashCan,
faUser,
faUsers,
faXmark faXmark
); );

126
src/views/AccountViewView.vue

@ -110,10 +110,20 @@
</span> </span>
</div> </div>
<div class="text-slate-500 text-sm font-bold">Public Key</div> <div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
<div class="text-sm text-slate-500 mb-1"> <div class="text-sm text-slate-500 mb-1">
<span <span>
><code>{{ publicHex }}</code> <code>{{ publicBase64 }}</code>
<button @click="copy(publicBase64)">
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
</button>
</span>
</div>
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
<div class="text-sm text-slate-500 mb-1">
<span>
<code>{{ publicHex }}</code>
<button @click="copy(publicHex)"> <button @click="copy(publicHex)">
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa> <fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
</button> </button>
@ -138,15 +148,6 @@
Edit Identity Edit Identity
</router-link> </router-link>
<h3 class="text-sm uppercase font-semibold mb-3">Contact Actions</h3>
<a
href="contact-scan.html"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
>
Scan New Contact
</a>
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3> <h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
<a <a
@ -193,7 +194,7 @@
href="" href=""
class="text-center text-md text-white px-1.5 py-2 rounded-md mb-6" class="text-center text-md text-white px-1.5 py-2 rounded-md mb-6"
v-bind:class="showContactGivesClassNames()" v-bind:class="showContactGivesClassNames()"
@click="toggleShowContactAmounts" @click="toggleShowContactAmounts()"
> >
{{ showContactGives ? "Showing" : "Hiding" }} {{ showContactGives ? "Showing" : "Hiding" }}
amounts given with contacts (Click to amounts given with contacts (Click to
@ -201,6 +202,29 @@
</button> </button>
</div> </div>
<div class="flex">
<button
class="text-center text-md text-blue-500 px-1.5 py-2"
@click="checkLimits()"
>
Check Limits
</button>
<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 v-bind:class="computedAlertClassNames()"> <div v-bind:class="computedAlertClassNames()">
<button <button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2" class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@ -220,23 +244,47 @@ import { Options, Vue } from "vue-class-component";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { db, accountsDB } from "@/db"; import { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import {
accessToken,
deriveAddress,
generateSeed,
newIdentifier,
} from "@/libs/crypto";
import { AppString } from "@/constants/app";
//import { testServerRegisterUser } from "../test"; //import { testServerRegisterUser } from "../test";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
interface RateLimits {
doneClaimsThisWeek: string;
doneRegistrationsThisMonth: string;
maxClaimsPerWeek: string;
maxRegistrationsPerMonth: string;
nextMonthBeginDateTime: string;
nextWeekBeginDateTime: string;
}
@Options({ @Options({
components: {}, components: {},
}) })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
address = ""; address = "";
derivationPath = "";
firstName = ""; firstName = "";
lastName = ""; lastName = "";
mnemonic = ""; mnemonic = "";
publicHex = ""; publicHex = "";
derivationPath = ""; publicBase64 = "";
limits: RateLimits | null = null;
showContactGives = false; showContactGives = false;
copy = useClipboard().copy; copy = useClipboard().copy;
readableTime(timeStr: string) {
return timeStr.substring(0, timeStr.indexOf("T"));
}
// '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 to register this user on the test server.
@ -281,6 +329,7 @@ export default class AccountViewView extends Vue {
const identity = JSON.parse(accounts[0].identity); const identity = JSON.parse(accounts[0].identity);
this.address = identity.did; this.address = identity.did;
this.publicHex = identity.keys[0].publicKeyHex; this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta.derivationPath; this.derivationPath = identity.keys[0].meta.derivationPath;
} catch (err) { } catch (err) {
this.alertMessage = this.alertMessage =
@ -310,13 +359,6 @@ export default class AccountViewView extends Vue {
} }
} }
public showContactGivesClassNames() {
return {
"bg-slate-900": !this.showContactGives,
"bg-green-600": this.showContactGives,
};
}
public async exportDatabase() { public async exportDatabase() {
try { try {
const blob = await db.export({ prettyJson: true }); const blob = await db.export({ prettyJson: true });
@ -340,6 +382,46 @@ export default class AccountViewView extends Vue {
} }
} }
async checkLimits() {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/report/rateLimits";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
this.limits = resp.data;
} else {
this.alertTitle = "Error from Server";
console.log("Bad response retrieving limits: ", resp.data);
if (resp.data.error?.message) {
this.alertMessage = resp.data.error?.message;
} else {
this.alertMessage = "Bad server response of " + resp.status;
}
this.isAlertVisible = true;
}
} catch (err) {
this.alertTitle = "Error from Server";
this.alertMessage = err as string;
this.isAlertVisible = true;
}
}
public showContactGivesClassNames() {
return {
"bg-slate-900": !this.showContactGives,
"bg-green-600": this.showContactGives,
};
}
alertMessage = ""; alertMessage = "";
alertTitle = ""; alertTitle = "";
isAlertVisible = false; isAlertVisible = false;

511
src/views/ContactsView.vue

@ -9,7 +9,7 @@
></router-link> ></router-link>
</li> </li>
<!-- Search --> <!-- Search -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link <router-link
:to="{ name: 'discover' }" :to="{ name: 'discover' }"
class="block text-center py-3 px-1" class="block text-center py-3 px-1"
@ -25,7 +25,7 @@
></router-link> ></router-link>
</li> </li>
<!-- Contacts --> <!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link <router-link
:to="{ name: 'contacts' }" :to="{ name: 'contacts' }"
class="block text-center py-3 px-1" class="block text-center py-3 px-1"
@ -46,7 +46,7 @@
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
My Contacts Your Contacts
</h1> </h1>
<!-- New Contact --> <!-- New Contact -->
@ -65,7 +65,7 @@
</button> </button>
</div> </div>
<div class="flex justify-between" v-if="showGiveTotals"> <div class="flex justify-between" v-if="showGiveNumbers">
<div class="w-full text-right"> <div class="w-full text-right">
Hours to Add: Hours to Add:
<input <input
@ -81,6 +81,22 @@
placeholder="Description" placeholder="Description"
v-model="hourDescriptionInput" v-model="hourDescriptionInput"
/> />
<br />
<br />
<button
href=""
class="text-center text-md text-white px-1.5 py-2 rounded-md mb-6"
v-bind:class="showGiveAmountsClassNames()"
@click="toggleShowGiveTotals()"
>
{{
showGiveTotals
? "Totals"
: showGiveConfirmed
? "Confirmed"
: "Unconfirmed"
}}
</button>
</div> </div>
</div> </div>
@ -96,17 +112,84 @@
{{ contact.name || "(no name)" }} {{ contact.name || "(no name)" }}
</h2> </h2>
<div class="text-sm truncate">{{ contact.did }}</div> <div class="text-sm truncate">{{ contact.did }}</div>
<div class="text-sm truncate">{{ contact.publicKeyBase64 }}</div> <div class="text-sm truncate" v-if="contact.publicKeyBase64">
<div v-if="showGiveTotals" class="float-right"> Public Key (base 64): {{ contact.publicKeyBase64 }}
</div>
<button
v-if="contact.seesMe"
class="tooltip"
@click="setVisibility(contact, false)"
>
<fa icon="eye" class="text-slate-900 fa-fw ml-1" />
<span class="tooltiptext">Can see you</span>
</button>
<button v-else class="tooltip" @click="setVisibility(contact, true)">
<span class="tooltiptext">Cannot see you</span>
<fa icon="eye-slash" class="text-slate-900 fa-fw ml-1" />
</button>
<button class="tooltip" @click="checkVisibility(contact)">
<span class="tooltiptext">Check Visibility</span>
<fa icon="rotate" class="text-slate-900 fa-fw ml-1" />
</button>
<button v-if="contact.registered" class="tooltip">
<span class="tooltiptext">Registered</span>
<fa icon="person-circle-check" class="text-slate-900 fa-fw ml-1" />
</button>
<button v-else @click="register(contact)" class="tooltip">
<span class="tooltiptext">Maybe not registered</span>
<fa
icon="person-circle-question"
class="text-slate-900 fa-fw ml-1"
/>
</button>
<button @click="deleteContact(contact)" class="px-9 tooltip">
<span class="tooltiptext">Delete!</span>
<fa icon="trash-can" class="text-red-600 fa-fw ml-1" />
</button>
<div v-if="showGiveNumbers" class="float-right">
<div class="float-right"> <div class="float-right">
to: {{ givenByMeTotals[contact.did] || 0 }} <div class="tooltip">
to:
{{
/* eslint-disable prettier/prettier */
this.showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0))
: this.showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
<span class="tooltiptext-left">{{
givenByMeDescriptions[contact.did]
}}</span>
<button <button
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="onClickAddGive(identity.did, contact.did)" @click="onClickAddGive(identity.did, contact.did)"
> >
+ +
</button> </button>
by: {{ givenToMeTotals[contact.did] || 0 }} </div>
<div class="tooltip px-2">
by:
{{
/* eslint-disable prettier/prettier */
this.showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: this.showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
<span class="tooltiptext-left">
{{ givenToMeDescriptions[contact.did] }}
</span>
<button <button
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="onClickAddGive(contact.did, identity.did)" @click="onClickAddGive(contact.did, identity.did)"
@ -116,6 +199,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</li> </li>
</ul> </ul>
</section> </section>
@ -135,13 +219,30 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core"; import { accountsDB, db } from "@/db";
import { accountsDB, db } from "../db"; import { Contact } from "@/db/tables/contacts";
import { Contact } from "../db/tables/contacts"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
const SERVICE_ID = "endorser.ch";
export interface GiveServerRecord {
agentDid: string;
amount: number;
confirmed: number;
description: string;
fullClaim: GiveVerifiableCredential;
handleId: string;
recipientDid: string;
unit: string;
}
export interface GiveVerifiableCredential { export interface GiveVerifiableCredential {
"@context": string; "@context": string;
@ -152,20 +253,38 @@ export interface GiveVerifiableCredential {
recipient: { identifier: string }; recipient: { identifier: string };
} }
export interface RegisterVerifiableCredential {
"@context": string;
"@type": string;
agent: { identifier: string };
object: string;
recipient: { identifier: string };
}
@Options({ @Options({
components: {}, components: {},
}) })
export default class ContactsView extends Vue { export default class ContactsView extends Vue {
contacts: Array<Contact> = []; contacts: Array<Contact> = [];
contactInput = ""; contactInput = "";
// { "did:...": concatenated-descriptions } entry for each contact
givenByMeDescriptions: Record<string, string> = {};
// { "did:...": amount } entry for each contact
givenByMeConfirmed: Record<string, number> = {};
// { "did:...": amount } entry for each contact // { "did:...": amount } entry for each contact
givenByMeTotals: Record<string, number> = {}; givenByMeUnconfirmed: Record<string, number> = {};
// { "did:...": concatenated-descriptions } entry for each contact
givenToMeDescriptions: Record<string, string> = {};
// { "did:...": amount } entry for each contact // { "did:...": amount } entry for each contact
givenToMeTotals: Record<string, number> = {}; givenToMeConfirmed: Record<string, number> = {};
// { "did:...": amount } entry for each contact
givenToMeUnconfirmed: Record<string, number> = {};
hourDescriptionInput = ""; hourDescriptionInput = "";
hourInput = "0"; hourInput = "0";
identity: IIdentifier | null = null; identity: IIdentifier | null = null;
showGiveTotals = false; showGiveNumbers = false;
showGiveTotals = true;
showGiveConfirmed = true;
// 'created' hook runs when the Vue instance is first created // 'created' hook runs when the Vue instance is first created
async created() { async created() {
@ -174,13 +293,16 @@ export default class ContactsView extends Vue {
this.identity = JSON.parse(accounts[0].identity); this.identity = JSON.parse(accounts[0].identity);
await db.open(); await db.open();
this.contacts = await db.contacts.toArray(); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.showGiveNumbers = !!settings?.showContactGivesInline;
const params = new URLSearchParams(window.location.search); if (this.showGiveNumbers) {
this.showGiveTotals = params.get("showGiveTotals") == "true";
if (this.showGiveTotals) {
this.loadGives(); this.loadGives();
} }
const allContacts = await db.contacts.toArray();
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts
);
} }
async onClickNewContact(): Promise<void> { async onClickNewContact(): Promise<void> {
@ -196,9 +318,18 @@ export default class ContactsView extends Vue {
publicKeyBase64 = this.contactInput.substring(commaPos2 + 1).trim(); publicKeyBase64 = this.contactInput.substring(commaPos2 + 1).trim();
} }
} }
// help with potential mistakes while this sharing requires copy-and-paste
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
// it must be all hex (compressed public key), so convert
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
}
const newContact = { did, name, publicKeyBase64 }; const newContact = { did, name, publicKeyBase64 };
await db.contacts.add(newContact); await db.contacts.add(newContact);
this.contacts = this.contacts.concat([newContact]); const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts
);
} }
async loadGives() { async loadGives() {
@ -223,18 +354,30 @@ export default class ContactsView extends Vue {
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
}; };
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
//console.log("Server response", resp.status, resp.data); console.log("All your gifts:", resp.data);
if (resp.status === 200) { if (resp.status === 200) {
const contactTotals: Record<string, number> = {}; const contactDescriptions: Record<string, string> = {};
for (const give of resp.data.data) { const contactConfirmed: Record<string, number> = {};
const contactUnconfirmed: Record<string, number> = {};
const allData: Array<GiveServerRecord> = resp.data.data;
for (const give of allData) {
if (give.unit == "HUR") { if (give.unit == "HUR") {
const recipDid: string = give.recipientDid; const recipDid: string = give.recipientDid;
const prevAmount = contactTotals[recipDid] || 0; if (give.confirmed) {
contactTotals[recipDid] = prevAmount + give.amount; const prevAmount = contactConfirmed[recipDid] || 0;
contactConfirmed[recipDid] = prevAmount + give.amount;
} else {
const prevAmount = contactUnconfirmed[recipDid] || 0;
contactUnconfirmed[recipDid] = prevAmount + give.amount;
} }
const prevDesc = contactDescriptions[recipDid] || "";
// Since many make the tooltip too big, we'll just use the latest;
contactDescriptions[recipDid] = give.description || prevDesc;
} }
//console.log("Done retrieving gives", contactTotals); }
this.givenByMeTotals = contactTotals; //console.log("Done retrieving gives", contactConfirmed);
this.givenByMeDescriptions = contactDescriptions;
this.givenByMeConfirmed = contactConfirmed;
} }
} catch (error) { } catch (error) {
this.alertTitle = "Error from Server"; this.alertTitle = "Error from Server";
@ -254,17 +397,29 @@ export default class ContactsView extends Vue {
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
}; };
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
//console.log("Server response", resp.status, resp.data); console.log("All gifts you've recieved:", resp.data);
if (resp.status === 200) { if (resp.status === 200) {
const contactTotals: Record<string, number> = {}; const contactDescriptions: Record<string, string> = {};
for (const give of resp.data.data) { const contactConfirmed: Record<string, number> = {};
const contactUnconfirmed: Record<string, number> = {};
const allData: Array<GiveServerRecord> = resp.data.data;
for (const give of allData) {
if (give.unit == "HUR") { if (give.unit == "HUR") {
const prevAmount = contactTotals[give.agentDid] || 0; if (give.confirmed) {
contactTotals[give.agentDid] = prevAmount + give.amount; const prevAmount = contactConfirmed[give.agentDid] || 0;
contactConfirmed[give.agentDid] = prevAmount + give.amount;
} else {
const prevAmount = contactUnconfirmed[give.agentDid] || 0;
contactUnconfirmed[give.agentDid] = prevAmount + give.amount;
}
const prevDesc = contactDescriptions[give.agentDid] || "";
// Since many make the tooltip too big, we'll just use the latest;
contactDescriptions[give.agentDid] = give.description || prevDesc;
} }
} }
//console.log("Done retrieving receipts", contactTotals); //console.log("Done retrieving receipts", contactConfirmed);
this.givenToMeTotals = contactTotals; this.givenToMeDescriptions = contactDescriptions;
this.givenToMeConfirmed = contactConfirmed;
} }
} catch (error) { } catch (error) {
this.alertTitle = "Error from Server"; this.alertTitle = "Error from Server";
@ -273,6 +428,187 @@ export default class ContactsView extends Vue {
} }
} }
async deleteContact(contact: Contact) {
if (
confirm(
"Are you sure you want to delete " +
this.nameForDid(this.contacts, contact.did) +
" with DID " +
contact.did +
" ?"
)
) {
await db.open();
await db.contacts.delete(contact.did);
this.contacts = R.without([contact], this.contacts);
}
}
async register(contact: Contact) {
if (
confirm(
"Are you sure you want to use one of your registrations for " +
this.nameForDid(this.contacts, contact.did) +
"?"
)
) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity);
// Make a claim
const vcClaim: RegisterVerifiableCredential = {
"@context": "https://schema.org",
"@type": "RegisterAction",
agent: { identifier: identity.did },
object: SERVICE_ID,
recipient: { identifier: contact.did },
};
// 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) {
// 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 endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.post(url, payload, { headers });
//console.log("Got resp data:", resp.data);
if (resp.data?.success?.handleId) {
contact.registered = true;
db.contacts.update(contact.did, { registered: true });
this.alertTitle = "Registration Success";
this.alertMessage = contact.name + " has been registered.";
this.isAlertVisible = true;
}
} catch (error) {
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
if (serverError.message) {
userMessage = serverError.message; // Info for the user
} else {
userMessage = JSON.stringify(serverError.toJSON());
}
} else {
userMessage = error as string;
}
// Now set that error for the user to see.
this.alertTitle = "Error with Server";
this.alertMessage = userMessage;
this.isAlertVisible = true;
}
}
}
}
async setVisibility(contact: Contact, visibility: boolean) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url =
endorserApiServer +
"/api/report/" +
(visibility ? "canSeeMe" : "cannotSeeMe");
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const payload = JSON.stringify({ did: contact.did });
try {
const resp = await this.axios.post(url, payload, { headers });
if (resp.status === 200) {
contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility });
} else {
this.alertTitle = "Error from Server";
console.log("Bad response setting visibility: ", resp.data);
if (resp.data.error?.message) {
this.alertMessage = resp.data.error?.message;
} else {
this.alertMessage = "Bad server response of " + resp.status;
}
this.isAlertVisible = true;
}
} catch (err) {
this.alertTitle = "Error from Server";
this.alertMessage = err as string;
this.isAlertVisible = true;
}
}
async checkVisibility(contact: Contact) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url =
endorserApiServer +
"/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did);
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const visibility = resp.data;
contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility });
this.alertTitle = "Refreshed";
this.alertMessage =
this.nameForContact(contact, true) +
" can " +
(visibility ? "" : "not ") +
"see your activity.";
this.isAlertVisible = true;
} else {
this.alertTitle = "Error from Server";
if (resp.data.error?.message) {
this.alertMessage = resp.data.error?.message;
} else {
this.alertMessage = "Bad server response of " + resp.status;
}
this.isAlertVisible = true;
}
} catch (err) {
this.alertTitle = "Error from Server";
this.alertMessage = err as string;
this.isAlertVisible = true;
}
}
// from https://stackoverflow.com/a/175787/845494 // from https://stackoverflow.com/a/175787/845494
// //
private isNumeric(str: string): boolean { private isNumeric(str: string): boolean {
@ -281,7 +617,11 @@ export default class ContactsView extends Vue {
private nameForDid(contacts: Array<Contact>, did: string): string { private nameForDid(contacts: Array<Contact>, did: string): string {
const contact = R.find((con) => con.did == did, contacts); const contact = R.find((con) => con.did == did, contacts);
return contact?.name || "this unnamed user"; return this.nameForContact(contact);
}
private nameForContact(contact?: Contact, capitalize?: boolean): string {
return contact?.name || (capitalize ? "T" : "t") + "this unnamed user";
} }
async onClickAddGive(fromDid: string, toDid: string): Promise<void> { async onClickAddGive(fromDid: string, toDid: string): Promise<void> {
@ -305,12 +645,19 @@ export default class ContactsView extends Vue {
} else { } else {
toFrom = "from " + this.nameForDid(this.contacts, fromDid) + " to you"; toFrom = "from " + this.nameForDid(this.contacts, fromDid) + " to you";
} }
let description;
if (this.hourDescriptionInput) {
description = " with description '" + this.hourDescriptionInput + "'";
} else {
description = " with no description";
}
if ( if (
confirm( confirm(
"Are you sure you want to record " + "Are you sure you want to record " +
this.hourInput + this.hourInput +
" hours " + " hours " +
toFrom + toFrom +
description +
"?" "?"
) )
) { ) {
@ -382,16 +729,17 @@ export default class ContactsView extends Vue {
this.alertTitle = ""; this.alertTitle = "";
this.alertMessage = ""; this.alertMessage = "";
if (fromDid === identity.did) { if (fromDid === identity.did) {
this.givenByMeTotals[toDid] = this.givenByMeTotals[toDid] + amount; this.givenByMeConfirmed[toDid] =
this.givenByMeConfirmed[toDid] + amount;
// do this to update the UI (is there a better way?) // do this to update the UI (is there a better way?)
// eslint-disable-next-line no-self-assign // eslint-disable-next-line no-self-assign
this.givenByMeTotals = this.givenByMeTotals; this.givenByMeConfirmed = this.givenByMeConfirmed;
} else { } else {
this.givenToMeTotals[fromDid] = this.givenToMeConfirmed[fromDid] =
this.givenToMeTotals[fromDid] + amount; this.givenToMeConfirmed[fromDid] + amount;
// do this to update the UI (is there a better way?) // do this to update the UI (is there a better way?)
// eslint-disable-next-line no-self-assign // eslint-disable-next-line no-self-assign
this.givenToMeTotals = this.givenToMeTotals; this.givenToMeConfirmed = this.givenToMeConfirmed;
} }
} }
} catch (error) { } catch (error) {
@ -414,6 +762,32 @@ export default class ContactsView extends Vue {
} }
} }
public selectedGiveTotal(
contactGivesConfirmed: Record<string, number>,
contactGivesUnconfirmed: Record<string, number>,
did: string
) {
/* eslint-disable prettier/prettier */
this.showGiveTotals
? ((contactGivesConfirmed[did] || 0) + (contactGivesUnconfirmed[did] || 0))
: this.showGiveConfirmed
? (contactGivesConfirmed[did] || 0)
: (contactGivesUnconfirmed[did] || 0);
/* eslint-enable prettier/prettier */
}
public toggleShowGiveTotals() {
if (this.showGiveTotals) {
this.showGiveTotals = false;
this.showGiveConfirmed = true;
} else if (this.showGiveConfirmed) {
this.showGiveTotals = false; // stays the same
this.showGiveConfirmed = false;
} else {
this.showGiveTotals = true;
this.showGiveConfirmed = true;
}
}
alertTitle = ""; alertTitle = "";
alertMessage = ""; alertMessage = "";
isAlertVisible = false; isAlertVisible = false;
@ -440,5 +814,62 @@ export default class ContactsView extends Vue {
"duration-300": true, "duration-300": true,
}; };
} }
public showGiveAmountsClassNames() {
return {
"bg-slate-900": this.showGiveTotals,
"bg-green-600": !this.showGiveTotals && this.showGiveConfirmed,
"bg-yellow-600": !this.showGiveTotals && !this.showGiveConfirmed,
};
}
} }
</script> </script>
<style>
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
/* Tooltip container */
.tooltip {
position: relative;
display: inline-block;
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
}
/* Tooltip text */
.tooltip .tooltiptext {
visibility: hidden;
width: 200px;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 1;
}
/* How do we share with the above so code isn't duplicated? */
.tooltip .tooltiptext-left {
visibility: hidden;
width: 200px;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 1;
bottom: 0%;
right: 105%;
margin-left: -60px;
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltiptext {
visibility: visible;
}
.tooltip:hover .tooltiptext-left {
visibility: visible;
}
</style>

12
src/views/HelpView.vue

@ -134,6 +134,18 @@
<li>Make sure you have your backup file (above), then contact us.</li> <li>Make sure you have your backup file (above), then contact us.</li>
</ul> </ul>
</div> </div>
<h2 class="text-xl font-semibold">
How do I add someone to my contacts?
</h2>
<p>
Tell them to copy their ID, which typically starts with "did:ethr:...",
and send it to you. Go to the Contacts
<fa icon="circle-user" class="fa-fw" /> page and enter that into the top
form. You may add a name by adding a comma followed by their name; you
may also add their public key by adding another comma followed by the
key.
</p>
</div> </div>
</section> </section>
</template> </template>

6
src/views/ProjectsView.vue

@ -9,7 +9,7 @@
></router-link> ></router-link>
</li> </li>
<!-- Search --> <!-- Search -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link <router-link
:to="{ name: 'discover' }" :to="{ name: 'discover' }"
class="block text-center py-3 px-1" class="block text-center py-3 px-1"
@ -17,7 +17,7 @@
></router-link> ></router-link>
</li> </li>
<!-- Projects --> <!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link <router-link
:to="{ name: 'projects' }" :to="{ name: 'projects' }"
class="block text-center py-3 px-1" class="block text-center py-3 px-1"
@ -45,7 +45,7 @@
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
My Plans Your Plans
</h1> </h1>
<!-- Quick Search --> <!-- Quick Search -->

Loading…
Cancel
Save