Browse Source

Merge remote-tracking branch 'original-origin/master' into feat/vitejs-trent

kb/add-usage-guide
Trent Larson 7 months ago
parent
commit
5828a290c7
  1. 11
      CHANGELOG.md
  2. 4
      README.md
  3. 6444
      package-lock.json
  4. 3
      package.json
  5. 39
      project.task.yaml
  6. 114
      src/App.vue
  7. 219
      src/components/FeedFilters.vue
  8. 88
      src/components/GiftedDialog.vue
  9. 74
      src/components/GiftedPhotoDialog.vue
  10. 10
      src/components/GiftedPrompts.vue
  11. 4
      src/components/OfferDialog.vue
  12. 8
      src/db/tables/settings.ts
  13. 111
      src/libs/endorserServer.ts
  14. 41
      src/libs/util.ts
  15. 6
      src/main.ts
  16. 15
      src/router/index.ts
  17. 40
      src/views/AccountViewView.vue
  18. 4
      src/views/ConfirmContactView.vue
  19. 10
      src/views/ContactAmountsView.vue
  20. 4
      src/views/ContactScanView.vue
  21. 55
      src/views/ContactsView.vue
  22. 77
      src/views/GiftedDetails.vue
  23. 13
      src/views/HelpNotificationsView.vue
  24. 69
      src/views/HelpOnboardingView.vue
  25. 37
      src/views/HelpView.vue
  26. 198
      src/views/HomeView.vue
  27. 4
      src/views/ImportAccountView.vue
  28. 4
      src/views/ImportDerivedAccountView.vue
  29. 2
      src/views/NewEditAccountView.vue
  30. 13
      src/views/NewEditProjectView.vue
  31. 40
      src/views/ProjectViewView.vue
  32. 6
      src/views/ProjectsView.vue
  33. 8
      src/views/QuickActionBvcEndView.vue
  34. 8
      src/views/SearchAreaView.vue
  35. 10
      src/views/StartView.vue

11
CHANGELOG.md

@ -10,7 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Nothing - Nothing
## [0.3.4] - 2024.03.21 ## [0.3.6] - 2024.03.24 - 3a07e31d6313ab95711265562d9023c42916e141
### Added
- Button to mirror photo during video
- More detailed onboarding help screen
- Public-data blurb
### Changed in DB or environment
- Nothing
## [0.3.5] - 2024.03.23 - 28754bdfb1e11aa221dd49a5dce4219b69cf6a9d
### Added ### Added
- Photo on gift records - Photo on gift records
### Fixed ### Fixed

4
README.md

@ -60,8 +60,6 @@ npm run build
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari` * `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
* Revert src/constants/app.ts and package.json (if that was prod).
* Commit changes. Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production. * Commit changes. Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
* [Tag with the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) * [Tag with the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
@ -133,7 +131,7 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
* Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data", and also manually remove the IndexedDB data if the DBs still show.) * Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data", and also manually remove the IndexedDB data if the DBs still show.)
* Clear notification permission. (In Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".) * Clear notification permission. (In Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".)
* Unregister service worker. (In Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`.) * Unregister service worker. (In Chrome, go to `chrome://serviceworker-internals`; in Firefox, go to `about:serviceworkers`.)
* Clear Cache Storage manually, possibly deleting the DB. (In Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.) * Clear Cache Storage manually, possibly deleting the DB. (In Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.)
(If you find more, add them to the HelpNotificationsView.vue file.) (If you find more, add them to the HelpNotificationsView.vue file.)

6444
package-lock.json

File diff suppressed because it is too large

3
package.json

@ -1,6 +1,6 @@
{ {
"name": "TimeSafari", "name": "TimeSafari",
"version": "0.3.4", "version": "0.3.7-beta",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -41,6 +41,7 @@
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"localstorage-slim": "^2.7.0", "localstorage-slim": "^2.7.0",
"lru-cache": "^10.2.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"merkletreejs": "^0.3.11", "merkletreejs": "^0.3.11",
"moment": "^2.30.1", "moment": "^2.30.1",

39
project.task.yaml

@ -1,35 +1,37 @@
tasks : tasks :
- bug - landscape doesn't show full camera - fix the notification link to the app
- bug - got blank screen and errors on iPhone with no bottom tabs - 01 change scanning flow - allow them to stay on the QR/scanning screen after scanning someone
- add to readme - check version, close tabs & restart phone if necessary
- bug maybe - a new give remembers the previous project
- alert & stop if give amount < 0
- add warning that all data (except ID) is public
- onboarding video
- .2 when adding a claim on home screen, push that claim to the top of the list
- 24 allow a person record with interests, including location; purpose? contact methods? enhance other connections the same? (suggestion from Philippines) assignee-group:ui - 24 contextual tutorials https://docs.google.com/document/d/11C_K3RM0rgo0onih20KFhcIzukZyq_CRWqaWX5om_kM/edit#heading=h.iwiwcydou5hw
- 24 Move to Vite assignee:jason
- feeds - add "remote" filter, if they choose 'visible' then warn that they won't see any others, cache list & don't reload front page on change
- .1 add shortcut from project (etc?) to the public project page in a browser
- .1 add KindSpring link to ideas
- .1 on feed, don't show "to someone anonymous" if it's to a project - .1 on feed, don't show "to someone anonymous" if it's to a project
- .1 on ideas, put an "x" to close it assignee-group:ui
- 16 save data backups in Google - 16 save data backups in Google
- 16 generate and use passkeys for identities - 16 generate and use passkeys for identities
- .5 show "give" buttons (eg. from anonymous) even if they can't give, greyed out, and give them a warning and instructions
- .2 when adding a claim on home screen, push that claim to the top of the list
- .2 fix give dialog from "more contacts" off home page to allow giving to this user - .2 fix give dialog from "more contacts" off home page to allow giving to this user
- .2 fix bottom of project selection map, where the icons are hidden but a tap goes to the icon's page assignee-group:ui
- .5 stop from seeing an error on the first page when browser doesn't support service workers (which I've seen on iPhone; visible in Firefox private window) - .5 stop from seeing an error on the first page when browser doesn't support service workers (which I've seen on iPhone; visible in Firefox private window)
- .2 don't show a warning on a totally new project when the authorized agent is set - .2 don't show a warning on a totally new project when the authorized agent is set
- .2 anchor hash into BTC - .2 anchor hash into BTC
- .2 list the "show more" contacts alphabetically - .2 list the "show more" contacts alphabetically
- .5 add back the explicit wait for browser subscription timing problems?
- .5 make Time Safari a share_target for images - .5 make Time Safari a share_target for images
- 08 add image on profile - 08 add image on profile
- ask to detect location & record it in settings - 01 ask to detect location & record it in settings
- if personal location is set, show potential local affiliations - 01 if personal location is set, show potential local affiliations
- 02 refactor the buttons for chosing a search location so that the actions are clear assignee-group:ui
- 24 compelling UI for credential presentations - 24 compelling UI for credential presentations
- discover who in my network has activity on a project - discover who in my network has activity on a project
@ -66,6 +68,7 @@ tasks :
- .1 add cursor-pointer on the icons for giving on the project page, and on the list of projects on the discover page - .1 add cursor-pointer on the icons for giving on the project page, and on the list of projects on the discover page
- .2 record when InfiniteScroll hits the end of the list and don't trigger any more loads (feed, project list, give & offer lists) - .2 record when InfiniteScroll hits the end of the list and don't trigger any more loads (feed, project list, give & offer lists)
- bug (that is hard to reproduce) - got blank screen and errors on iPhone with no bottom tabs
- bug - turning notifications on from the help screen did not stay, though account screen toggle did stay (From Jason on iPhone.) - bug - turning notifications on from the help screen did not stay, though account screen toggle did stay (From Jason on iPhone.)
- refactor - supply the projectId to the OfferDialog just like we do with the GiftedDialog offerId (in the "open" method, maybe as well as an attribute) - refactor - supply the projectId to the OfferDialog just like we do with the GiftedDialog offerId (in the "open" method, maybe as well as an attribute)
- the confirm button on each give on the ProjectViewView page doesn't have all the context of the ClaimView page, so it can show sometimes inappropriately; consider consolidation - the confirm button on each give on the ProjectViewView page doesn't have all the context of the ClaimView page, so it can show sometimes inappropriately; consider consolidation
@ -97,7 +100,7 @@ tasks :
- .3 check that Android shows "back" buttons on screens without bottom tray - .3 check that Android shows "back" buttons on screens without bottom tray
- .1 Make give description text box into something that expands as they type? - .1 Make give description text box into something that expands as they type?
- .2 Show a warning if both giver and recipient are the same (but still allow?) - .2 Show a warning if both giver and recipient are the same (but still allow?)
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui - .5 Shrink the buttons on project pages so they don't expand to the width of the screen assignee-group:ui
- .5 Display a more appealing confirmation on the map when erasing the marker - .5 Display a more appealing confirmation on the map when erasing the marker
- .5 remove references to localStorage for projectId (now that it's pulling from the path) - .5 remove references to localStorage for projectId (now that it's pulling from the path)
- switch some checks for activeDid to check for isRegistered - switch some checks for activeDid to check for isRegistered
@ -124,11 +127,14 @@ tasks :
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie) - 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
- .5 show seed phrase in a QR code for transfer to another device - .5 show seed phrase in a QR code for transfer to another device
- .5 on DiscoverView, switch to a filter UI (eg. just from friend - .5 on DiscoverView, switch to a filter UI (eg. just from friend)
- .5 don't show "Offer" on project screen if they aren't registered - .5 don't show "Offer" on project screen if they aren't registered
- 01 especially for iOS, check for new version & update, eg. https://stackoverflow.com/questions/52221805/any-way-yet-to-auto-update-or-just-clear-the-cache-on-a-pwa-on-ios - 01 especially for iOS, check for new version & update, eg. https://stackoverflow.com/questions/52221805/any-way-yet-to-auto-update-or-just-clear-the-cache-on-a-pwa-on-ios
- 24 Move to Vite - 24 allow a person record with interests, including location; purpose? contact methods? enhance other connections the same? (suggestion from Philippines) assignee-group:ui
- 24 brief introduction slides https://docs.google.com/document/d/11C_K3RM0rgo0onih20KFhcIzukZyq_CRWqaWX5om_kM/edit#heading=h.iwiwcydou5hw
- 12 feedback https://docs.google.com/document/d/11C_K3RM0rgo0onih20KFhcIzukZyq_CRWqaWX5om_kM/edit#heading=h.iwiwcydou5hw
- 32 accept images for projects - 32 accept images for projects
- 32 accept images for contacts - 32 accept images for contacts
- import project interactions from GitHub/GitLab and manage signing - import project interactions from GitHub/GitLab and manage signing
@ -142,7 +148,6 @@ tasks :
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances - for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning) - for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
- .5 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui
- .5 Replace Gifted/Give in ContactsView with GiftedDialog - .5 Replace Gifted/Give in ContactsView with GiftedDialog
- Stats : - Stats :

114
src/App.vue

