sw-cleanup #92

Open
anomalist wants to merge 29 commits from sw-cleanup into master
  1. 4
      CHANGELOG.md
  2. 13
      README.md
  3. 72
      project.task.yaml
  4. 68
      src/App.vue
  5. 13
      src/constants/app.ts
  6. 3
      src/db/index.ts
  7. 2
      src/db/tables/settings.ts
  8. 6
      src/libs/endorserServer.ts
  9. 3
      src/libs/util.ts
  10. 8
      src/main.ts
  11. 8
      src/router/index.ts
  12. 87
      src/views/AccountViewView.vue
  13. 25
      src/views/ContactQRScanShowView.vue
  14. 142
      src/views/ContactsView.vue
  15. 4
      src/views/DiscoverView.vue
  16. 65
      src/views/HelpNotificationsView.vue
  17. 15
      src/views/HelpView.vue
  18. 10
      src/views/HomeView.vue
  19. 61
      src/views/NewEditProjectView.vue
  20. 96
      src/views/ProjectViewView.vue
  21. 26
      sw_scripts/additional-scripts.js
  22. 11
      sw_scripts/safari-notifications.js
  23. 10
      web-push.md

4
CHANGELOG.md

@ -8,8 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- Web push notifications
## [0.1.3] - 2023.11 ## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde
### Added ### Added
- Contact name editing - Contact name editing
### Changed ### Changed

13
README.md

@ -64,6 +64,10 @@ Under the "Your Identity" screen, click "Advanced", click "Switch Identity / No
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain. For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
### Icons
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
### Manual walk-through ### Manual walk-through
- Clear the browser cache for localhost for a new user. - Clear the browser cache for localhost for a new user.
@ -90,7 +94,7 @@ For your own web-push tests, change the 'vapid' URL in App.vue, and install apps
- Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard. - Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard.
- Go back to /start and import test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` with this this seed phrase: - Go back to /start and import test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` with this this seed phrase:
`seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control` `rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).) (Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
- Go to "Your Contacts" screen and add the ID you copied to the clipboard, and hit "+" to add them. - Go to "Your Contacts" screen and add the ID you copied to the clipboard, and hit "+" to add them.
@ -99,10 +103,9 @@ For your own web-push tests, change the 'vapid' URL in App.vue, and install apps
### Clear/Reset data & restart ### Clear/Reset data & restart
* Data: Clear the browser cache for localhost. * Clear cache for localhost.
* Notifications: * Unregister service worker (in Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers` or `about:debugging`).
* Under browser settings, look for "notification" and remove this server. * Clear notification permission (in Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search).
* Under about:debugging, find the service worker and Unregister.

72
project.task.yaml

