Browse Source

load feed of give records on home screen

world-fix
Trent Larson 1 year ago
parent
commit
6daa515d19
  1. 4
      project.task.yaml
  2. 2
      src/components/InfiniteScroll.vue
  3. 4
      src/db/index.ts
  4. 30
      src/libs/endorserServer.ts
  5. 26
      src/router/index.ts
  6. 3
      src/views/AccountViewView.vue
  7. 228
      src/views/HomeView.vue
  8. 6
      src/views/NewEditProjectView.vue
  9. 1
      src/views/StatisticsView.vue

4
project.task.yaml

@ -31,7 +31,9 @@
- Ensure each action sent to the server has a confirmation. - Ensure each action sent to the server has a confirmation.
- discover screen - Feed screen
- 01 save the feed in settings storage
- .5 add user-specific data
- .5 customize favicon - .5 customize favicon
- .5 make advanced features harder to access; advanced build? - .5 make advanced features harder to access; advanced build?

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();
} }

4
src/db/index.ts

@ -44,8 +44,8 @@ const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
/** /**
* 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, even if the secret * It's good practice to keep the data encrypted at rest, so we'll do that even
* is stored next to the app. * if the secret is stored right next to the app.
*/ */
const secret = const secret =
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey(); localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();

30
src/libs/endorserServer.ts

@ -47,3 +47,33 @@ export interface RegisterVerifiableCredential {
object: string; object: string;
recipient: { identifier: string }; recipient: { identifier: string };
} }
// 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";
const UNKNOWN_ENTITY = "Someone Unknown";
const UNKNOWN_VISIBLE = "Someone Unnamed";
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 || "(no name)";
} else if (isHiddenDid(did)) {
return UNKNOWN_ENTITY;
} else {
return UNKNOWN_VISIBLE;
}
}
}

26
src/router/index.ts

@ -1,13 +1,13 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { accountsDB } from "@/db"; import { accountsDB } from "@/db";
const routes: Array<RouteRecordRaw> = [ /**
{ *
path: "/", * @param to :RouteLocationNormalized
name: "home", * @param from :RouteLocationNormalized
component: () => * @param next :NavigationGuardNext
import(/* webpackChunkName: "start" */ "../views/DiscoverView.vue"), */
beforeEnter: async (to, from, next) => { const enterOrStart = async (to, from, next) => {
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) {
@ -15,7 +15,15 @@ const routes: Array<RouteRecordRaw> = [
} else { } else {
next({ name: "start" }); next({ name: "start" });
} }
}, };
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "home",
component: () =>
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
beforeEnter: enterOrStart,
}, },
{ {
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",

3
src/views/AccountViewView.vue

@ -383,11 +383,12 @@ 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()
//testServerRegisterUser(); //testServerRegisterUser();
try { try {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);

228
src/views/HomeView.vue

@ -1,15 +1,231 @@
<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">
Feed
</h1>
<span :class="{ hidden: isHiddenSpinner }">
<fa icon="spinner" class="fa-fw"></fa>
Loading&hellip;
</span>
<div>
<!-- Results List -->
<ul class="">
<li
class="border-b border-slate-300"
v-for="record in feedData"
:key="record.id"
>
{{ 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 { 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 { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { didInfo } from "@/libs/endorserServer";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import * as R from "ramda";
@Options({ @Options({
components: { components: {},
HelloWorld,
},
}) })
export default class HomeView extends Vue {} export default class HomeView extends Vue {
accounts: Array<Account> = [];
apiServer = "";
contacts: Array<Contact> = [];
feedAllLoaded = false;
feedData = [];
feedPreviousOldestId = null;
isHiddenSpinner = true;
// 'created' hook runs when the Vue instance is first created
async created() {
await accountsDB.open();
this.accounts = await accountsDB.accounts.toArray();
console.log("Accounts:", this.accounts);
await db.open();
this.contacts = await db.contacts.toArray();
}
// '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].id;
}
})
.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 token = await accessToken(identifier)
//const afterQuery = afterId == null ? "" : "&afterId=" + afterId;
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
return fetch(this.apiServer + "/api/v2/report/gives?" + beforeQuery, {
method: "GET",
headers: {
"Content-Type": "application/json",
//"Uport-Push-Token": token,
},
})
.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 = giver; //didInfo(giver, identifiers, contacts);
const gaveAmount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: claim.object;
// recipient.did is for legacy data, before March 2023
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
const gaveRecipientInfo = gaveRecipientId
? " to " + gaveRecipientId //didInfo(gaveRecipientId, identifiers, contacts)
: "";
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;
}
// 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>

6
src/views/NewEditProjectView.vue

@ -122,7 +122,7 @@ 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);
@ -222,7 +222,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,7 +254,7 @@ 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);

1
src/views/StatisticsView.vue

@ -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