@ -1,7 +1,7 @@
<template> <template>
<router-view /> <router-view />
<!-- https://github.com/emmanuelsw/notiwind --> <!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
<NotificationGroup group="alert"> <NotificationGroup group="alert">
<div <div
class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end" class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end"
@ -129,6 +129,7 @@
</div> </div>
</NotificationGroup> </NotificationGroup>
<!-- These are general-purpose messages - except there are some for turning app notifications on and off. -->
<NotificationGroup group="modal"> <NotificationGroup group="modal">
<div class="fixed z-[100] top-0 inset-x-0 w-full"> <div class="fixed z-[100] top-0 inset-x-0 w-full">
<Notification <Notification
@ -148,6 +149,7 @@
class="w-full" class="w-full"
role="alert" role="alert"
> >
<!-- type "confirm" will post a message and, with onYes function, show a "Yes" button to call that function -->
<div <div
v-if="notification.type === 'confirm'" v-if="notification.type === 'confirm'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50" class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
@ -161,6 +163,7 @@
</p> </p>
<button <button
v-if="notification.onYes"
@click=" @click="
notification.onYes(); notification.onYes();
close(notification.id); close(notification.id);
@ -174,7 +177,7 @@
@click="close(notification.id)" @click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md" class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
> >
Cancel {{ notification.onYes ? "Cancel" : "Close" }}
</button> </button>
</div> </div>
</div> </div>
@ -188,7 +191,7 @@
> >
<div class="w-full px-6 py-6 text-slate-900 text-center"> <div class="w-full px-6 py-6 text-slate-900 text-center">
<p v-if="serviceWorkerReady" class="text-lg mb-4"> <p v-if="serviceWorkerReady" class="text-lg mb-4">
Would you like to <b>turn on</b> notifications for this app? Would you like to be notified of new activity once a day?
</p> </p>
<p v-else class="text-lg mb-4"> <p v-else class="text-lg mb-4">
Waiting for system initialization, which may take up to 10 Waiting for system initialization, which may take up to 10
@ -196,22 +199,42 @@
<fa icon="spinner" spin /> <fa icon="spinner" spin />
</p> </p>
<div v-if="serviceWorkerReady">
<span class="flex flex-row justify-center">
<span class="mt-2">Yes, tell me at: </span>
<input
type="number"
class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20"
v-model="hourInput"
/>
<span
class="rounded-r border border-slate-400 bg-slate-200 text-center text-blue-500 mt-2 px-2 py-2 w-20"
@click="hourAm = !hourAm"
>
<span v-if="hourAm"> AM <fa icon="chevron-down" /> </span>
<span v-else> PM <fa icon="chevron-up" /> </span>
</span>
</span>
<button <button
v-if="serviceWorkerReady" class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white mt-2 px-2 py-2 rounded-md"
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
@click=" @click="
() => {
if (checkHour()) {
close(notification.id); close(notification.id);
turnOnNotifications(); turnOnNotifications();
}
}
" "
> >
Turn on Notifications Turn on Daily Message
</button> </button>
</div>
<button <button
@click="close(notification.id)" @click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md" class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white mt-4 px-2 py-2 rounded-md"
> >
Maybe Later No, Not Now
</button> </button>
</div> </div>
</div> </div>
@ -294,8 +317,11 @@
<style></style> <style></style>
<script lang="ts"> <script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import axios from "axios"; import axios from "axios";
import { Vue, Component } from "vue-facing-decorator";
import * as libsUtil from "@/libs/util";
interface ServiceWorkerMessage { interface ServiceWorkerMessage {
type: string; type: string;
data: string; data: string;
@ -319,6 +345,10 @@ interface VapidResponse {
}; };
} }
interface PushSubscriptionWithTime extends PushSubscriptionJSON {
notifyTime: { utcHour: number };
}
import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@ -329,7 +359,9 @@ export default class App extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
b64 = ""; b64 = "";
serviceWorkerReady = false; hourAm = true;
hourInput = "8";
serviceWorkerReady = true;
async mounted() { async mounted() {
try { try {
@ -463,6 +495,48 @@ export default class App extends Vue {
}); });
} }
// this allows us to show an error without closing the dialog
checkHour() {
if (!libsUtil.isNumeric(this.hourInput)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not a Number",
text: "The time must be an hour number.",
},
5000,
);
return false;
}
const hourNum = libsUtil.numberOrZero(this.hourInput);
if (!Number.isInteger(hourNum)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not a Whole Number",
text: "The time must be a whole hour number.",
},
5000,
);
return false;
}
if (hourNum < 1 || 12 < hourNum) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not a Whole Number",
text: "The time must be an hour between 1 and 12.",
},
5000,
);
return false;
}
return true;
}
public async turnOnNotifications() { public async turnOnNotifications() {
return this.askPermission() return this.askPermission()
.then((permission) => { .then((permission) => {
@ -488,13 +562,25 @@ export default class App extends Vue {
}, },
-1, -1,
); );
this.sendSubscriptionToServer(subscription); // we already checked that this is a valid hour number
return subscription; const rawHourNum = libsUtil.numberOrZero(this.hourInput);
const adjHourNum = rawHourNum + (this.hourAm ? 0 : 12);
const hourNum = adjHourNum % 24;
const utcHour =
hourNum + Math.round(new Date().getTimezoneOffset() / 60);
const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24;
const subscriptionWithTime: PushSubscriptionWithTime = {
notifyTime: { utcHour: finalUtcHour },
...subscription.toJSON(),
};
await this.sendSubscriptionToServer(subscriptionWithTime);
return subscriptionWithTime;
} else { } else {
throw new Error("Subscription object is not available."); throw new Error("Subscription object is not available.");
} }
}) })
.then(async (subscription) => { .then(async (subscription: PushSubscriptionWithTime) => {
console.log( console.log(
"Subscription data sent to server and all finished successfully.", "Subscription data sent to server and all finished successfully.",
); );
@ -585,7 +671,7 @@ export default class App extends Vue {
} }
private sendSubscriptionToServer( private sendSubscriptionToServer(
subscription: PushSubscription, subscription: PushSubscriptionWithTime,
): Promise<void> { ): Promise<void> {
console.log("About to send subscription...", subscription); console.log("About to send subscription...", subscription);
return fetch("/web-push/subscribe", { return fetch("/web-push/subscribe", {

219
src/components/FeedFilters.vue

@ -0,0 +1,219 @@
<template>
<div v-if="visible" id="dialogFeedFilters" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Feed Filters</h1>
<p class="mb-4 font-bold">Show only activities that</p>
<div class="grid grid-cols-1 gap-2">
<div
class="flex items-center justify-between cursor-pointer"
@click="toggleHasVisibleDid()"
>
<!-- label -->
<div>Include someone visible to me</div>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="hasVisibleDid"
name="toggleFilterFromMyContacts"
class="sr-only"
/>
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</div>
<em>or</em>
<div
class="flex items-center justify-between cursor-pointer"
@click="
hasSearchBox
? toggleNearby()
: $router.push({ name: 'search-area' })
"
>
<!-- label -->
<div>Are nearby</div>
<!-- toggle -->
<div v-if="hasSearchBox" class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="isNearby"
name="toggleFilterNearby"
class="sr-only"
/>
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
<div v-else class="relative ml-2">
<button class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500">
Select Location
</button>
</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-4">
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="setAll()"
>
Set All
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="clearAll()"
>
Clear All
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="done()"
>
Done
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import {
LMap,
LMarker,
LRectangle,
LTileLayer,
} from "@vue-leaflet/vue-leaflet";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { db } from "@/db/index";
@Component({
components: {
LRectangle,
LMap,
LMarker,
LTileLayer,
},
})
export default class FeedFilters extends Vue {
onCloseIfChanged = () => {};
hasSearchBox = false;
hasVisibleDid = false;
isNearby = false;
settingChanged = false;
visible = false;
async open(onCloseIfChanged: () => void) {
this.onCloseIfChanged = onCloseIfChanged;
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.hasVisibleDid = !!settings?.filterFeedByVisible;
this.isNearby = !!settings?.filterFeedByNearby;
if (settings?.searchBoxes && settings.searchBoxes.length > 0) {
this.hasSearchBox = true;
}
this.settingChanged = false;
this.visible = true;
}
toggleHasVisibleDid() {
this.settingChanged = true;
this.hasVisibleDid = !this.hasVisibleDid;
db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByVisible: this.hasVisibleDid,
});
}
toggleNearby() {
this.settingChanged = true;
this.isNearby = !this.isNearby;
db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: this.isNearby,
});
}
async clearAll() {
if (this.hasVisibleDid || this.isNearby) {
this.settingChanged = true;
}
db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: false,
filterFeedByVisible: false,
});
this.hasVisibleDid = false;
this.isNearby = false;
}
async setAll() {
if (!this.hasVisibleDid || !this.isNearby) {
this.settingChanged = true;
}
db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: true,
filterFeedByVisible: true,
});
this.hasVisibleDid = true;
this.isNearby = true;
}
close() {
if (this.settingChanged) {
this.onCloseIfChanged();
}
this.visible = false;
}
done() {
this.close();
}
}
</script>
<style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
#dialogFeedFilters.dialog-overlay {
z-index: 99999;
overflow: scroll;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 500px;
}
</style>

88
src/components/GiftedDialog.vue

@ -53,15 +53,21 @@
}" }"
class="text-blue-500" class="text-blue-500"
> >
More Options Photo, ...
</router-link> </router-link>
</span> </span>
</div> </div>
<p class="text-center mb-2 mt-6 italic"> <p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world Sign & Send to publish to the world
<fa
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
/>
</p> </p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button <button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@click="confirm" @click="confirm"
> >
Sign &amp; Send Sign &amp; Send
@ -74,6 +80,7 @@
</button> </button>
</div> </div>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -195,6 +202,45 @@ export default class GiftedDialog extends Vue {
} }
async confirm() { async confirm() {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identifier before you can record a give.",
},
3000,
);
return;
}
if (parseFloat(this.amountInput) < 0) {
this.$notify(
{
group: "alert",
type: "danger",
text: "You may not send a negative number.",
title: "",
},
2000,
);
return;
}
if (!this.description && !parseFloat(this.amountInput)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `You must enter a description or some number of ${
this.libsUtil.UNIT_LONG[this.unitCode]
}.`,
},
2000,
);
return;
}
this.close(); this.close();
this.$notify( this.$notify(
{ {
@ -229,32 +275,6 @@ export default class GiftedDialog extends Vue {
amountInput: number, amountInput: number,
unitCode: string = "HUR", unitCode: string = "HUR",
) { ) {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identifier before you can record a give.",
},
-1,
);
return;
}
if (!description && !amountInput) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `You must enter a description or some number of ${this.libsUtil.UNIT_LONG[unitCode]}.`,
},
-1,
);
return;
}
try { try {
const identity = await libsUtil.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
@ -339,6 +359,18 @@ export default class GiftedDialog extends Vue {
result.response?.data?.error?.message result.response?.data?.error?.message
); );
} }
explainData() {
this.$notify(
{
group: "alert",
type: "success",
title: "Data Sharing",
text: libsUtil.PRIVACY_MESSAGE,
},
-1,
);
}
} }
</script> </script>

74
src/components/GiftedPhotoDialog.vue

