Browse Source

load feed of give records on home screen

pull/23/head
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. 30
      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.
- discover screen
- Feed screen
- 01 save the feed in settings storage
- .5 add user-specific data
- .5 customize favicon
- .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;
private observer!: IntersectionObserver;
// 'mounted' hook runs after initial render
mounted() {
const options = {
root: this.$refs.scrollContainer as HTMLElement,
@ -24,6 +25,7 @@ export default class InfiniteScroll extends Vue {
this.observer.observe(this.$refs.sentinel as HTMLElement);
}
// 'beforeUnmount' hook runs before unmounting the component
beforeUnmount() {
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.
*
* It's good practice to keep the data encrypted at rest, even if the secret
* is stored next to the app.
* 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 =
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();

30
src/libs/endorserServer.ts

@ -47,3 +47,33 @@ export interface RegisterVerifiableCredential {
object: 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;
}
}
}

30
src/router/index.ts

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

228
src/views/HomeView.vue

@ -1,15 +1,231 @@
<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>
<script lang="ts">
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({
components: {
HelloWorld,
},
components: {},
})
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>

6
src/views/NewEditProjectView.vue

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

1
src/views/StatisticsView.vue

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

Loading…
Cancel
Save