@ -1,51 +1,59 @@
tasks: tasks:
- remove hard-coded anomalistlabs.com
- don't show "Give" & "Offer" on project screen if they don't have an identifier
- allow some gives even if they aren't registered
- in endorser-push-server - mount folder for persistent sqlite DB outside of container
- extract private_key_hex in webpush.py
- 40 notifications : - 40 notifications :
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew - push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
- extract private_key_hex in py-push-server webpush.py
- lock down regenerate_vapid endpoint (so only we admins can do it on demand)
- remove sleep in py-push-server app.py
- revisit "maybe" and "never" buttons on accont screen
- see if we can detect OS-level notifications if turned off
- write troubleshooting docs for notifications
- .2 change the "claims" verbiage in feeds (eg. safari-notifications.js)
- .5 allow to manage their notifications even without an identity
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
- .3 fix the Project-location-selection map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui - .3 fix the Project-location-selection map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
- .5 Add infinite scroll to gifts on the home page - .5 Add infinite scroll to gifts on the home page
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
- .2 figure out why endorser-mobile search doesn't find recently created PlanAction
- .1 when creating a plan, select location and then make sure you can deselect on Android
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164 assignee:trent
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
- fix cert generation (since it didn't happen automatically for Nov 30)
- Discuss whether the remaining tasks are worthwhile before MVP release. - .5 If notifications are not enabled, add message to front page with link/button to enable
- show VC details... somehow:
- .5 make a VC details page, or link to endorser.ch (including confirmations)
- 01 allow download of each VC (& confirmations, to show that they actually own their data)
- 04 allow user to download VCs, mine + ones I can see about me from others
- add VC confirmation?
- Release Minimum Viable Product :
- generate new webpush.db entry, webpush.py private_key_hex & subscription_info & vapid_claims email
- .5 deploy endorser.ch server above Dec 1 (to get plan searches by names as well as descriptions)
- 08 thorough testing for errors & edge cases
- 01 ensure ability to recover server remotely, and add redundant access
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
- Add disclaimers.
- Switch default server to the public server.
- Deploy to a server.
- Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS.
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
- allow some gives even if they aren't registered
- .5 Add start date to project
- .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?
- 04 allow user to download claims, mine + ones I can see about me from others
- .5 customize favicon assignee-group:ui - .5 customize favicon assignee-group:ui
- .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 - 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 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 make a VC details page, or link to endorser.ch
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
- .5 include the hash of the latest commit on help page next to version (maybe Trent's git-hash branch) - .5 include the hash of the latest commit on help page next to version (maybe Trent's git-hash branch)
- .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)
- bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent - bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too) - switch some checks for activeDid to check for isRegistered
- allow download of each VC (to show that they can actually own their data) - .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
- .5 fix cert generation on server (since it didn't happen automatically for Nov 30)
- contacts v+ : - contacts v+ :
- 01 Import all the non-sensitive data (ie. contacts & settings). - 01 Import all the non-sensitive data (ie. contacts & settings).
- .2 show error to user when adding a duplicate contact - .2 show error to user when adding a duplicate contact
- 01 parse input more robustly (with CSV lib and not commas) - 01 parse input more robustly (with CSV lib and not commas)
- stats v1 : - stats v1 :
- 01 show numeric stats - 01 show numeric stats
- 04 show different graphic for projects vs people (gnome?) on world - 04 show different graphic for projects vs people (gnome?) on world
@ -54,21 +62,9 @@ tasks:
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version") - maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie) - 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
- Release Minimum Viable Product :
- generate new webpush.db entries, data/webpush.db private_key_hex & subscription_info & vapid_claims email
- .5 deploy endorser.ch server above Dec 1 (to get plan searches by names as well as descriptions)
- 08 thorough testing for errors & edge cases
- 01 ensure ability to recover server remotely, and add redundant access
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
- Add disclaimers.
- Switch default server to the public server.
- Deploy to a server.
- Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS.
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- .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
- 24 Move to Vite - 24 Move to Vite
- 32 accept images for projects - 32 accept images for projects

68
src/App.vue

@ -261,7 +261,7 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component } from "vue-facing-decorator"; import { Vue, Component } from "vue-facing-decorator";
import axios, { AxiosError } from "axios"; import axios from "axios";
interface ServiceWorkerMessage { interface ServiceWorkerMessage {
type: string; type: string;
data: string; data: string;
@ -285,22 +285,64 @@ interface VapidResponse {
}; };
} }
import { AppString } from "@/constants/app";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component @Component
export default class App extends Vue { export default class App extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
b64 = ""; b64 = "";
mounted() {
axios async mounted() {
.get("https://timesafari-pwa.anomalistlabs.com/web-push/vapid") try {
.then((response: VapidResponse) => { await db.open();
this.b64 = response.data.vapidKey; const settings = await db.settings.get(MASTER_SETTINGS_KEY);
console.log(this.b64); let pushUrl: string = AppString.DEFAULT_PUSH_SERVER;
navigator.serviceWorker.addEventListener("controllerchange", () => { if (settings?.webPushServer) {
console.log("New service worker is now controlling the page"); pushUrl = settings.webPushServer;
}
await axios
.get(pushUrl + "/web-push/vapid")
.then((response: VapidResponse) => {
this.b64 = response.data?.vapidKey || "";
console.log("Got vapid key:", this.b64);
navigator.serviceWorker.addEventListener("controllerchange", () => {
console.log("New service worker is now controlling the page");
});
}); });
}) if (!this.b64) {
.catch((error: AxiosError) => { this.$notify(
console.error("API error", error); {
}); group: "alert",
type: "danger",
title: "Error Setting Notifications",
text: "Could not set notifications.",
},
-1,
);
}
} catch (error) {
console.error("Got an error initializing notifications:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Notifications",
text: "Got an error setting notifications.",
},
-1,
);
}
} }
private sendMessageToServiceWorker( private sendMessageToServiceWorker(

13
src/constants/app.ts

@ -4,20 +4,27 @@
* See also ../libs/veramo/setup.ts * See also ../libs/veramo/setup.ts
*/ */
export enum AppString { export enum AppString {
APP_NAME = "TimeSafari", APP_NAME = "Time Safari",
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch", PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch", TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000", LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER, DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
PROD_PUSH_SERVER = "https://timesafari.app",
TEST1_PUSH_SERVER = "https://test.timesafari.app",
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
DEFAULT_PUSH_SERVER = TEST1_PUSH_SERVER,
} }
/** /**
* See notiwind package * The possible values for "group" and "type" are in App.vue.
* From the notiwind package
*/ */
export interface NotificationIface { export interface NotificationIface {
group: string; group: string; // "alert" | "modal"
type: string; // "toast" | "info" | "success" | "warning" | "danger" type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string; title: string;
text: string; text: string;

3
src/db/index.ts

@ -45,5 +45,8 @@ db.on("populate", () => {
db.settings.add({ db.settings.add({
id: MASTER_SETTINGS_KEY, id: MASTER_SETTINGS_KEY,
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER, apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
// remember that things you add from now on aren't automatically in the DB for old users
webPushServer: AppString.DEFAULT_PUSH_SERVER,
}); });
}); });

2
src/db/tables/settings.ts

@ -20,9 +20,9 @@ export type Settings = {
lastViewedClaimId?: string; // Last viewed claim ID lastViewedClaimId?: string; // Last viewed claim ID
lastNotifiedClaimId?: string; // Last notified claim ID lastNotifiedClaimId?: string; // Last notified claim ID
isRegistered?: boolean; isRegistered?: boolean;
webPushServer?: string; // Web Push server URL
// Array of named search boxes defined by bounding boxes // Array of named search boxes defined by bounding boxes
searchBoxes?: Array<{ searchBoxes?: Array<{
name: string; name: string;
bbox: BoundingBox; bbox: BoundingBox;

6
src/libs/endorserServer.ts

@ -69,6 +69,8 @@ export interface OfferServerRecord {
validThrough: string; validThrough: string;
} }
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id4
export interface GiveVerifiableCredential { export interface GiveVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree "@context"?: string; // optional when embedded, eg. in an Agree
"@type": "GiveAction"; "@type": "GiveAction";
@ -80,6 +82,8 @@ export interface GiveVerifiableCredential {
recipient?: { identifier: string }; recipient?: { identifier: string };
} }
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id8
export interface OfferVerifiableCredential { export interface OfferVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree "@context"?: string; // optional when embedded, eg. in an Agree
"@type": "Offer"; "@type": "Offer";
@ -93,6 +97,8 @@ export interface OfferVerifiableCredential {
validThrough?: string; validThrough?: string;
} }
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id7
export interface PlanVerifiableCredential { export interface PlanVerifiableCredential {
"@context": "https://schema.org"; "@context": "https://schema.org";
"@type": "PlanAction"; "@type": "PlanAction";

3
src/libs/util.ts

@ -0,0 +1,3 @@
export const isGlobalUri = (uri: string) => {
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
};

8
src/main.ts

@ -14,6 +14,7 @@ import {
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faBan, faBan,
faBitcoinSign,
faBurst, faBurst,
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
@ -27,6 +28,7 @@ import {
faCoins, faCoins,
faComment, faComment,
faCopy, faCopy,
faDollar,
faEllipsisVertical, faEllipsisVertical,
faEye, faEye,
faEyeSlash, faEyeSlash,
@ -34,6 +36,7 @@ import {
faFloppyDisk, faFloppyDisk,
faFolderOpen, faFolderOpen,
faGift, faGift,
faGlobe,
faHand, faHand,
faHouseChimney, faHouseChimney,
faLocationDot, faLocationDot,
@ -44,6 +47,7 @@ import {
faPersonCircleCheck, faPersonCircleCheck,
faPersonCircleQuestion, faPersonCircleQuestion,
faPlus, faPlus,
faQuestion,
faQrcode, faQrcode,
faRotate, faRotate,
faShareNodes, faShareNodes,
@ -61,6 +65,7 @@ library.add(
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faBan, faBan,
faBitcoinSign,
faBurst, faBurst,
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
@ -74,6 +79,7 @@ library.add(
faCoins, faCoins,
faComment, faComment,
faCopy, faCopy,
faDollar,
faEllipsisVertical, faEllipsisVertical,
faEye, faEye,
faEyeSlash, faEyeSlash,
@ -81,6 +87,7 @@ library.add(
faFloppyDisk, faFloppyDisk,
faFolderOpen, faFolderOpen,
faGift, faGift,
faGlobe,
faHand, faHand,
faHouseChimney, faHouseChimney,
faLocationDot, faLocationDot,
@ -92,6 +99,7 @@ library.add(
faPersonCircleQuestion, faPersonCircleQuestion,
faPlus, faPlus,
faQrcode, faQrcode,
faQuestion,
faRotate, faRotate,
faShareNodes, faShareNodes,
faSpinner, faSpinner,

8
src/router/index.ts

@ -91,6 +91,14 @@ const routes: Array<RouteRecordRaw> = [
component: () => component: () =>
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"), import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
}, },
{
path: "/help-notifications",
name: "help-notifications",
component: () =>
import(
/* webpackChunkName: "help-notifications" */ "../views/HelpNotificationsView.vue"
),
},
{ {
path: "/identity-switcher", path: "/identity-switcher",
name: "identity-switcher", name: "identity-switcher",

87
src/views/AccountViewView.vue

@ -294,6 +294,14 @@
</div> </div>
</label> </label>
<div class="flex py-2">
<button class="text-blue-500">
<router-link :to="{ name: 'statistics' }" class="block text-center">
See Global Animated History of Giving
</router-link>
</button>
</div>
<div class="flex py-2"> <div class="flex py-2">
<button class="text-blue-500"> <button class="text-blue-500">
<!-- id used by puppeteer test script --> <!-- id used by puppeteer test script -->
@ -307,14 +315,6 @@
</button> </button>
</div> </div>
<div class="flex py-2">
<button class="text-blue-500">
<router-link :to="{ name: 'statistics' }" class="block text-center">
See Achievements & Statistics
</router-link>
</button>
</div>
<div class="flex py-4"> <div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2> <h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
<input <input
@ -330,30 +330,71 @@
<fa icon="floppy-disk" class="fa-fw" color="white"></fa> <fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button> </button>
<button <button
class="px-4 rounded bg-slate-200 border border-slate-400" class="px-3 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.PROD_ENDORSER_API_SERVER)" @click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
> >
Use Prod Use Prod
</button> </button>
<button <button
class="px-4 rounded bg-slate-200 border border-slate-400" class="px-3 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.TEST_ENDORSER_API_SERVER)" @click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
> >
Use Test Use Test
</button> </button>
<button <button
class="px-4 rounded bg-slate-200 border border-slate-400" class="px-3 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.LOCAL_ENDORSER_API_SERVER)" @click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
> >
Use Local Use Local
</button> </button>
</div> </div>
<div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2">
Notification Push Server
</h2>
<input
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
v-model="webPushServerInput"
/>
<button
v-if="webPushServerInput != webPushServer"
class="px-4 rounded bg-red-500 border border-slate-400"
@click="onClickSavePushServer()"
>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.PROD_PUSH_SERVER"
>
Use Prod
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.TEST1_PUSH_SERVER"
>
Use Test 1
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.TEST2_PUSH_SERVER"
>
Use Test 2
</button>
</div>
<span class="px-4 text-sm" v-if="!webPushServerInput">
When that setting is blank, this app will use the default web push
server URL:
{{ AppConstants.DEFAULT_PUSH_SERVER }}
</span>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError, AxiosRequestConfig } from "axios";
import "dexie-export-import"; import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
@ -387,7 +428,7 @@ interface IAccount {
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
Constants = AppString; AppConstants = AppString;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
@ -398,6 +439,8 @@ export default class AccountViewView extends Vue {
numAccounts = 0; numAccounts = 0;
publicHex = ""; publicHex = "";
publicBase64 = ""; publicBase64 = "";
webPushServer = "";
webPushServerInput = "";
limits: RateLimits | null = null; limits: RateLimits | null = null;
limitsMessage = ""; limitsMessage = "";
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
@ -545,6 +588,8 @@ export default class AccountViewView extends Vue {
(settings?.firstName || "") + (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3 (settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.webPushServer = (settings?.webPushServer as string) || "";
this.webPushServerInput = (settings?.webPushServer as string) || "";
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
} }
@ -769,7 +814,7 @@ export default class AccountViewView extends Vue {
private async fetchRateLimits(identity: IIdentifier) { private async fetchRateLimits(identity: IIdentifier) {
const url = `${this.apiServer}/api/report/rateLimits`; const url = `${this.apiServer}/api/report/rateLimits`;
const headers = await this.getHeaders(identity); const headers = await this.getHeaders(identity);
return await this.axios.get(url, { headers }); return await this.axios.get(url, { headers } as AxiosRequestConfig);
} }
/** /**
@ -872,8 +917,12 @@ export default class AccountViewView extends Vue {
this.apiServer = this.apiServerInput; this.apiServer = this.apiServerInput;
} }
setApiServerInput(value: string) { async onClickSavePushServer() {
this.apiServerInput = value; await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
webPushServer: this.webPushServerInput,
});
this.webPushServer = this.webPushServerInput;
} }
} }
</script> </script>

25
src/views/ContactQRScanShowView.vue

@ -20,7 +20,7 @@
</h1> </h1>
</div> </div>
<div @click="onCopyToClipboard()"> <div @click="onCopyToClipboard()" v-if="activeDid">
<!-- <!--
Play with display options: https://qr-code-styling.com/ Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3 See docs: https://www.npmjs.com/package/qr-code-generator-vue3
@ -32,6 +32,15 @@
class="flex justify-center" class="flex justify-center"
/> />
</div> </div>
<div class="text-center" v-else>
You have no identitifiers yet, so
<router-link :to="{ name: 'start' }" class="text-blue-500">
create your identifier.
</router-link>
<br />
We recommend you do that first; otherwise, these contacts won't see your
activity.
</div>
<h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1> <h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1>
<qrcode-stream @detect="onScanDetect" @error="onScanError" /> <qrcode-stream @detect="onScanDetect" @error="onScanError" />
@ -91,7 +100,7 @@ export default class ContactQRScanShow extends Vue {
if (!identity) { if (!identity) {
throw new Error( throw new Error(
"Attempted to load Give records with no identity available.", "Attempted to show contact info with no identity available.",
); );
} }
return identity; return identity;
@ -106,17 +115,7 @@ export default class ContactQRScanShow extends Vue {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
if (!account) { if (account) {
this.$notify(
{
group: "alert",
type: "warning",
title: "",
text: "You have no identity yet.",
},
-1,
);
} else {
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
const publicKeyHex = identity.keys[0].publicKeyHex; const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64"); const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");

142
src/views/ContactsView.vue

@ -27,7 +27,7 @@
</span> </span>
<input <input
type="text" type="text"
placeholder="DID, Name, Public Key" placeholder="DID, Name, Public Key (base 16 or 64)"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2" class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
v-model="contactInput" v-model="contactInput"
/> />
@ -110,48 +110,50 @@
</div> </div>
<div id="ContactActions" class="flex gap-1.5 mt-2"> <div id="ContactActions" class="flex gap-1.5 mt-2">
<button <div v-if="activeDid">
v-if="contact.seesMe" <button
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md" v-if="contact.seesMe"
@click="setVisibility(contact, false, true)" class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
title="They can see you" @click="setVisibility(contact, false, true)"
> title="They can see you"
<fa icon="eye" class="fa-fw" /> >
</button> <fa icon="eye" class="fa-fw" />
<button </button>
v-else <button
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="setVisibility(contact, true, true)"
title="They cannot see you"
>
<fa icon="eye-slash" class="fa-fw" />
</button>
<button
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="checkVisibility(contact)"
title="Check Visibility"
>
<fa icon="rotate" class="fa-fw" />
</button>
<button
@click="register(contact)"
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
>
<fa
v-if="contact.registered"
icon="person-circle-check"
class="fa-fw"
title="Registered"
/>
<fa
v-else v-else
icon="person-circle-question" class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
class="fa-fw" @click="setVisibility(contact, true, true)"
title="Registration Unknown" title="They cannot see you"
/> >
</button> <fa icon="eye-slash" class="fa-fw" />
</button>
<button
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="checkVisibility(contact)"
title="Check Visibility"
v-if="activeDid"
>
<fa icon="rotate" class="fa-fw" />
</button>
<button
@click="register(contact)"
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
v-if="activeDid"
>
<fa
v-if="contact.registered"
icon="person-circle-check"
class="fa-fw"
title="Registered"
/>
<fa
v-else
icon="person-circle-question"
class="fa-fw"
title="Registration Unknown"
/>
</button>
</div>
<button <button
@click="deleteContact(contact)" @click="deleteContact(contact)"
@ -263,6 +265,7 @@ import {
SimpleSigner, SimpleSigner,
} from "@/libs/crypto"; } from "@/libs/crypto";
import { import {
CONTACT_URL_PREFIX,
GiveServerRecord, GiveServerRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
RegisterVerifiableCredential, RegisterVerifiableCredential,
@ -303,6 +306,7 @@ export default class ContactsView extends Vue {
givenToMeUnconfirmed: Record<string, number> = {}; givenToMeUnconfirmed: Record<string, number> = {};
hourDescriptionInput = ""; hourDescriptionInput = "";
hourInput = "0"; hourInput = "0";
isRegistered = false;
showGiveNumbers = false; showGiveNumbers = false;
showGiveTotals = true; showGiveTotals = true;
showGiveConfirmed = true; showGiveConfirmed = true;
@ -312,6 +316,7 @@ export default class ContactsView extends Vue {
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.isRegistered = !!settings?.isRegistered;
this.showGiveNumbers = !!settings?.showContactGivesInline; this.showGiveNumbers = !!settings?.showContactGivesInline;
if (this.showGiveNumbers) { if (this.showGiveNumbers) {
@ -475,6 +480,12 @@ export default class ContactsView extends Vue {
); );
return; return;
} }
if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) {
await this.newContactFromScan(this.contactInput);
return;
}
let did = this.contactInput; let did = this.contactInput;
let name, publicKeyBase64; let name, publicKeyBase64;
const commaPos1 = this.contactInput.indexOf(","); const commaPos1 = this.contactInput.indexOf(",");
@ -493,7 +504,7 @@ export default class ContactsView extends Vue {
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64"); publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
} }
const newContact = { did, name, publicKeyBase64 }; const newContact = { did, name, publicKeyBase64 };
return this.addContact(newContact); await this.addContact(newContact);
} }
async newContactFromScan(url: string): Promise<void> { async newContactFromScan(url: string): Promise<void> {
@ -540,31 +551,39 @@ export default class ContactsView extends Vue {
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""), (a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts, allContacts,
); );
this.setVisibility(newContact, true, false); let addedMessage;
if (this.activeDid) {
this.setVisibility(newContact, true, false);
addedMessage =
newContact.name +
" was added, and your activity is visible to them.";
} else {
addedMessage = newContact.name + " was added.";
}
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Contact Added", title: "Contact Added",
text: text: addedMessage,
newContact.name +
" was added, and your activity is visible to them.",
}, },
-1, 5000,
);
// putting this last so that it shows on the top
this.$notify(
{
group: "alert",
type: "info",
title: "New User?",
text:
"If " +
newContact.name +
" is a new user, be sure to register them.",
},
-1,
); );
if (this.isRegistered) {
// putting this last so that it shows on the top
this.$notify(
{
group: "alert",
type: "info",
title: "New User?",
text:
"If " +
newContact.name +
" is a new user, be sure to register them.",
},
-1,
);
}
}) })
.catch((err) => { .catch((err) => {
console.error("Error when adding contact to storage:", err); console.error("Error when adding contact to storage:", err);
@ -938,6 +957,7 @@ export default class ContactsView extends Vue {
} }
} }
// similar function is in endorserServer.ts
private async createAndSubmitGive( private async createAndSubmitGive(
identity: IIdentifier, identity: IIdentifier,
fromDid: string, fromDid: string,

4
src/views/DiscoverView.vue

@ -358,9 +358,7 @@ export default class DiscoverView extends Vue {
const plans: ProjectData[] = results.data; const plans: ProjectData[] = results.data;
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId = plan.handleId, rowid } = plan; const { name, description, handleId = plan.handleId, rowid } = plan;
if (beforeId !== plan["rowid"]) { this.projects.push({ name, description, handleId, rowid });
this.projects.push({ name, description, handleId, rowid });
}
} }
} else { } else {
this.projects = results.data; this.projects = results.data;

65
src/views/HelpNotificationsView.vue

@ -0,0 +1,65 @@
<template>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Notification Help
</h1>
</div>
<div>
<p>Here are things to try to get notifications working.</p>
<h2 class="text-xl font-semibold">Test</h2>
<p>Somehow call the service-worker self.showNotification</p>
<h2 class="text-xl font-semibold">Check OS-level permissions</h2>
<p>
Walk-throughs & screenshots, maybe for all combinations of OS &
browsers.
</p>
<h2 class="text-xl font-semibold">Check browser-level permissions</h2>
<p>Walk-throughs & screenshots for browser settings</p>
<h2 class="text-xl font-semibold">Explain full reset to start again</h2>
<p>
Walk-throughs for clearing everything & subscribing anew to get a
message
</p>
<h2 class="text-xl font-semibold">Auto-detection</h2>
<p>Show results of auto-detection whether they're turned on</p>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } })
export default class HelpNotificationsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
}
</script>

15
src/views/HelpView.vue

@ -181,6 +181,21 @@
different page. different page.
</p> </p>
<h2 class="text-xl font-semibold">
How do I access even more functionality?
</h2>
<p>
There is an "Advanced" section at the bottom of the Account
<fa icon="circle-user" /> page.
</p>
<p>
There is a even more functionality in a mobile app (and more
documentation) at
<a href="https://endorser.ch" class="text-blue-500">
EndorserSearch.com
</a>
</p>
<h2 class="text-xl font-semibold">What is your privacy policy?</h2> <h2 class="text-xl font-semibold">What is your privacy policy?</h2>
<p> <p>
See See

10
src/views/HomeView.vue

@ -104,7 +104,7 @@
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm" class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedId" v-if="record.jwtId == feedLastViewedId"
> >
You've seen all the following before You've seen all the following
</div> </div>
<div class="flex"> <div class="flex">
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa> <fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
@ -171,13 +171,7 @@ export default class HomeView extends Vue {
.equals(activeDid) .equals(activeDid)
.first()) as Account; .first()) as Account;
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse(account?.identity || "null");
return identity; // may be null
if (!identity) {
throw new Error(
"Attempted to load Give records with no identity available.",
);
}
return identity;
} }
public async getHeaders(identity: IIdentifier) { public async getHeaders(identity: IIdentifier) {

61
src/views/NewEditProjectView.vue

@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa ><fa icon="chevron-left" class="fa-fw"></fa
></router-link> ></router-link>
[New/Edit] Plan Edit Idea
</h1> </h1>
</div> </div>
@ -24,22 +24,28 @@
<input <input
type="text" type="text"
placeholder="Project Name" placeholder="Idea Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="projectName" v-model="fullClaim.name"
/> />
<textarea <textarea
placeholder="Description" placeholder="Description"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
rows="5" rows="5"
v-model="description" v-model="fullClaim.description"
maxlength="500" maxlength="5000"
></textarea> ></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4"> <div class="text-xs text-slate-500 italic -mt-3 mb-4">
{{ description.length }}/500 max. characters {{ fullClaim.description.length }}/5000 max. characters
</div> </div>
<input
v-model="fullClaim.url"
placeholder="Website"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
/>
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<input <input
type="checkbox" type="checkbox"
@ -72,7 +78,7 @@
name="OpenStreetMap" name="OpenStreetMap"
/> />
<l-marker <l-marker
v-if="latitude || longitude" v-if="latitude && longitude"
:lat-lng="[latitude, longitude]" :lat-lng="[latitude, longitude]"
@click="maybeEraseLatLong()" @click="maybeEraseLatLong()"
/> />
@ -136,13 +142,17 @@ export default class NewEditProjectView extends Vue {
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
description = "";
errorMessage = ""; errorMessage = "";
fullClaim: PlanVerifiableCredential = {
"@context": "https://schema.org",
"@type": "PlanAction",
name: "",
description: "",
}; // this default is only to avoid errors before plan is loaded
includeLocation = false; includeLocation = false;
latitude = 0; latitude = 0;
longitude = 0; longitude = 0;
numAccounts = 0; numAccounts = 0;
projectName = "";
zoom = 2; zoom = 2;
async beforeCreate() { async beforeCreate() {
@ -214,9 +224,12 @@ export default class NewEditProjectView extends Vue {
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
if (resp.status === 200) { if (resp.status === 200) {
const claim = resp.data.claim; this.fullClaim = resp.data.claim;
this.projectName = claim.name; if (this.fullClaim?.location) {
this.description = claim.description; this.includeLocation = true;
this.latitude = this.fullClaim.location.geo.latitude;
this.longitude = this.fullClaim.location.geo.longitude;
}
} }
} catch (error) { } catch (error) {
console.error("Got error retrieving that project", error); console.error("Got error retrieving that project", error);
@ -225,13 +238,7 @@ export default class NewEditProjectView extends Vue {
private async SaveProject(identity: IIdentifier) { private async SaveProject(identity: IIdentifier) {
// Make a claim // Make a claim
const vcClaim: PlanVerifiableCredential = { const vcClaim: PlanVerifiableCredential = this.fullClaim;
"@context": "https://schema.org",
"@type": "PlanAction",
name: this.projectName,
description: this.description,
identifier: this.projectId || undefined,
};
if (this.projectId) { if (this.projectId) {
vcClaim.identifier = this.projectId; vcClaim.identifier = this.projectId;
} }
@ -293,6 +300,20 @@ export default class NewEditProjectView extends Vue {
2000, 2000,
this, this,
); );
} else {
console.log(
"Got unexpected 'data' inside response from server",
resp,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Saving Idea",
text: "Server did not save the idea. Try again.",
},
-1,
);
} }
} catch (error) { } catch (error) {
let userMessage = "There was an error saving the project."; let userMessage = "There was an error saving the project.";
@ -300,8 +321,8 @@ export default class NewEditProjectView extends Vue {
error?: { message?: string }; error?: { message?: string };
}>; }>;
if (serverError) { if (serverError) {
console.log("Got error from server", serverError);
if (Object.prototype.hasOwnProperty.call(serverError, "message")) { if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
console.log(serverError);
userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user. userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user.
this.$notify( this.$notify(
{ {

96
src/views/ProjectViewView.vue

@ -12,7 +12,7 @@
> >
<fa icon="chevron-left" class="fa-fw"></fa> <fa icon="chevron-left" class="fa-fw"></fa>
</button> </button>
View Plan Idea
</h1> </h1>
</div> </div>
@ -35,7 +35,7 @@
<fa icon="user" class="fa-fw text-slate-400"></fa> <fa icon="user" class="fa-fw text-slate-400"></fa>
{{ issuer }} {{ issuer }}
</div> </div>
<div> <div v-if="timeSince">
<fa icon="calendar" class="fa-fw text-slate-400"></fa> <fa icon="calendar" class="fa-fw text-slate-400"></fa>
{{ timeSince }} {{ timeSince }}
</div> </div>
@ -45,8 +45,13 @@
:href="getOpenStreetMapUrl()" :href="getOpenStreetMapUrl()"
target="_blank" target="_blank"
class="underline" class="underline"
> >Map View
Map View </a>
</div>
<div v-if="url">
<fa icon="globe" class="fa-fw text-slate-400"></fa>
<a :href="addScheme(url)" target="_blank" class="underline"
>{{ domainForWebsite(this.url) }}
</a> </a>
</div> </div>
</div> </div>
@ -56,8 +61,11 @@
<div class="text-sm text-slate-500"> <div class="text-sm text-slate-500">
<div v-if="!expanded"> <div v-if="!expanded">
{{ truncatedDesc }} {{ truncatedDesc }}
<a v-if="description.length >= truncateLength" @click="expandText" <a
>Read More</a v-if="description.length >= truncateLength"
@click="expandText"
class="uppercase text-xs font-semibold text-slate-700"
>... Read More</a
> >
</div> </div>
<div v-else> <div v-else>
@ -65,7 +73,7 @@
<a <a
@click="collapseText" @click="collapseText"
class="uppercase text-xs font-semibold text-slate-700" class="uppercase text-xs font-semibold text-slate-700"
>Read Less</a >- Read Less</a
> >
</div> </div>
</div> </div>
@ -148,7 +156,7 @@
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4"> <div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4">
<div class="bg-slate-100 px-4 py-3 rounded-md"> <div class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3"> <h3 class="text-sm uppercase font-semibold mb-3">
Offered To This Project Offered To This Idea
</h3> </h3>
<div v-if="offersToThis.length === 0"> <div v-if="offersToThis.length === 0">
@ -167,8 +175,10 @@
{{ didInfo(offer.agentDid, activeDid, allMyDids, allContacts) }} {{ didInfo(offer.agentDid, activeDid, allMyDids, allContacts) }}
</span> </span>
<span v-if="offer.amount"> <span v-if="offer.amount">
<fa icon="coins" class="fa-fw text-slate-400"></fa> <fa
{{ offer.amount }} :icon="iconForUnitCode(offer.unit)"
class="fa-fw text-slate-400"
/>{{ offer.amount }}
</span> </span>
</div> </div>
<div v-if="offer.objectDescription" class="text-slate-500"> <div v-if="offer.objectDescription" class="text-slate-500">
@ -180,9 +190,7 @@
</div> </div>
<div class="bg-slate-100 px-4 py-3 rounded-md"> <div class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3"> <h3 class="text-sm uppercase font-semibold mb-3">Given To This Idea</h3>
Given To This Project
</h3>
<div v-if="givesToThis.length === 0">(None yet. Record one above.)</div> <div v-if="givesToThis.length === 0">(None yet. Record one above.)</div>
@ -197,9 +205,11 @@
><fa icon="user" class="fa-fw text-slate-400"></fa> ><fa icon="user" class="fa-fw text-slate-400"></fa>
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }} {{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
</span> </span>
<span v-if="give.amount" <span v-if="give.amount">
><fa icon="coins" class="fa-fw text-slate-400"></fa> <fa
{{ give.amount }} :icon="iconForUnitCode(give.unit)"
class="fa-fw text-slate-400"
/>{{ give.amount }}
</span> </span>
</div> </div>
<div v-if="give.description" class="text-slate-500"> <div v-if="give.description" class="text-slate-500">
@ -216,7 +226,7 @@
class="bg-slate-100 px-4 py-3 rounded-md" class="bg-slate-100 px-4 py-3 rounded-md"
> >
<h3 class="text-sm uppercase font-semibold mb-3"> <h3 class="text-sm uppercase font-semibold mb-3">
Contributions To This Project Contributions To This Idea
</h3> </h3>
<ul> <ul>
<li v-for="plan in fulfillersToThis" :key="plan.handleId"> <li v-for="plan in fulfillersToThis" :key="plan.handleId">
@ -232,7 +242,7 @@
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md"> <div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3"> <h3 class="text-sm uppercase font-semibold mb-3">
Contributions By This Project Contributions By This Idea
</h3> </h3>
<button <button
@click="onClickLoadProject(fulfilledByThis.handleId)" @click="onClickLoadProject(fulfilledByThis.handleId)"
@ -267,6 +277,7 @@ import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { isGlobalUri } from "@/libs/util";
import { import {
didInfo, didInfo,
GiverInputInfo, GiverInputInfo,
@ -309,6 +320,7 @@ export default class ProjectViewView extends Vue {
timeSince = ""; timeSince = "";
truncatedDesc = ""; truncatedDesc = "";
truncateLength = 40; truncateLength = 40;
url = "";
async created() { async created() {
await db.open(); await db.open();
@ -410,6 +422,7 @@ export default class ProjectViewView extends Vue {
this.truncatedDesc = this.description.slice(0, this.truncateLength); this.truncatedDesc = this.description.slice(0, this.truncateLength);
this.latitude = resp.data.claim?.location?.geo?.latitude || 0; this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
this.longitude = resp.data.claim?.location?.geo?.longitude || 0; this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
this.url = resp.data.claim?.url || "";
} else if (resp.status === 404) { } else if (resp.status === 404) {
// actually, axios throws an error so we never get here // actually, axios throws an error so we never get here
this.$notify( this.$notify(
@ -627,5 +640,52 @@ export default class ProjectViewView extends Vue {
openOfferDialog() { openOfferDialog() {
(this.$refs.customOfferDialog as OfferDialog).open(); (this.$refs.customOfferDialog as OfferDialog).open();
} }
UNIT_CODES: Record<string, Record<string, string>> = {
BTC: {
name: "Bitcoin",
faIcon: "bitcoin-sign",
},
HUR: {
name: "hours",
faIcon: "clock",
},
USD: {
name: "US Dollars",
faIcon: "dollar",
},
};
iconForUnitCode(unitCode: string) {
return this.UNIT_CODES[unitCode]?.faIcon || "question";
}
// return an HTTPS URL if it's not a global URL
addScheme(url: string) {
if (!isGlobalUri(url)) {
return "https://" + url;
}
return url;
}
// return just the domain for display, if possible
domainForWebsite(url: string) {
try {
const hostname = new URL(url).hostname;
if (!hostname) {
// happens for non-http URLs
return url;
} else if (url.endsWith(hostname)) {
// it's just the domain
return hostname;
} else {
// there's more, but don't bother displaying the whole thing
return hostname + "...";
}
} catch (error: unknown) {
// must not be a valid URL
return url;
}
}
} }
</script> </script>

26
sw_scripts/additional-scripts.js

@ -5,7 +5,7 @@ importScripts(
); );
self.addEventListener("install", (event) => { self.addEventListener("install", (event) => {
console.error(event); console.error("Adding event listener for:", event);
importScripts( importScripts(
"safari-notifications.js", "safari-notifications.js",
"nacl.js", "nacl.js",
@ -23,16 +23,20 @@ self.addEventListener("push", function (event) {
payload = JSON.parse(event.data.text()); payload = JSON.parse(event.data.text());
} }
const message = await self.getNotificationCount(); const message = await self.getNotificationCount();
console.error(message); if (message) {
const title = payload ? payload.title : "Custom Title"; console.log("Will notify user:", message);
const options = { const title = payload ? payload.title : "Custom Title";
body: message, const options = {
icon: payload ? payload.icon : "icon.png", body: message,
badge: payload ? payload.badge : "badge.png", icon: payload ? payload.icon : "icon.png",
}; badge: payload ? payload.badge : "badge.png",
await self.registration.showNotification(title, options); };
await self.registration.showNotification(title, options);
} else {
console.log("No notification message, so will not tell the user.");
}
} catch (error) { } catch (error) {
console.error("Error in processing the push event:", error); console.error("Error processing the push event:", error);
} }
})(), })(),
); );
@ -51,7 +55,7 @@ self.addEventListener("activate", (event) => {
}); });
self.addEventListener("fetch", (event) => { self.addEventListener("fetch", (event) => {
console.log(event.request); console.log("Got fetch event", event.request);
}); });
self.addEventListener("error", (event) => { self.addEventListener("error", (event) => {

11
sw_scripts/safari-notifications.js

@ -466,6 +466,7 @@ async function getNotificationCount() {
if ("secret" in self) { if ("secret" in self) {
secret = self.secret; secret = self.secret;
const secretUint8Array = self.decodeBase64(secret); const secretUint8Array = self.decodeBase64(secret);
// 1 is our master settings ID; see MASTER_SETTINGS_KEY
const settings = await getSettingById(1); const settings = await getSettingById(1);
let lastNotifiedClaimId = null; let lastNotifiedClaimId = null;
if ("lastNotifiedClaimId" in settings) { if ("lastNotifiedClaimId" in settings) {
@ -496,7 +497,7 @@ async function getNotificationCount() {
headers["Authorization"] = "Bearer " + (await accessToken(identifier)); headers["Authorization"] = "Bearer " + (await accessToken(identifier));
let response = await fetch( let response = await fetch(
"https://test-api.endorser.ch/api/v2/report/claims", settings["apiServer"] + "/api/v2/report/claims",
{ {
method: "GET", method: "GET",
headers: headers, headers: headers,
@ -513,15 +514,13 @@ async function getNotificationCount() {
} }
newClaims++; newClaims++;
} }
if (newClaims === 0) { if (newClaims > 0) {
result = "You have no new claims today."; result = `There are ${newClaims} new activities on TimeSafari`;
} else {
result = `${newClaims} have been shared with you`;
} }
const most_recent_notified = claims[0]["id"]; const most_recent_notified = claims[0]["id"];
await setMostRecentNotified(most_recent_notified); await setMostRecentNotified(most_recent_notified);
} else { } else {
console.error(response.status); console.error("The service worker got a bad response status when fetching claims:", response.status, response);
} }
break; break;
} }

10
web-push.md

@ -390,3 +390,13 @@ While notifications are turned on, the user can tap on the App Notifications tog
- "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on). - "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on).
- "Leave it On" to make no changes and dismiss the dialog. - "Leave it On" to make no changes and dismiss the dialog.
# NOTIFICATION STATES
* Unpermissioned. Push server cannot send notifications to the user because it does not have permission.
This may be the same as when the user gave permission in the past but has since revoked it at the OS or browser
level, outside the app. (User can change to Permissioned when the user gives permission.)
* Permissioned. (User can change to Unpermissioned via the OS or browser settings.)
* Active. (User can change to Muted when the user mutes notifications.)
* Muted. (User can change to Active when the user toggles it.)
(Turning mute off automatically after some amount of time is not planned in version 1.)

Loading…
Cancel
Save