@ -43,22 +43,45 @@
<img :src="URL.createObjectURL(blob)" class="mt-2 rounded" /> <img :src="URL.createObjectURL(blob)" class="mt-2 rounded" />
</div> </div>
</div> </div>
<div v-else> <div v-else ref="cameraContainer">
<!-- <!--
Camera "resolution" doesn't change how it shows on screen but rather stretches the result, eg the following which just stretches it vertically: Camera "resolution" doesn't change how it shows on screen but rather stretches the result, eg the following which just stretches it vertically:
:resolution="{ width: 375, height: 812 }" :resolution="{ width: 375, height: 812 }"
--> -->
<camera facingMode="environment" autoplay ref="camera"> <camera
facingMode="environment"
autoplay
ref="camera"
@started="cameraStarted()"
>
<div <div
class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 landscape:right-0 landscape:top-0 landscape:bottom-0 flex landscape:flex-row justify-center items-center portrait:pb-2 landscape:pr-4" class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 portrait:pb-2 landscape:right-0 landscape:top-0 landscape:bottom-0 landscape:pr-4 flex landscape:flex-row justify-center items-center"
> >
<button <button
@click="takeImage" @click="takeImage()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none" class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
> >
<fa icon="camera" class="w-[1em]"></fa> <fa icon="camera" class="w-[1em]"></fa>
</button> </button>
</div> </div>
<div
class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
>
<button
@click="swapMirrorClass()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<fa icon="left-right" class="w-[1em]"></fa>
</button>
</div>
<div v-if="numDevices > 1" class="absolute bottom-2 right-4">
<button
@click="switchCamera()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<fa icon="rotate" class="w-[1em]"></fa>
</button>
</div>
</camera> </camera>
</div> </div>
</div> </div>
@ -80,12 +103,12 @@ import { accessToken } from "@/libs/crypto";
export default class GiftedPhotoDialog extends Vue { export default class GiftedPhotoDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeDeviceNumber = 0;
activeDid = ""; activeDid = "";
blob: Blob | null = null; blob: Blob | null = null;
mirror = false;
numDevices = 0;
setImage: (arg: string) => void = () => {}; setImage: (arg: string) => void = () => {};
imageHeight?: number = window.innerHeight / 2;
imageWidth?: number = window.innerWidth / 2;
imageWarning = ".";
uploading = false; uploading = false;
visible = false; visible = false;
@ -129,6 +152,21 @@ export default class GiftedPhotoDialog extends Vue {
this.blob = null; this.blob = null;
} }
async cameraStarted() {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
if (cameraComponent) {
this.numDevices = (await cameraComponent.devices(["videoinput"])).length;
this.mirror = cameraComponent.facingMode === "user";
}
}
async switchCamera() {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
this.activeDeviceNumber = (this.activeDeviceNumber + 1) % this.numDevices;
const devices = await cameraComponent?.devices(["videoinput"]);
cameraComponent?.changeCamera(devices[this.activeDeviceNumber].deviceId);
}
async takeImage(/* payload: MouseEvent */) { async takeImage(/* payload: MouseEvent */) {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>; const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
@ -200,7 +238,6 @@ export default class GiftedPhotoDialog extends Vue {
<canvas id="canvas" width="320" height="240"></canvas> <canvas id="canvas" width="320" height="240"></canvas>
async cameraClicked() { async cameraClicked() {
console.log("camera_button clicked");
const video = document.querySelector("#video"); const video = document.querySelector("#video");
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
video: true, video: true,
@ -211,7 +248,6 @@ export default class GiftedPhotoDialog extends Vue {
} }
} }
photoSnapped() { photoSnapped() {
console.log("snap_photo clicked");
const video = document.querySelector("#video"); const video = document.querySelector("#video");
const canvas = document.querySelector("#canvas"); const canvas = document.querySelector("#canvas");
if ( if (
@ -232,7 +268,6 @@ export default class GiftedPhotoDialog extends Vue {
// data url of the image // data url of the image
const image_data_url = canvas?.toDataURL("image/jpeg"); const image_data_url = canvas?.toDataURL("image/jpeg");
console.log(image_data_url);
} }
} }
****/ ****/
@ -287,6 +322,17 @@ export default class GiftedPhotoDialog extends Vue {
this.blob = null; this.blob = null;
} }
} }
swapMirrorClass() {
this.mirror = !this.mirror;
if (this.mirror) {
(this.$refs.cameraContainer as HTMLElement).classList.add("mirror-video");
} else {
(this.$refs.cameraContainer as HTMLElement).classList.remove(
"mirror-video",
);
}
}
} }
</script> </script>
@ -311,4 +357,12 @@ export default class GiftedPhotoDialog extends Vue {
width: 100%; width: 100%;
max-width: 700px; max-width: 700px;
} }
.mirror-video {
transform: scaleX(-1);
-webkit-transform: scaleX(-1); /* For Safari */
-moz-transform: scaleX(-1); /* For Firefox */
-ms-transform: scaleX(-1); /* For IE */
-o-transform: scaleX(-1); /* For Opera */
}
</style> </style>

10
src/components/GiftedPrompts.vue

@ -1,7 +1,15 @@
<template> <template>
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Here's one:</h1> <h1 class="text-xl font-bold text-center mb-4 relative">
Here's one:
<div
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
@click="cancel"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</h1>
<span class="flex justify-between"> <span class="flex justify-between">
<span <span
class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex" class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex"

4
src/components/OfferDialog.vue

@ -50,8 +50,9 @@
<p class="text-center mt-6 mb-2 italic"> <p class="text-center mt-6 mb-2 italic">
Sign & Send to publish to the world Sign & Send to publish to the world
</p> </p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button <button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@click="confirm" @click="confirm"
> >
Sign &amp; Send Sign &amp; Send
@ -64,6 +65,7 @@
</button> </button>
</div> </div>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">

8
src/db/tables/settings.ts

@ -16,6 +16,10 @@ export type Settings = {
activeDid?: string; // Active Decentralized ID activeDid?: string; // Active Decentralized ID
apiServer?: string; // API server URL apiServer?: string; // API server URL
filterFeedByNearby?: boolean; // filter by nearby
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden
firstName?: string; // User's first name firstName?: string; // User's first name
isRegistered?: boolean; isRegistered?: boolean;
lastName?: string; // deprecated - put all names in firstName lastName?: string; // deprecated - put all names in firstName
@ -38,6 +42,10 @@ export type Settings = {
webPushServer?: string; // Web Push server URL webPushServer?: string; // Web Push server URL
}; };
export function isAnyFeedFilterOn(settings: Settings): boolean {
return !!(settings.filterFeedByNearby || settings.filterFeedByVisible);
}
/** /**
* Schema for the Settings table in the database. * Schema for the Settings table in the database.
*/ */

111
src/libs/endorserServer.ts

@ -1,9 +1,11 @@
import { Axios, AxiosResponse, RawAxiosRequestHeaders } from "axios";
import * as didJwt from "did-jwt";
import { LRUCache } from "lru-cache";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import { Axios, AxiosResponse } from "axios";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { accessToken, SimpleSigner } from "@/libs/crypto";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims // the object in RegisterAction claims
@ -49,7 +51,7 @@ export interface GenericVerifiableCredential {
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
} }
export interface GenericServerRecord extends GenericVerifiableCredential { export interface GenericCredWrapper extends GenericVerifiableCredential {
handleId?: string; handleId?: string;
id: string; id: string;
issuedAt: string; issuedAt: string;
@ -58,7 +60,7 @@ export interface GenericServerRecord extends GenericVerifiableCredential {
claim: Record<string, any>; claim: Record<string, any>;
claimType?: string; claimType?: string;
} }
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = { export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper = {
"@context": SCHEMA_ORG_CONTEXT, "@context": SCHEMA_ORG_CONTEXT,
"@type": "", "@type": "",
claim: {}, claim: {},
@ -68,7 +70,7 @@ export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
}; };
// a summary record; the VC is found the fullClaim field // a summary record; the VC is found the fullClaim field
export interface GiveServerRecord { export interface GiveSummaryRecord {
agentDid: string; agentDid: string;
amount: number; amount: number;
amountConfirmed: number; amountConfirmed: number;
@ -83,7 +85,7 @@ export interface GiveServerRecord {
} }
// a summary record; the VC is found the fullClaim field // a summary record; the VC is found the fullClaim field
export interface OfferServerRecord { export interface OfferSummaryRecord {
amount: number; amount: number;
amountGiven: number; amountGiven: number;
amountGivenConfirmed: number; amountGivenConfirmed: number;
@ -101,7 +103,7 @@ export interface OfferServerRecord {
} }
// a summary record; the VC is not currently part of this record // a summary record; the VC is not currently part of this record
export interface PlanServerRecord { export interface PlanSummaryRecord {
agentDid?: string; // optional, if the issuer wants someone else to manage as well agentDid?: string; // optional, if the issuer wants someone else to manage as well
description: string; description: string;
endTime?: string; endTime?: string;
@ -110,6 +112,7 @@ export interface PlanServerRecord {
issuerDid: string; issuerDid: string;
locLat?: number; locLat?: number;
locLon?: number; locLon?: number;
name?: string;
startTime?: string; startTime?: string;
url?: string; url?: string;
} }
@ -256,6 +259,10 @@ export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6 // See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN"; const HIDDEN_DID = "did:none:HIDDEN";
const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
max: 500,
});
export function isDid(did: string) { export function isDid(did: string) {
return did.startsWith("did:"); return did.startsWith("did:");
} }
@ -269,7 +276,7 @@ export function isEmptyOrHiddenDid(did?: string) {
} }
/** /**
* @return true for any nested string where func(input) === true * @return true for any string within this primitive/object/array where func(input) === true
* *
* Similar logic is found in endorser-mobile. * Similar logic is found in endorser-mobile.
*/ */
@ -304,6 +311,12 @@ export function containsHiddenDid(obj: any) {
return testRecursivelyOnStrings(isHiddenDid, obj); return testRecursivelyOnStrings(isHiddenDid, obj);
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const containsNonHiddenDid = (obj: any) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return testRecursivelyOnStrings((s: any) => isDid(s) && !isHiddenDid(s), obj);
};
export function stripEndorserPrefix(claimId: string) { export function stripEndorserPrefix(claimId: string) {
if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) { if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) {
return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length); return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length);
@ -403,8 +416,11 @@ export function didInfoForContact(
return myId return myId
? { displayName: "You (Alt ID)", known: true } ? { displayName: "You (Alt ID)", known: true }
: isHiddenDid(did) : isHiddenDid(did)
? { displayName: "Someone Outside Your Network", known: false } ? { displayName: "Someone Totally Outside Your View", known: false }
: { displayName: "Someone Outside Contacts", known: false }; : {
displayName: "Someone Visible But Outside Your Contact List",
known: false,
};
} }
} }
@ -423,6 +439,71 @@ export function didInfo(
return didInfoForContact(did, activeDid, contact, allMyDids).displayName; return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
} }
async function getHeaders(identity: IIdentifier | null) {
const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
return headers;
}
/**
* @param handleId nullable -- which means that "undefined" will be returned
* @param identity nullable -- which means no private info will be returned
* @param axios
* @param apiServer
*/
export async function getPlanFromCache(
handleId: string | null,
identity: IIdentifier | null,
axios: Axios,
apiServer: string,
) {
if (!handleId) {
return undefined;
}
let cred = planCache.get(handleId);
if (!cred) {
const url =
apiServer +
"/api/v2/report/plans?handleId=" +
encodeURIComponent(handleId);
const headers = await getHeaders(identity);
try {
const resp = await axios.get(url, { headers });
if (resp.status === 200 && resp.data?.data?.length > 0) {
cred = resp.data.data[0];
planCache.set(handleId, cred);
} else {
console.log(
"Failed to load plan with handle",
handleId,
" Got data:",
resp.data,
);
}
} catch (error) {
console.log(
"Failed to load plan with handle",
handleId,
" Got error:",
error,
);
}
}
return cred;
}
export async function setPlanInCache(
handleId: string,
planSummary: PlanSummaryRecord,
) {
planCache.set(handleId, planSummary);
}
/** /**
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim * For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
* *
@ -475,7 +556,7 @@ export async function createAndSubmitGive(
vcClaim.image = imageUrl; vcClaim.image = imageUrl;
} }
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericServerRecord, vcClaim as GenericCredWrapper,
identity, identity,
apiServer, apiServer,
axios, axios,
@ -524,7 +605,7 @@ export async function createAndSubmitOffer(
}; };
} }
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericServerRecord, vcClaim as GenericCredWrapper,
identity, identity,
apiServer, apiServer,
axios, axios,
@ -695,7 +776,7 @@ const claimSummary = (claim: Record<string, any>) => {
similar code is also contained in endorser-mobile similar code is also contained in endorser-mobile
**/ **/
export const claimSpecialDescription = ( export const claimSpecialDescription = (
record: GenericServerRecord, record: GenericCredWrapper,
activeDid: string, activeDid: string,
identifiers: Array<string>, identifiers: Array<string>,
contacts: Array<Contact>, contacts: Array<Contact>,
@ -789,7 +870,7 @@ export const claimSpecialDescription = (
"...]" "...]"
); );
} else { } else {
return issuer + " declared " + claimSummary(claim as GenericServerRecord); return issuer + " declared " + claimSummary(claim as GenericCredWrapper);
} }
}; };

41
src/libs/util.ts

@ -9,13 +9,11 @@ import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { GenericServerRecord, containsHiddenDid } from "@/libs/endorserServer"; import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
// If you edit this, check that the numbers still line up on the side in the alert (on mobile, too), export const PRIVACY_MESSAGE =
// and make sure they can take all actions while the notification shows. "The data you send be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to those you allow.";
export const ONBOARD_MESSAGE =
"1) Read through all their yellow prompts. 2) Add them to your Contacts by scanning with the QR icon that is by the input box. 3) Click the person icon to register them. 4) Show them your QR so they'll scan you. 5) Have them enable notifications.";
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
export const UNIT_SHORT: Record<string, string> = { export const UNIT_SHORT: Record<string, string> = {
@ -55,9 +53,13 @@ export function iconForUnitCode(unitCode: string) {
} }
// from https://stackoverflow.com/a/175787/845494 // from https://stackoverflow.com/a/175787/845494
// ... though it appears even this isn't precisely right so keep doing "|| 0" or something in sensitive places
// //
export function isNumeric(str: string): boolean { export function isNumeric(str: string): boolean {
return !isNaN(+str); // This ignore commentary is because typescript complains when you pass a string to isNaN.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return !isNaN(str) && !isNaN(parseFloat(str));
} }
export function numberOrZero(str: string): number { export function numberOrZero(str: string): number {
@ -68,7 +70,7 @@ export const isGlobalUri = (uri: string) => {
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/)); return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
}; };
export const giveIsConfirmable = (veriClaim: GenericServerRecord) => { export const giveIsConfirmable = (veriClaim: GenericCredWrapper) => {
return veriClaim.claimType === "GiveAction"; return veriClaim.claimType === "GiveAction";
}; };
@ -84,7 +86,7 @@ export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
* @param veriClaim is expected to have fields: claim, claimType, and issuer * @param veriClaim is expected to have fields: claim, claimType, and issuer
*/ */
export const isGiveRecordTheUserCanConfirm = ( export const isGiveRecordTheUserCanConfirm = (
veriClaim: GenericServerRecord, veriClaim: GenericCredWrapper,
activeDid: string, activeDid: string,
confirmerIdList: string[] = [], confirmerIdList: string[] = [],
) => { ) => {
@ -100,9 +102,9 @@ export const isGiveRecordTheUserCanConfirm = (
* @returns the DID of the person who offered, or undefined if hidden * @returns the DID of the person who offered, or undefined if hidden
* @param veriClaim is expected to have fields: claim and issuer * @param veriClaim is expected to have fields: claim and issuer
*/ */
export const offerGiverDid: ( export const offerGiverDid: (arg0: GenericCredWrapper) => string | undefined = (
arg0: GenericServerRecord, veriClaim,
) => string | undefined = (veriClaim) => { ) => {
let giver; let giver;
if ( if (
veriClaim.claim.offeredBy?.identifier && veriClaim.claim.offeredBy?.identifier &&
@ -119,7 +121,7 @@ export const offerGiverDid: (
* @returns true if the user can fulfill the offer * @returns true if the user can fulfill the offer
* @param veriClaim is expected to have fields: claim, claimType, and issuer * @param veriClaim is expected to have fields: claim, claimType, and issuer
*/ */
export const canFulfillOffer = (veriClaim: GenericServerRecord) => { export const canFulfillOffer = (veriClaim: GenericCredWrapper) => {
return !!(veriClaim.claimType === "Offer" && offerGiverDid(veriClaim)); return !!(veriClaim.claimType === "Offer" && offerGiverDid(veriClaim));
}; };
@ -249,7 +251,7 @@ function getBase64(subscription: PushSubscription, key: PushEncryptionKeyName) {
} }
export const sendTestThroughPushServer = async ( export const sendTestThroughPushServer = async (
subscription: PushSubscription, subscriptionJSON: PushSubscriptionJSON,
skipFilter: boolean, skipFilter: boolean,
): Promise<AxiosResponse> => { ): Promise<AxiosResponse> => {
await db.open(); await db.open();
@ -264,18 +266,11 @@ export const sendTestThroughPushServer = async (
// Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213 // Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION"; const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
const authB64 = getBase64(subscription, "auth");
const p256dhB64 = getBase64(subscription, "p256dh");
const newPayload = { const newPayload = {
endpoint: subscription.endpoint, // eslint-disable-next-line prettier/prettier
keys: { message: `Test, where you will see this message ${ skipFilter ? "un" : "" }filtered.`,
auth: authB64,
p256dh: p256dhB64,
},
message: `Test, where you will see this message ${
skipFilter ? "un" : ""
}filtered.`,
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push", title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
...subscriptionJSON,
}; };
console.log("Sending a test web push message:", newPayload); console.log("Sending a test web push message:", newPayload);
const payloadStr = JSON.stringify(newPayload); const payloadStr = JSON.stringify(newPayload);

6
src/main.ts

@ -20,8 +20,10 @@ import {
faCalendar, faCalendar,
faCamera, faCamera,
faCheck, faCheck,
faChevronDown,
faChevronLeft, faChevronLeft,
faChevronRight, faChevronRight,
faChevronUp,
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleInfo, faCircleInfo,
@ -45,6 +47,7 @@ import {
faHand, faHand,
faHandHoldingHeart, faHandHoldingHeart,
faHouseChimney, faHouseChimney,
faLeftRight,
faLocationDot, faLocationDot,
faLongArrowAltLeft, faLongArrowAltLeft,
faLongArrowAltRight, faLongArrowAltRight,
@ -80,8 +83,10 @@ library.add(
faCalendar, faCalendar,
faCamera, faCamera,
faCheck, faCheck,
faChevronDown,
faChevronLeft, faChevronLeft,
faChevronRight, faChevronRight,
faChevronUp,
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleInfo, faCircleInfo,
@ -105,6 +110,7 @@ library.add(
faHand, faHand,
faHandHoldingHeart, faHandHoldingHeart,
faHouseChimney, faHouseChimney,
faLeftRight,
faLocationDot, faLocationDot,
faLongArrowAltLeft, faLongArrowAltLeft,
faLongArrowAltRight, faLongArrowAltRight,

15
src/router/index.ts

@ -78,16 +78,21 @@ const routes: Array<RouteRecordRaw> = [
name: "help", name: "help",
component: () => import("../views/HelpView.vue"), component: () => import("../views/HelpView.vue"),
}, },
{
path: "/",
name: "home",
component: () => import("../views/HomeView.vue"),
},
{ {
path: "/help-notifications", path: "/help-notifications",
name: "help-notifications", name: "help-notifications",
component: () => import("../views/HelpNotificationsView.vue"), component: () => import("../views/HelpNotificationsView.vue"),
}, },
{
path: "/help-onboarding",
name: "help-onboarding",
component: () => import("../views/HelpOnboardingView.vue"),
},
{
path: "/",
name: "home",
component: () => import("../views/HomeView.vue"),
},
{ {
path: "/identity-switcher", path: "/identity-switcher",
name: "identity-switcher", name: "identity-switcher",

40
src/views/AccountViewView.vue

@ -112,6 +112,8 @@
</div> </div>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
<!-- label -->
<div class="mb-2 font-bold">Settings</div>
<div <div
v-if="!notificationMaybeChanged" v-if="!notificationMaybeChanged"
class="flex items-center justify-between cursor-pointer" class="flex items-center justify-between cursor-pointer"
@ -140,16 +142,38 @@
Notification status may have changed. Refresh this page to see the Notification status may have changed. Refresh this page to see the
latest setting. latest setting.
</div> </div>
<router-link class="px-4 text-sm text-blue-500" to="/help-notifications"> <router-link class="pl-4 text-sm text-blue-500" to="/help-notifications">
Troubleshoot your notification setup. Troubleshoot your notification setup.
</router-link> </router-link>
<router-link
:to="{ name: 'search-area' }"
v-if="activeDid"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-6"
>
Set Search Area
<!-- If already set, change button label to "Change Search Area" -->
</router-link>
<div class="text-slate-500 text-sm font-bold mt-6 mb-2">
Topics of Interest
</div>
<textarea
class="block w-full rounded border border-slate-400 px-3 py-2"
rows="3"
value="longing, rusted, seventeen, daybreak, furnace, nine, benign, homecoming, one, freight car"
>
</textarea>
<div class="text-slate-500 text-sm mt-2 mb-2">
Separate topics with a comma.
</div>
</div> </div>
<div <div
v-if="activeDid" v-if="activeDid"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8" class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
> >
<div class="mb-2">Usage Limits</div> <div class="mb-2 font-bold">Usage Limits</div>
<!-- show spinner if loading limits --> <!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="text-center"> <div v-if="loadingLimits" class="text-center">
Checking&hellip; <fa icon="spinner" class="fa-spin"></fa> Checking&hellip; <fa icon="spinner" class="fa-spin"></fa>
@ -200,7 +224,7 @@
</div> </div>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
<div>Data Export</div> <div class="mb-2 font-bold">Data Export</div>
<router-link <router-link
:to="{ name: 'seed-backup' }" :to="{ name: 'seed-backup' }"
v-if="activeDid" v-if="activeDid"
@ -211,7 +235,7 @@
<button <button
v-bind:class="computedStartDownloadLinkClassNames()" v-bind:class="computedStartDownloadLinkClassNames()"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6" class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="exportDatabase()" @click="exportDatabase()"
> >
Download Settings & Contacts Download Settings & Contacts
@ -221,7 +245,7 @@
<a <a
ref="downloadLink" ref="downloadLink"
v-bind:class="computedDownloadLinkClassNames()" v-bind:class="computedDownloadLinkClassNames()"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" class="block w-full text-center text-md uppercase bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
> >
If no download happened yet, click again here to download now. If no download happened yet, click again here to download now.
</a> </a>
@ -503,7 +527,7 @@
<button> <button>
<router-link <router-link
:to="{ name: 'statistics' }" :to="{ name: 'statistics' }"
class="block w-fit text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2" class="block w-fit text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2"
> >
See Global Animated History of Giving See Global Animated History of Giving
</router-link> </router-link>
@ -950,13 +974,13 @@ export default class AccountViewView extends Vue {
public computedStartDownloadLinkClassNames() { public computedStartDownloadLinkClassNames() {
return { return {
invisible: this.downloadUrl, hidden: this.downloadUrl,
}; };
} }
public computedDownloadLinkClassNames() { public computedDownloadLinkClassNames() {
return { return {
invisible: !this.downloadUrl, hidden: !this.downloadUrl,
}; };
} }

4
src/views/ConfirmContactView.vue

@ -30,9 +30,10 @@
</div> </div>
<div class="mt-8"> <div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<input <input
type="submit" type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
value="Add Contact" value="Add Contact"
/> />
<button <button
@ -42,6 +43,7 @@
Cancel Cancel
</button> </button>
</div> </div>
</div>
</section> </section>
</template> </template>

10
src/views/ContactAmountsView.vue

@ -119,7 +119,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import { import {
AgreeVerifiableCredential, AgreeVerifiableCredential,
GiveServerRecord, GiveSummaryRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
SCHEMA_ORG_CONTEXT, SCHEMA_ORG_CONTEXT,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
@ -131,7 +131,7 @@ export default class ContactAmountssView extends Vue {
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
contact: Contact | null = null; contact: Contact | null = null;
giveRecords: Array<GiveServerRecord> = []; giveRecords: Array<GiveSummaryRecord> = [];
numAccounts = 0; numAccounts = 0;
async beforeCreate() { async beforeCreate() {
@ -197,7 +197,7 @@ export default class ContactAmountssView extends Vue {
async loadGives(activeDid: string, contact: Contact) { async loadGives(activeDid: string, contact: Contact) {
try { try {
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
let result: Array<GiveServerRecord> = []; let result: Array<GiveSummaryRecord> = [];
const url = const url =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
@ -252,7 +252,7 @@ export default class ContactAmountssView extends Vue {
); );
} }
const sortedResult: Array<GiveServerRecord> = R.sort( const sortedResult: Array<GiveSummaryRecord> = R.sort(
(a, b) => (a, b) =>
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(), new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
result, result,
@ -271,7 +271,7 @@ export default class ContactAmountssView extends Vue {
} }
} }
async confirm(record: GiveServerRecord) { async confirm(record: GiveSummaryRecord) {
// Make claim // Make claim
// I use clone here because otherwise it gets a Proxy object. // I use clone here because otherwise it gets a Proxy object.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

4
src/views/ContactScanView.vue

@ -65,9 +65,10 @@
/> />
<div class="mt-8"> <div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<input <input
type="submit" type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
value="Look Up Contact" value="Look Up Contact"
/> />
<button <button
@ -77,6 +78,7 @@
Cancel Cancel
</button> </button>
</div> </div>
</div>
</section> </section>
</template> </template>

55
src/views/ContactsView.vue

@ -10,7 +10,8 @@
<span /> <span />
<span> <span>
<a <a
@click="showHintsForOnboarding()" href="/help-onboarding"
target="_blank"
class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1" class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1"
> >
Onboarding Guide Onboarding Guide
@ -302,7 +303,7 @@ import {
import { import {
CONTACT_CSV_HEADER, CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
GiveServerRecord, GiveSummaryRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
isDid, isDid,
RegisterVerifiableCredential, RegisterVerifiableCredential,
@ -409,7 +410,7 @@ export default class ContactsView extends Vue {
} }
const handleResponse = ( const handleResponse = (
resp: { status: number; data: { data: GiveServerRecord[] } }, resp: { status: number; data: { data: GiveSummaryRecord[] } },
descriptions: Record<string, string>, descriptions: Record<string, string>,
confirmed: Record<string, number>, confirmed: Record<string, number>,
unconfirmed: Record<string, number>, unconfirmed: Record<string, number>,
@ -510,18 +511,6 @@ export default class ContactsView extends Vue {
} }
} }
showHintsForOnboarding() {
this.$notify(
{
group: "alert",
type: "info",
title: "Onboard Someone",
text: libsUtil.ONBOARD_MESSAGE,
},
-1,
);
}
async onClickNewContact(): Promise<void> { async onClickNewContact(): Promise<void> {
if (!this.contactInput) { if (!this.contactInput) {
this.$notify( this.$notify(
@ -531,7 +520,7 @@ export default class ContactsView extends Vue {
title: "No Contact", title: "No Contact",
text: "There was no contact info to add.", text: "There was no contact info to add.",
}, },
-1, 3000,
); );
return; return;
} }
@ -559,7 +548,7 @@ export default class ContactsView extends Vue {
title: "Contacts Added", title: "Contacts Added",
text: "Each contact was added. Nothing was sent to the server.", text: "Each contact was added. Nothing was sent to the server.",
}, },
-1, // keeping it up so that the "visibility" message is seen 3000, // keeping it up so that the "visibility" message is seen
); );
} catch (e) { } catch (e) {
this.$notify( this.$notify(
@ -664,7 +653,7 @@ export default class ContactsView extends Vue {
title: "No Contact Info", title: "No Contact Info",
text: "The contact info could not be parsed.", text: "The contact info could not be parsed.",
}, },
-1, 3000,
); );
return; return;
} else { } else {
@ -686,7 +675,7 @@ export default class ContactsView extends Vue {
title: "Incomplete Contact", title: "Incomplete Contact",
text: "Cannot add a contact without a DID.", text: "Cannot add a contact without a DID.",
}, },
-1, 5000,
); );
return; return;
} }
@ -698,7 +687,7 @@ export default class ContactsView extends Vue {
title: "Invalid DID", title: "Invalid DID",
text: "The DID is not valid. It must begin with 'did:'", text: "The DID is not valid. It must begin with 'did:'",
}, },
-1, 5000,
); );
return; return;
} }
@ -737,7 +726,7 @@ export default class ContactsView extends Vue {
title: "Contact Added", title: "Contact Added",
text: addedMessage, text: addedMessage,
}, },
-1, // keeping it up so that the "visibility" message is seen 3000,
); );
}) })
.catch((err) => { .catch((err) => {
@ -853,7 +842,7 @@ export default class ContactsView extends Vue {
title: "Registration Still Unknown", title: "Registration Still Unknown",
text: message, text: message,
}, },
-1, 5000,
); );
} else if (resp.data?.success?.handleId) { } else if (resp.data?.success?.handleId) {
contact.registered = true; contact.registered = true;
@ -892,7 +881,7 @@ export default class ContactsView extends Vue {
title: "Registration Error", title: "Registration Error",
text: userMessage, text: userMessage,
}, },
-1, 5000,
); );
} }
} }
@ -933,7 +922,7 @@ export default class ContactsView extends Vue {
(visibility ? "" : "not ") + (visibility ? "" : "not ") +
"see your activity.", "see your activity.",
}, },
-1, 3000,
); );
} }
contact.seesMe = visibility; contact.seesMe = visibility;
@ -953,7 +942,7 @@ export default class ContactsView extends Vue {
title: "Error Setting Visibility", title: "Error Setting Visibility",
text: message, text: message,
}, },
-1, 5000,
); );
} }
} catch (err) { } catch (err) {
@ -965,7 +954,7 @@ export default class ContactsView extends Vue {
title: "Error Setting Visibility", title: "Error Setting Visibility",
text: "Check connectivity and try again.", text: "Check connectivity and try again.",
}, },
-1, 5000,
); );
} }
} }
@ -997,7 +986,7 @@ export default class ContactsView extends Vue {
(visibility ? "" : "not ") + (visibility ? "" : "not ") +
"see your activity.", "see your activity.",
}, },
-1, 3000,
); );
} else { } else {
console.error("Got bad server response checking visibility:", resp); console.error("Got bad server response checking visibility:", resp);
@ -1009,7 +998,7 @@ export default class ContactsView extends Vue {
title: "Error Checking Visibility", title: "Error Checking Visibility",
text: message, text: message,
}, },
-1, 5000,
); );
} }
} catch (err) { } catch (err) {
@ -1021,7 +1010,7 @@ export default class ContactsView extends Vue {
title: "Error Checking Visibility", title: "Error Checking Visibility",
text: "Check connectivity and try again.", text: "Check connectivity and try again.",
}, },
-1, 3000,
); );
} }
} }
@ -1069,7 +1058,7 @@ export default class ContactsView extends Vue {
title: "Input Error", title: "Input Error",
text: "This is not a valid number of hours: " + this.hourInput, text: "This is not a valid number of hours: " + this.hourInput,
}, },
-1, 3000,
); );
} else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) { } else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) {
this.$notify( this.$notify(
@ -1079,7 +1068,7 @@ export default class ContactsView extends Vue {
title: "Input Error", title: "Input Error",
text: "Giving no hours or description does nothing.", text: "Giving no hours or description does nothing.",
}, },
-1, 3000,
); );
} else if (!identity) { } else if (!identity) {
this.$notify( this.$notify(
@ -1089,7 +1078,7 @@ export default class ContactsView extends Vue {
title: "Status Error", title: "Status Error",
text: "No identifier is available.", text: "No identifier is available.",
}, },
-1, 3000,
); );
} else { } else {
// ask to confirm amount // ask to confirm amount
@ -1218,7 +1207,7 @@ export default class ContactsView extends Vue {
title: "Error Sending Give", title: "Error Sending Give",
text: userMessage, text: userMessage,
}, },
-1, 5000,
); );
} }
} }

77
src/views/GiftedDetails.vue

@ -92,9 +92,15 @@
<p class="text-center mb-2 mt-6 italic"> <p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world Sign & Send to publish to the world
<fa
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
/>
</p> </p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button <button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@click="confirm" @click="confirm"
> >
Sign &amp; Send Sign &amp; Send
@ -105,6 +111,7 @@
> >
Cancel Cancel
</button> </button>
</div>
</section> </section>
</template> </template>
@ -288,41 +295,31 @@ export default class GiftedDetails extends Vue {
} }
async confirm() { async confirm() {
if (!this.activeDid) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "toast", type: "danger",
text: "Recording the give...", title: "Error",
title: "", text: "You must select an identifier before you can record a give.",
}, },
1000, 2000,
); );
// this is asynchronous, but we don't need to wait for it to complete return;
await this.recordGive();
} }
if (parseFloat(this.amountInput) < 0) {
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param amountInput may be 0
* @param unitCode may be omitted, defaults to "HUR"
*/
public async recordGive() {
if (!this.activeDid) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", text: "You may not send a negative number.",
text: "You must select an identifier before you can record a give.", title: "",
}, },
-1, 2000,
); );
return; return;
} }
if (!this.description && !parseFloat(this.amountInput)) {
if (!this.description && !this.amountInput) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -332,11 +329,33 @@ export default class GiftedDetails extends Vue {
this.libsUtil.UNIT_LONG[this.unitCode] this.libsUtil.UNIT_LONG[this.unitCode]
}.`, }.`,
}, },
-1, 2000,
); );
return; return;
} }
this.$notify(
{
group: "alert",
type: "toast",
text: "Recording the give...",
title: "",
},
1000,
);
// this is asynchronous, but we don't need to wait for it to complete
await this.recordGive();
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param amountInput may be 0
* @param unitCode may be omitted, defaults to "HUR"
*/
public async recordGive() {
try { try {
const identity = await libsUtil.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
@ -424,5 +443,17 @@ export default class GiftedDetails extends Vue {
result.response?.data?.error?.message result.response?.data?.error?.message
); );
} }
explainData() {
this.$notify(
{
group: "alert",
type: "success",
title: "Data Sharing",
text: libsUtil.PRIVACY_MESSAGE,
},
-1,
);
}
} }
</script> </script>

13
src/views/HelpNotificationsView.vue

@ -301,12 +301,13 @@ import { sendTestThroughPushServer } from "@/libs/util";
export default class HelpNotificationsView extends Vue { export default class HelpNotificationsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
subscription: PushSubscription | null = null; subscriptionJSON?: PushSubscriptionJSON;
async mounted() { async mounted() {
try { try {
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription(); const fullSub = await registration.pushManager.getSubscription();
this.subscriptionJSON = fullSub?.toJSON();
} catch (error) { } catch (error) {
console.error("Mount error:", error); console.error("Mount error:", error);
} }
@ -315,13 +316,13 @@ export default class HelpNotificationsView extends Vue {
alertWebPushSubscription() { alertWebPushSubscription() {
console.log( console.log(
"Web push subscription:", "Web push subscription:",
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging JSON.parse(JSON.stringify(this.subscriptionJSON)), // gives more info than plain console logging
); );
alert(JSON.stringify(this.subscription)); alert(JSON.stringify(this.subscriptionJSON));
} }
async sendTestWebPushMessage(skipFilter: boolean = false) { async sendTestWebPushMessage(skipFilter: boolean = false) {
if (!this.subscription) { if (!this.subscriptionJSON) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -336,7 +337,7 @@ export default class HelpNotificationsView extends Vue {
} }
try { try {
await sendTestThroughPushServer(this.subscription, skipFilter); await sendTestThroughPushServer(this.subscriptionJSON, skipFilter);
this.$notify( this.$notify(
{ {

69
src/views/HelpOnboardingView.vue

@ -0,0 +1,69 @@
<template>
<!-- Don't include nav buttons since this is shown in a different window. -->
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Don't include 'back' button since this is shown in a different window. -->
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Time Safari Onboarding Instructions
</h1>
</div>
<!-- eslint-disable prettier/prettier -->
<div class="ml-4">
<h1 class="font-bold text-xl">Install</h1>
<div>
<p>
1) Have them visit TimeSafari.app in a browser, preferably Chrome or Safari.
</p>
<p>
2) Have them "Install" the site to their desktop.
</p>
</div>
<h1 class="font-bold text-xl">Add Contact & Register</h1>
<div>
<p>
3) Have them follow their yellow prompts.
</p>
<p>
4) Add them to your contacts <fa icon="users" />
</p>
<p>
5) Register them <fa icon="person-circle-question" />
</p>
<p>
6) Add yourself to their contacts <fa icon="users" />
</p>
</div>
<h1 class="font-bold text-xl">Enable Notifications</h1>
<div>
<p>
7) Enable notifications from <fa icon="circle-user" />
</p>
</div>
<h1 class="font-bold text-xl">Discuss Backups</h1>
<div>
<p>
8) Exporting backups <fa icon="circle-user" /> are important if they lose their phone --- especially for the Identifier Seed!
</p>
</div>
</div>
<!-- eslint enable -->
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { QuickNav } })
export default class Help extends Vue {}
</script>

37
src/views/HelpView.vue

@ -39,22 +39,22 @@
and network. and network.
</p> </p>
<p> <p>
You can show giving and also offer help to ideas, based on others' You highlight giving and also offer help to ideas -- which could be
willingness to help out, too. You can record your own ideas and invite conditional on others' willingness to help, too.
others to collaborate. You can record your own ideas and invite others to collaborate.
</p> </p>
<p> <p>
This app uses the power of cryptography to build a reputation, recording This app uses the power of cryptography to build a reputation, recording
activity that you can share at your discretion. You put some activity activity that you can share at your discretion. You put some activity
public, but your sensitive information is not shared with anyone, public, but these services don't share your ID with others without explicit consent.
including our services. This is in contrast to Meta and Google, who hold This is in contrast to Meta and Google, who hold
your data and allow you use it. Those services are useful, but they have your data and allow you use it while they manage sharing...
the control; this app gives you the control. those services are useful but they have the control, whereas this app gives you the control.
</p> </p>
<h2 class="text-xl font-semibold">How do I get started?</h2> <h2 class="text-xl font-semibold">How do I get started?</h2>
<p> <p>
You need someone to register you -- usually the person who told you You need someone to register you, like the person who told you
about this app, on the Contacts about this app, on the Contacts
<fa icon="users" class="fa-fw" /> page. After they register you, you can <fa icon="users" class="fa-fw" /> page. After they register you, you can
select any contact on the home page (or "anonymous") and record your select any contact on the home page (or "anonymous") and record your
@ -83,9 +83,9 @@
<h2 class="text-xl font-semibold">How do I add someone else?</h2> <h2 class="text-xl font-semibold">How do I add someone else?</h2>
<p> <p>
<button class="text-blue-500" @click="showOnboardInfo"> <a href="/help-onboarding" target="_blank" class="text-blue-500">
Click here to show an alert with the steps. Click here to show an alert with the steps.
</button> </a>
To start scanning, go To start scanning, go
<router-link class="text-blue-500" to="/contact-qr">here.</router-link> <router-link class="text-blue-500" to="/contact-qr">here.</router-link>
</p> </p>
@ -198,9 +198,7 @@
<ul> <ul>
<li class="list-disc list-outside ml-4"> <li class="list-disc list-outside ml-4">
Chrome: Chrome:
<a href="chrome://settings/content/all" class="text-blue-500" Clear at chrome://settings/content/all and
>clear here</a
>
also clear under dev tools Application also clear under dev tools Application
</li> </li>
<li class="list-disc list-outside ml-4"> <li class="list-disc list-outside ml-4">
@ -378,7 +376,6 @@ import { Component, Vue } from "vue-facing-decorator";
import * as Package from "../../package.json"; import * as Package from "../../package.json";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { ONBOARD_MESSAGE } from "@/libs/util";
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class Help extends Vue { export default class Help extends Vue {
@ -386,17 +383,5 @@ export default class Help extends Vue {
package = Package; package = Package;
commitHash = import.meta.env.VITE_GIT_HASH; commitHash = import.meta.env.VITE_GIT_HASH;
showOnboardInfo() {
this.$notify(
{
group: "alert",
type: "info",
title: "Onboard Someone",
text: ONBOARD_MESSAGE,
},
-1,
);
}
} }
</script> </script>

198
src/views/HomeView.vue

@ -118,7 +118,9 @@
<h2 class="text-xl font-bold">Record Something Given By:</h2> <h2 class="text-xl font-bold">Record Something Given By:</h2>
</div> </div>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5"> <ul
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5"
>
<li @click="openDialog()"> <li @click="openDialog()">
<img <img
src="../assets/blank-square.svg" src="../assets/blank-square.svg"
@ -158,7 +160,7 @@
</router-link> </router-link>
<button <button
@click="openGiftedPrompts()" @click="openGiftedPrompts()"
class="block text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" class="block text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
> >
Ideas... Ideas...
</button> </button>
@ -172,10 +174,29 @@
showGivenToUser="true" showGivenToUser="true"
/> />
<GiftedPrompts ref="giftedPrompts" /> <GiftedPrompts ref="giftedPrompts" />
<FeedFilters ref="feedFilters" />
<!-- Results List --> <!-- Results List -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-bold mb-4">Latest Activity</h2> <div class="flex items-center mb-4">
<h2 class="text-xl font-bold">Latest Activity</h2>
<button @click="openFeedFilters()" class="block text-center ml-auto">
<span class="text-sm uppercase text-white">
<span
v-if="resultsAreFiltered()"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md"
>
Filtered
</span>
<span
v-else
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md"
>
Unfiltered
</span>
</span>
</button>
</div>
<InfiniteScroll @reached-bottom="loadMoreGives"> <InfiniteScroll @reached-bottom="loadMoreGives">
<ul class="border-t border-slate-300"> <ul class="border-t border-slate-300">
<li <li
@ -191,37 +212,36 @@
</div> </div>
<div class="grid grid-cols-12"> <div class="grid grid-cols-12">
<span class="col-span-11 justify-self-start"> <span class="col-span-1 justify-self-start">
<span> <span>
<fa <fa
v-if="record.giver.known || record.receiver.known" v-if="record.giver.known || record.receiver.known"
icon="circle-user" icon="circle-user"
class="col-span-1 pt-1 pl-0 pr-3 text-slate-500" class="pt-1 text-slate-500"
/>
<fa
v-else
icon="gift"
class="col-span-1 pt-1 pl-3 pr-0 text-slate-500"
/> />
<fa v-else icon="gift" class="pt-1 pl-3 text-slate-500" />
</span>
</span> </span>
<span class="col-span-10 justify-self-stretch">
<span class="pl-2">
{{ giveDescription(record) }} {{ giveDescription(record) }}
</span>
<a @click="onClickLoadClaim(record.jwtId)"> <a @click="onClickLoadClaim(record.jwtId)">
<fa <fa
icon="circle-info" icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer" class="pl-2 text-blue-500 cursor-pointer"
></fa> ></fa>
</a> </a>
</span> </span>
<span class="col-span-1 justify-self-end shrink"> <span class="col-span-1 justify-self-end">
<router-link <router-link
v-if="record.fulfillsPlanHandleId" v-if="record.fulfillsPlanHandleId"
:to=" :to="
'/project/' + '/project/' +
encodeURIComponent(record.fulfillsPlanHandleId) encodeURIComponent(record.fulfillsPlanHandleId)
" "
class="justify-end"
> >
<fa icon="hammer" class="ml-4 pl-2 text-blue-500"></fa> <fa icon="hammer" class="text-blue-500"></fa>
</router-link> </router-link>
</span> </span>
</div> </div>
@ -238,11 +258,17 @@
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip; <fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip;
</p> </p>
</div> </div>
<div v-if="!isFeedLoading && feedData.length === 0">
<p class="text-slate-500 text-center italic mt-4 mb-4">
No claims match your filters.
</p>
</div>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import * as R from "ramda";
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
@ -250,6 +276,7 @@ import { Component, Vue } from "vue-facing-decorator";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import GiftedPrompts from "@/components/GiftedPrompts.vue"; import GiftedPrompts from "@/components/GiftedPrompts.vue";
import FeedFilters from "@/components/FeedFilters.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
@ -257,22 +284,30 @@ import { NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import {
BoundingBox,
isAnyFeedFilterOn,
MASTER_SETTINGS_KEY,
Settings,
} from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import {
contactForDid, contactForDid,
containsNonHiddenDid,
didInfoForContact, didInfoForContact,
getPlanFromCache,
GiverInputInfo, GiverInputInfo,
GiveServerRecord, GiveSummaryRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { generateSaveAndActivateIdentity } from "@/libs/util"; import { generateSaveAndActivateIdentity } from "@/libs/util";
interface GiveRecordWithContactInfo extends GiveServerRecord { interface GiveRecordWithContactInfo extends GiveSummaryRecord {
giver: { giver: {
displayName: string; displayName: string;
known: boolean; known: boolean;
}; };
image: string; image: string;
recipientProjectName: string | undefined;
receiver: { receiver: {
displayName: string; displayName: string;
known: boolean; known: boolean;
@ -283,6 +318,7 @@ interface GiveRecordWithContactInfo extends GiveServerRecord {
components: { components: {
GiftedDialog, GiftedDialog,
GiftedPrompts, GiftedPrompts,
FeedFilters,
QuickNav, QuickNav,
EntityIcon, EntityIcon,
InfiniteScroll, InfiniteScroll,
@ -299,9 +335,16 @@ export default class HomeView extends Vue {
feedData: GiveRecordWithContactInfo[] = []; feedData: GiveRecordWithContactInfo[] = [];
feedPreviousOldestId?: string; feedPreviousOldestId?: string;
feedLastViewedClaimId?: string; feedLastViewedClaimId?: string;
isAnyFeedFilterOn: boolean;
isCreatingIdentifier = false; isCreatingIdentifier = false;
isFeedFilteredByVisible = false;
isFeedFilteredByNearby = false;
isFeedLoading = true; isFeedLoading = true;
isRegistered = false; isRegistered = false;
searchBoxes: Array<{
name: string;
bbox: BoundingBox;
}> = [];
showShortcutBvc = false; showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
@ -324,7 +367,7 @@ export default class HomeView extends Vue {
return headers; return headers;
} }
async created() { async mounted() {
try { try {
await accountsDB.open(); await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
@ -336,9 +379,14 @@ export default class HomeView extends Vue {
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedClaimId = settings?.lastViewedClaimId; this.feedLastViewedClaimId = settings?.lastViewedClaimId;
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.searchBoxes = settings?.searchBoxes || [];
this.showShortcutBvc = !!settings?.showShortcutBvc; this.showShortcutBvc = !!settings?.showShortcutBvc;
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
if (this.allMyDids.length === 0) { if (this.allMyDids.length === 0) {
this.isCreatingIdentifier = true; this.isCreatingIdentifier = true;
this.activeDid = await generateSaveAndActivateIdentity(); this.activeDid = await generateSaveAndActivateIdentity();
@ -367,6 +415,10 @@ export default class HomeView extends Vue {
} }
} }
resultsAreFiltered() {
return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
}
notificationsSupported() { notificationsSupported() {
return "Notification" in window; return "Notification" in window;
} }
@ -376,27 +428,34 @@ export default class HomeView extends Vue {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
const identity = await this.getIdentity(this.activeDid);
if (this.activeDid) { if (this.activeDid) {
await accountsDB.open(); if (identity) {
const allAccounts = await accountsDB.accounts.toArray(); headers["Authorization"] = "Bearer " + (await accessToken(identity));
const account = allAccounts.find( } else {
(acc) => acc.did === this.activeDid,
) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error( throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.", "An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
); );
} }
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else { } else {
// it's OK without auth... we just won't get any identifiers // it's OK without auth... we just won't get any identifiers
} }
return headers; return headers;
} }
// only called when a setting was changed
async reloadFeedOnChange() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
this.feedData = [];
this.feedPreviousOldestId = undefined;
this.updateAllFeed();
}
/** /**
* Data loader used by infinite scroller * Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load * @param payload is the flag from the InfiniteScroll indicating if it should load
@ -407,14 +466,30 @@ export default class HomeView extends Vue {
} }
} }
latLongInAnySearchBox(lat: number, long: number) {
for (const boxInfo of this.searchBoxes) {
if (
boxInfo.bbox.westLong <= long &&
long <= boxInfo.bbox.eastLong &&
boxInfo.bbox.minLat <= lat &&
lat <= boxInfo.bbox.maxLat
) {
return true;
}
}
}
public async updateAllFeed() { public async updateAllFeed() {
this.isFeedLoading = true; this.isFeedLoading = true;
let endOfResults = true;
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId) await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
.then(async (results) => { .then(async (results) => {
if (results.data.length > 0) { if (results.data.length > 0) {
endOfResults = false;
// include the descriptions of the giver and receiver // include the descriptions of the giver and receiver
const newFeedData: GiveRecordWithContactInfo = results.data.map( const identity = await this.getIdentity(this.activeDid);
(record: GiveServerRecord) => { const newFeedData: Array<Promise<GiveRecordWithContactInfo>> =
results.data.map(async (record: GiveSummaryRecord) => {
// similar code is in endorser-mobile utility.ts // similar code is in endorser-mobile utility.ts
// claim.claim happen for some claims wrapped in a Verifiable Credential // claim.claim happen for some claims wrapped in a Verifiable Credential
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -425,6 +500,36 @@ export default class HomeView extends Vue {
// recipient.did is for legacy data, before March 2023 // recipient.did is for legacy data, before March 2023
const recipientDid = const recipientDid =
claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
const plan = await getPlanFromCache(
record.fulfillsPlanHandleId,
identity,
this.axios,
this.apiServer,
);
// check if the record should be filtered out
let anyMatch = false;
if (
this.isFeedFilteredByVisible &&
containsNonHiddenDid(record)
) {
// has a visible DID so it's a keeper
anyMatch = true;
}
if (!anyMatch && this.isFeedFilteredByNearby) {
// check if the associated project has a location inside user's search box
if (record.fulfillsPlanHandleId) {
if (plan?.locLat && plan?.locLon) {
if (this.latLongInAnySearchBox(plan.locLat, plan.locLon)) {
anyMatch = true;
}
}
}
}
if (this.isAnyFeedFilterOn && !anyMatch) {
return null;
}
return { return {
...record, ...record,
giver: didInfoForContact( giver: didInfoForContact(
@ -434,6 +539,7 @@ export default class HomeView extends Vue {
this.allMyDids, this.allMyDids,
), ),
image: claim.image, image: claim.image,
recipientProjectName: plan?.name,
receiver: didInfoForContact( receiver: didInfoForContact(
recipientDid, recipientDid,
this.activeDid, this.activeDid,
@ -441,9 +547,11 @@ export default class HomeView extends Vue {
this.allMyDids, this.allMyDids,
), ),
}; };
}, });
); const allNewFeedData: GiveRecordWithContactInfo[] =
this.feedData = this.feedData.concat(newFeedData); await Promise.all(newFeedData);
const filteredFeedData = allNewFeedData.filter(R.isNotNil);
this.feedData = this.feedData.concat(filteredFeedData);
this.feedPreviousOldestId = this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId; results.data[results.data.length - 1].jwtId;
// The following update is only done on the first load. // The following update is only done on the first load.
@ -470,6 +578,10 @@ export default class HomeView extends Vue {
-1, -1,
); );
}); });
if (this.feedData.length === 0 && !endOfResults) {
// repeat until there's at least some data
this.updateAllFeed();
}
this.isFeedLoading = false; this.isFeedLoading = false;
} }
@ -536,12 +648,28 @@ export default class HomeView extends Vue {
return `${giverInfo.displayName} gave to ${recipientInfo.displayName}: ${gaveAmount}`; return `${giverInfo.displayName} gave to ${recipientInfo.displayName}: ${gaveAmount}`;
} else if (giverInfo.known) { } else if (giverInfo.known) {
// giver is named but recipient is not // giver is named but recipient is not
// show the project name if to one
if (giveRecord.recipientProjectName) {
// retrieve the project name
return `${giverInfo.displayName} gave: ${gaveAmount} (to the project ${giveRecord.recipientProjectName})`;
}
// it's not to a project
return `${giverInfo.displayName} gave: ${gaveAmount} (to ${recipientInfo.displayName})`; return `${giverInfo.displayName} gave: ${gaveAmount} (to ${recipientInfo.displayName})`;
} else if (recipientInfo.known) { } else if (recipientInfo.known) {
// recipient is named but giver is not // recipient is named but giver is not
return `${recipientInfo.displayName} received: ${gaveAmount} (from ${giverInfo.displayName})`; return `${recipientInfo.displayName} received: ${gaveAmount} (from ${giverInfo.displayName})`;
} else { } else {
// neither giver nor recipient are named // neither giver nor recipient are named
// show the project name if to one
if (giveRecord.recipientProjectName) {
// retrieve the project name
return `${gaveAmount} (to the project ${giveRecord.recipientProjectName})`;
}
// it's not to a project
let peopleInfo; let peopleInfo;
if (giverInfo.displayName === recipientInfo.displayName) { if (giverInfo.displayName === recipientInfo.displayName) {
peopleInfo = `between two who are ${giverInfo.displayName}`; peopleInfo = `between two who are ${giverInfo.displayName}`;
@ -574,5 +702,9 @@ export default class HomeView extends Vue {
openGiftedPrompts() { openGiftedPrompts() {
(this.$refs.giftedPrompts as GiftedPrompts).open(); (this.$refs.giftedPrompts as GiftedPrompts).open();
} }
openFeedFilters() {
(this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange);
}
} }
</script> </script>

4
src/views/ImportAccountView.vue

@ -56,9 +56,10 @@
</div> </div>
<div class="mt-8"> <div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button <button
@click="fromMnemonic()" @click="fromMnemonic()"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
> >
Import Import
</button> </button>
@ -70,6 +71,7 @@
Cancel Cancel
</button> </button>
</div> </div>
</div>
</section> </section>
</template> </template>

4
src/views/ImportDerivedAccountView.vue

@ -49,9 +49,10 @@
</ul> </ul>
</div> </div>
<div class="mt-8"> <div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button <button
@click="incrementDerivation()" @click="incrementDerivation()"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
> >
Increment and Import Increment and Import
</button> </button>
@ -63,6 +64,7 @@
Cancel Cancel
</button> </button>
</div> </div>
</div>
</section> </section>
</template> </template>

2
src/views/NewEditAccountView.vue

@ -22,6 +22,7 @@
/> />
<div class="mt-8"> <div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button <button
type="button" type="button"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
@ -38,6 +39,7 @@
Cancel Cancel
</button> </button>
</div> </div>
</div>
</section> </section>
</template> </template>

13
src/views/NewEditProjectView.vue

@ -73,16 +73,17 @@
/> />
<label for="includeLocation">Include Location</label> <label for="includeLocation">Include Location</label>
</div> </div>
<div v-if="includeLocation" style="height: 600px; width: 800px"> <div v-if="includeLocation" class="mb-4 aspect-video">
<div class="px-2 py-2"> <p class="text-sm mb-2 text-slate-500">
For your security, choose a location nearby but not exactly at the For your security, choose a location nearby but not exactly at the
place. place.
</div> </p>
<l-map <l-map
ref="map" ref="map"
v-model:zoom="zoom" v-model:zoom="zoom"
:center="[0, 0]" :center="[0, 0]"
class="!z-40 rounded-md"
@click=" @click="
(event) => { (event) => {
latitude = event.latlng.lat; latitude = event.latlng.lat;
@ -104,6 +105,7 @@
</div> </div>
<div class="mt-8"> <div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button <button
:disabled="isHiddenSave" :disabled="isHiddenSave"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
@ -127,6 +129,7 @@
Cancel Cancel
</button> </button>
</div> </div>
</div>
</section> </section>
</template> </template>
@ -265,6 +268,8 @@ export default class NewEditProjectView extends Vue {
vcClaim.agent = { vcClaim.agent = {
identifier: this.agentDid, identifier: this.agentDid,
}; };
} else {
delete vcClaim.agent;
} }
if (this.includeLocation) { if (this.includeLocation) {
vcClaim.location = { vcClaim.location = {
@ -274,6 +279,8 @@ export default class NewEditProjectView extends Vue {
longitude: this.longitude, longitude: this.longitude,
}, },
}; };
} else {
delete vcClaim.location;
} }
// Make a payload for the claim // Make a payload for the claim
const vcPayload = { const vcPayload = {

40
src/views/ProjectViewView.vue

@ -98,7 +98,7 @@
</div> </div>
<a @click="onClickLoadClaim(projectId)" class="cursor-pointer"> <a @click="onClickLoadClaim(projectId)" class="cursor-pointer">
<fa icon="circle-info" class="pl-2 pt-1 text-blue-500" /> <fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
</a> </a>
</div> </div>
@ -134,7 +134,9 @@
<div class="text-center"> <div class="text-center">
<p class="mt-2 mb-4 text-center">Record a contribution from:</p> <p class="mt-2 mb-4 text-center">Record a contribution from:</p>
</div> </div>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5"> <ul
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5"
>
<li @click="openGiftDialog({ name: 'you', did: activeDid })"> <li @click="openGiftDialog({ name: 'you', did: activeDid })">
<fa icon="hand" class="fa-fw text-slate-400 text-5xl" /> <fa icon="hand" class="fa-fw text-slate-400 text-5xl" />
<h3 <h3
@ -230,7 +232,7 @@
@click="onClickLoadClaim(offer.jwtId as string)" @click="onClickLoadClaim(offer.jwtId as string)"
class="cursor-pointer" class="cursor-pointer"
> >
<fa icon="circle-info" class="pl-2 pt-1 text-blue-500" /> <fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
</a> </a>
<a <a
v-if="checkIsFulfillable(offer)" v-if="checkIsFulfillable(offer)"
@ -289,7 +291,7 @@
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<a @click="onClickLoadClaim(give.jwtId)"> <a @click="onClickLoadClaim(give.jwtId)">
<fa icon="circle-info" class="text-blue-500 cursor-pointer" /> <fa icon="file-lines" class="text-blue-500 cursor-pointer" />
</a> </a>
<a v-if="checkIsConfirmable(give)" @click="confirmClaim(give)"> <a v-if="checkIsConfirmable(give)" @click="confirmClaim(give)">
<fa icon="circle-check" class="text-blue-500 cursor-pointer" /> <fa icon="circle-check" class="text-blue-500 cursor-pointer" />
@ -360,11 +362,11 @@ import { accessToken } from "@/libs/crypto";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { import {
BLANK_GENERIC_SERVER_RECORD, BLANK_GENERIC_SERVER_RECORD,
GenericServerRecord, GenericCredWrapper,
GiverInputInfo, GiverInputInfo,
GiveServerRecord, GiveSummaryRecord,
OfferServerRecord, OfferSummaryRecord,
PlanServerRecord, PlanSummaryRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
@ -388,14 +390,14 @@ export default class ProjectViewView extends Vue {
apiServer = ""; apiServer = "";
description = ""; description = "";
expanded = false; expanded = false;
fulfilledByThis: PlanServerRecord | null = null; fulfilledByThis: PlanSummaryRecord | null = null;
fulfillersToThis: Array<PlanServerRecord> = []; fulfillersToThis: Array<PlanSummaryRecord> = [];
givesToThis: Array<GiveServerRecord> = []; givesToThis: Array<GiveSummaryRecord> = [];
issuer = ""; issuer = "";
latitude = 0; latitude = 0;
longitude = 0; longitude = 0;
name = ""; name = "";
offersToThis: Array<OfferServerRecord> = []; offersToThis: Array<OfferSummaryRecord> = [];
projectId = localStorage.getItem("projectId") || ""; // handle ID projectId = localStorage.getItem("projectId") || ""; // handle ID
showDidCopy = false; showDidCopy = false;
timeSince = ""; timeSince = "";
@ -718,8 +720,8 @@ export default class ProjectViewView extends Vue {
this.$router.push(route); this.$router.push(route);
} }
checkIsFulfillable(offer: OfferServerRecord) { checkIsFulfillable(offer: OfferSummaryRecord) {
const offerRecord: GenericServerRecord = { const offerRecord: GenericCredWrapper = {
...BLANK_GENERIC_SERVER_RECORD, ...BLANK_GENERIC_SERVER_RECORD,
claim: offer.fullClaim, claim: offer.fullClaim,
claimType: "Offer", claimType: "Offer",
@ -728,8 +730,8 @@ export default class ProjectViewView extends Vue {
return libsUtil.canFulfillOffer(offerRecord); return libsUtil.canFulfillOffer(offerRecord);
} }
onClickFulfillGiveToOffer(offer: OfferServerRecord) { onClickFulfillGiveToOffer(offer: OfferSummaryRecord) {
const offerRecord: GenericServerRecord = { const offerRecord: GenericCredWrapper = {
...BLANK_GENERIC_SERVER_RECORD, ...BLANK_GENERIC_SERVER_RECORD,
claim: offer.fullClaim, claim: offer.fullClaim,
issuer: offer.offeredByDid, issuer: offer.offeredByDid,
@ -768,8 +770,8 @@ export default class ProjectViewView extends Vue {
} }
} }
checkIsConfirmable(give: GiveServerRecord) { checkIsConfirmable(give: GiveSummaryRecord) {
const giveDetails: GenericServerRecord = { const giveDetails: GenericCredWrapper = {
...BLANK_GENERIC_SERVER_RECORD, ...BLANK_GENERIC_SERVER_RECORD,
claim: give.fullClaim, claim: give.fullClaim,
claimType: "GiveAction", claimType: "GiveAction",
@ -779,7 +781,7 @@ export default class ProjectViewView extends Vue {
} }
// similar code is found in ClaimView // similar code is found in ClaimView
async confirmClaim(give: GiveServerRecord) { async confirmClaim(give: GiveSummaryRecord) {
if (confirm("Do you personally confirm that this is true?")) { if (confirm("Do you personally confirm that this is true?")) {
// similar logic is found in endorser-mobile // similar logic is found in endorser-mobile
const goodClaim = serverUtil.removeSchemaContext( const goodClaim = serverUtil.removeSchemaContext(

6
src/views/ProjectsView.vue

@ -167,7 +167,7 @@
<a @click="onClickLoadClaim(offer.jwtId)"> <a @click="onClickLoadClaim(offer.jwtId)">
<fa <fa
icon="circle-info" icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer" class="pl-2 text-blue-500 cursor-pointer"
></fa> ></fa>
</a> </a>
@ -223,7 +223,7 @@ import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import ProjectIcon from "@/components/ProjectIcon.vue"; import ProjectIcon from "@/components/ProjectIcon.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { OfferServerRecord, PlanData } from "@/libs/endorserServer"; import { OfferSummaryRecord, PlanData } from "@/libs/endorserServer";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
@Component({ @Component({
@ -237,7 +237,7 @@ export default class ProjectsView extends Vue {
currentIid: IIdentifier; currentIid: IIdentifier;
isLoading = false; isLoading = false;
numAccounts = 0; numAccounts = 0;
offers: OfferServerRecord[] = []; offers: OfferSummaryRecord[] = [];
showOffers = true; showOffers = true;
showProjects = false; showProjects = false;

8
src/views/QuickActionBvcEndView.vue

@ -60,7 +60,7 @@
}} }}
<a @click="onClickLoadClaim(record.id)"> <a @click="onClickLoadClaim(record.id)">
<fa <fa
icon="circle-info" icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer" class="pl-2 text-blue-500 cursor-pointer"
/> />
</a> </a>
@ -146,7 +146,7 @@ import {
createAndSubmitConfirmation, createAndSubmitConfirmation,
createAndSubmitGive, createAndSubmitGive,
ErrorResult, ErrorResult,
GenericServerRecord, GenericCredWrapper,
GenericVerifiableCredential, GenericVerifiableCredential,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
@ -166,7 +166,7 @@ export default class QuickActionBvcBeginView extends Vue {
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
claimCountWithHidden = 0; claimCountWithHidden = 0;
claimsToConfirm: GenericServerRecord[] = []; claimsToConfirm: GenericCredWrapper[] = [];
claimsToConfirmSelected: string[] = []; claimsToConfirmSelected: string[] = [];
description = "breakfast"; description = "breakfast";
loadingConfirms = true; loadingConfirms = true;
@ -228,7 +228,7 @@ export default class QuickActionBvcBeginView extends Vue {
} }
await response.json().then((data) => { await response.json().then((data) => {
const dataByOthers = R.reject( const dataByOthers = R.reject(
(claim: GenericServerRecord) => claim.issuer === this.activeDid, (claim: GenericCredWrapper) => claim.issuer === this.activeDid,
data, data,
); );
const dataByOthersWithoutHidden = R.reject( const dataByOthersWithoutHidden = R.reject(

8
src/views/SearchAreaView.vue

@ -64,10 +64,11 @@
</div> </div>
</div> </div>
<div style="height: 600px; width: 800px"> <div class="mb-4 aspect-video">
<l-map <l-map
ref="map" ref="map"
:center="[localCenterLat, localCenterLong]" :center="[localCenterLat, localCenterLong]"
class="!z-40 rounded-md"
v-model:zoom="localZoom" v-model:zoom="localZoom"
@click="setMapPoint" @click="setMapPoint"
> >
@ -208,9 +209,9 @@ export default class DiscoverView extends Vue {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Saved", title: "Saved",
text: "That has been saved in your preferences.", text: "That has been saved in your preferences. You can now filter by it on your home screen feed.",
}, },
-1, 7000,
); );
this.$router.back(); this.$router.back();
} catch (err) { } catch (err) {
@ -246,6 +247,7 @@ export default class DiscoverView extends Vue {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: [], searchBoxes: [],
filterFeedByNearby: false,
}); });
this.searchBox = null; this.searchBox = null;
this.localCenterLat = 0; this.localCenterLat = 0;

10
src/views/StartView.vue

@ -23,6 +23,7 @@
<!-- id used by puppeteer test script --> <!-- id used by puppeteer test script -->
<div id="start-question" class="mt-8"> <div id="start-question" class="mt-8">
<div class="max-w-3xl mx-auto">
<p class="text-center text-xl font-light"> <p class="text-center text-xl font-light">
Do you want a new identifier of your own? Do you want a new identifier of your own?
</p> </p>
@ -36,24 +37,27 @@
</p> </p>
<a <a
@click="onClickYes()" @click="onClickYes()"
class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
> >
Yes, generate one Yes, generate one
</a> </a>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<a <a
@click="onClickNo()" @click="onClickNo()"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-2" class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
> >
No, I have a seed No, I have a seed
</a> </a>
<a <a
v-if="numAccounts > 0" v-if="numAccounts > 0"
@click="onClickDerive()" @click="onClickDerive()"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-2" class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
> >
Derive new address from existing seed Derive new address from existing seed
</a> </a>
</div> </div>
</div>
</div>
</section> </section>
</template> </template>

Loading…
Cancel
Save