Compare commits

..

36 Commits

Author SHA1 Message Date
d26d1d3601 bump to version 0.1.8 2023-12-27 19:50:47 -07:00
1e6159869f update daily check title & documentation 2023-12-27 18:51:49 -07:00
75d15ddeb9 add note to install as an app 2023-12-27 14:46:10 -07:00
051a0a97d8 fix issuer name in project list 2023-12-27 14:13:05 -07:00
f8d3fe2ee1 enhance service-worker logging, allow for filtered-push test 2023-12-27 13:59:24 -07:00
4f0a046723 fix quick-give check on contact page & add message on detailed view 2023-12-27 13:58:42 -07:00
c4a0458c08 doc: add to notification-help page & task list 2023-12-26 21:43:04 -07:00
25b1598fcb doc: add more help for the notifications 2023-12-26 17:48:14 -07:00
ddbb700c34 Merge pull request 'Daily service-worker logging' (#100) from sw-log into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#100
2023-12-25 14:51:22 -05:00
fd8877900b add another alert message & test button 2023-12-25 12:51:06 -07:00
05c6ddda02 allow a test notification from the notification help screen 2023-12-24 21:24:51 -07:00
853eb3c623 include the data in the logged info for a service worker "push" 2023-12-23 19:34:26 -07:00
44cfe0d88e allow notifications even without an ID 2023-12-22 14:22:13 -07:00
7fe256dc9e log service worker messages to the DB (now works) 2023-12-22 12:51:18 -07:00
e739d0be7c update error messages to be less... confusing 2023-12-22 09:19:36 -07:00
8d873b51bd doc: update tasks 2023-12-21 21:03:47 -07:00
d7f4acb702 make more adjustments to try and get logging to work 2023-12-21 20:50:35 -07:00
f8002c4550 add DB logging for service-worker events 2023-12-20 20:40:00 -07:00
d6b1386741 add console logging for all service worker events 2023-12-20 19:49:04 -07:00
50fdd95c60 increment version & add -beta 2023-12-19 20:41:57 -07:00
91c6c7c11c bump to version 0.1.7 2023-12-19 20:39:11 -07:00
4e28dc8de6 update commentary, help, kudos 2023-12-19 20:35:28 -07:00
fb425f0d51 update all icon images to our own art 2023-12-19 20:29:19 -07:00
a19aebcb37 simplify the notification message logic, hopefully fixing what's on servers 2023-12-18 19:12:09 -07:00
d0697c1ef4 fix top warnings when on prod or non-prod servers 2023-12-18 16:06:55 -07:00
1dd2333624 Merge pull request 'claim-view-improvements' (#99) from claim-view-improvements into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#99
2023-12-18 16:54:02 -05:00
Jose Olarte III
b4b78f6a2c Recolored buttons 2023-12-18 19:46:58 +08:00
Jose Olarte III
3c0f6ce0de Design and uniformity tweaks 2023-12-18 19:44:03 +08:00
5534f8fa50 fix logic for prod & test host detection 2023-12-17 20:17:45 -07:00
a5004d475e bump version to next -beta 2023-12-17 20:02:28 -07:00
b445b1234f bump version to 0.1.6 2023-12-17 20:00:12 -07:00
17c96dd01a fix linting, etc with previous feature (env warning) 2023-12-16 17:17:54 -07:00
6ad17101b2 Merge pull request 'add warning if on unexpected server' (#98) from server-warn into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#98
2023-12-16 10:08:17 -05:00
b4085ffaa7 Merge branch 'master' into server-warn 2023-12-16 08:08:00 -07:00
4f2cb55753 add warning if on unexpected server 2023-12-16 08:04:16 -07:00
ebf9164ecc Merge pull request 'add infinite scroll to the home page feed' (#97) from home-infinite into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#97
2023-12-15 07:49:01 -05:00
48 changed files with 1249 additions and 345 deletions

View File

@@ -9,7 +9,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.1.6] ## [0.1.8] - 2023.12.27
### Added
- DB logging for service-worker events
- Help page for notifications
- Test notification & web-push triggers inside app
- Check that the app is installed
### Fixed
- Project issuer display name
## [0.1.7] - 2023.12.19 - 91c6c7c11c71f96006cc876fc946f1f98a274ba2
### Changed
- Icons
### Fixed
- Notification switch now shows message
- Prod/test server warning message at top of page
## [0.1.6] - 2023.12.17 - b445b1234fbfcf6b37d695373f259aab0eda1118
### Added
- Infinite scroll on home page
### Changed
- UI improvements
- Show web-push subscription info
- Icon
## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad ## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad

View File

@@ -22,17 +22,17 @@ npm run lint
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",` If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
* `npx prettier --write ./sw_scripts/` * Update the CHANGELOG.md & the version in package.json, run `npm install`, and commit.
...to make sure the service worker scripts are in proper form * [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
* Update the CHANGELOG.md & the version in package.json, run `npm install`, and commit. Tag wth the new version: `git tag 0.1.0` * If production, change src/constants/app.ts DEFAULT_*_SERVER to be "PROD" and package.json to remove "_Test".
* If production, change src/constants/app.ts DEFAULT_*_SERVER to be PROD.
* `npm run build` * `npm run build`
* Revert src/constants/app.ts & change version to "-beta" * `npx prettier --write ./sw_scripts/`
...to make sure the service worker scripts are in proper form. It's only important if you changed something in that directory.
* `cp sw_scripts/[ns]* dist/` * `cp sw_scripts/[ns]* dist/`
@@ -40,6 +40,8 @@ If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js,
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntu@endorser.ch:time-safari` * `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntu@endorser.ch:time-safari`
* Revert src/constants/app.ts and/or package.json, edit package.json to increment version & add "-beta", `npm install`, and commit.
## Tests ## Tests
@@ -110,10 +112,12 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
### Clear/Reset data & restart ### Clear/Reset data & restart
* 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".) * 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.)
* Unregister service worker (in Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`). * 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`.)
* Clear Cache Storage (in Chrome, in dev tools under Application; in Firefox, in dev tools under Storage). * Clear Cache Storage. (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.)
@@ -136,3 +140,4 @@ Gifts make the world go 'round!
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org * [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439) * [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg) * [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
* Time Safari logo assisted by [DALL-E in ChatGPT](https://chat.openai.com/g/g-2fkFE8rbu-dall-e)

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "TimeSafari", "name": "TimeSafari_Test",
"version": "0.1.6-beta", "version": "0.1.8",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "TimeSafari", "name": "TimeSafari_Test",
"version": "0.1.6-beta", "version": "0.1.8",
"dependencies": { "dependencies": {
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/fontawesome-svg-core": "^6.4.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "TimeSafari", "name": "TimeSafari_Test",
"version": "0.1.6-beta", "version": "0.1.8",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",

View File

@@ -2,35 +2,19 @@
tasks: tasks:
- 08 notifications : - 08 notifications :
- get it to work on Android - .2 after turning on notification, don't wait in push server but wait in client for message test
- get it to work on iOS - insert tooling (exportable logs?) so that we can see problems and troubleshoot as we onboard
- lock down regenerate_vapid endpoint (so only we admins can do it on demand) - if navigator.serviceWorker is null, then tell the user to wait
- make the app behave correctly when App Notifications are turned off - Local install works after cleared out cache in Chrome
- remove sleep in py-push-server app.py?
- write troubleshooting docs for notifications
- make the "App Notifications" toggle on when they turn notifications on
- make the "App Notifications" toggle off when they turn notifications off
- in py-push-server, when sending a push to a subscriber and we get on a 410 "error #106", delete the subscription record
- https://gitea.anomalistdesign.com/trent_larson/py-push-server/pulls/3/files
- remove "notification push server" advanced setting since it only makes sense on the current domain
- prompt user to install on their home screen
- back-and-forth on discovery & project pages led to "You need an identity to load your projects." error on product page when I had an identity - fix maskable icon
- fix the projects on /discover to show the issuer (currently all "Someone Anonymous")
- give users next public key hash
- .3 bug - make or edit a project, choose "Include location", and see the map display shows on top of the bottom icons assignee-group:ui - .3 bug - make or edit a project, choose "Include location", and see the map display shows on top of the bottom icons assignee-group:ui
- .5 Add infinite scroll to gifts on the home page
- .5 If notifications are not enabled, add message to front page with link/button to enable - .5 If notifications are not enabled, add message to front page with link/button to enable
- 01 server - show all claim details when issued by the issuer
- add note after contact addition that they can see your info
- enhance help page instructions for debugging
- add way to test quickly a push notification
- help instructions for PWA install problems (secret failed, must reinstall)
- look at other examples for better UI friend.tech
- show VC details... somehow: - show VC details... somehow:
- 01 show my VCs - most interesting, or via search - 01 show my VCs - most interesting, or via search
- 01 allow download of each VC (& confirmations, to show that they actually own their data) - 01 allow download of each VC (& confirmations, to show that they actually own their data)
@@ -50,10 +34,17 @@ tasks:
- Other features - donation vs give, show offers, show give & outstanding totals, show network view, restrict registration, connect to contacts - Other features - donation vs give, show offers, show give & outstanding totals, show network view, restrict registration, connect to contacts
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- remove 'rowid' references (that are sqlite-specific)
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too) - make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
- 01 make the prod build copy the sw_scripts - 02 watch for the service worker activation before showing the button to turn on notifications
- 01 server - show all claim details when issued by the issuer
- bug - got error adding on Firefox user #0 as contact for themselves
- bug (that is hard to reproduce) - back-and-forth on discovery & project pages led to "You need an identity to load your projects." error on product page when I had an identity
- bug (that is hard to reproduce) - in Chrome, install app then delete app and try from Chrome browser and see log errors "Uncaught TypeError: self.appendDailyLog is not a function"
- bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent
- 01 send visibility signal as a VC and store it - 01 send visibility signal as a VC and store it
- 04 remove 'rowid' references (that are sqlite-specific); may involve server
- 04 look at other examples for better UI friend.tech
- 01 make the prod build copy the sw_scripts
- .5 Add start date to project - .5 Add start date to project
- .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?
@@ -61,12 +52,13 @@ tasks:
- .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 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
- switch some checks for activeDid to check for isRegistered - switch some checks for activeDid to check for isRegistered
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show" - .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) - .5 fix cert generation on server (since it didn't happen automatically for Nov 30)
- warn if they're using the web (android only?)
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps
https://web.dev/articles/get-installed-related-apps
- 04 fix lack of initial notification in Firefox (on MacOS, maybe others) - 04 fix lack of initial notification in Firefox (on MacOS, maybe others)
@@ -89,6 +81,7 @@ tasks:
- 24 Move to Vite - 24 Move to Vite
- 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
- linking between projects or plans : - linking between projects or plans :
- show total time given to & from a project - show total time given to & from a project

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 50 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -281,7 +281,7 @@ interface VapidResponse {
}; };
} }
import { AppString } from "@/constants/app"; import { DEFAULT_PUSH_SERVER } 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";
@@ -302,7 +302,7 @@ export default class App extends Vue {
try { try {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
let pushUrl: string = AppString.DEFAULT_PUSH_SERVER; let pushUrl = DEFAULT_PUSH_SERVER;
if (settings?.webPushServer) { if (settings?.webPushServer) {
pushUrl = settings.webPushServer; pushUrl = settings.webPushServer;
} }
@@ -329,10 +329,7 @@ export default class App extends Vue {
} }
} catch (error) { } catch (error) {
if (window.location.host.startsWith("localhost")) { if (window.location.host.startsWith("localhost")) {
console.log( console.log("Ignoring the error getting VAPID for local development.");
"Ignoring this error getting VAPID for local development:",
error,
);
} else { } else {
console.error("Got an error initializing notifications:", error); console.error("Got an error initializing notifications:", error);
this.$notify( this.$notify(
@@ -373,6 +370,7 @@ export default class App extends Vue {
} }
private askPermission(): Promise<NotificationPermission> { private askPermission(): Promise<NotificationPermission> {
console.log("Requesting permission for notifications:", navigator);
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) { if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
return Promise.reject("Service worker not available."); return Promise.reject("Service worker not available.");
} }
@@ -423,7 +421,7 @@ export default class App extends Vue {
}); });
} }
async turnOnNotifications() { public async turnOnNotifications() {
return this.askPermission() return this.askPermission()
.then((permission) => { .then((permission) => {
console.log("Permission granted:", permission); console.log("Permission granted:", permission);
@@ -445,7 +443,18 @@ export default class App extends Vue {
} }
}) })
.then(() => { .then(() => {
console.log("Subscription data sent to server."); console.log(
"Subscription data sent to server and all finished successfully.",
);
this.$notify(
{
group: "alert",
type: "success",
title: "Notifications Turned On",
text: "Notifications are on. You should see one on your device; if not, see the 'Troubleshoot' page.",
},
-1,
);
}) })
.catch((error) => { .catch((error) => {
console.error( console.error(
@@ -462,9 +471,7 @@ export default class App extends Vue {
"An error occurred setting notification permissions:", "An error occurred setting notification permissions:",
error, error,
); );
alert( alert("Some error occurred setting notification permissions.");
"Some error occurred setting notification permissions. See logs.",
);
}); });
} }
@@ -511,11 +518,7 @@ export default class App extends Vue {
resolve(); resolve();
}) })
.catch((error) => { .catch((error) => {
console.error( console.error("Push subscription failed:", error, options);
"Subscription or server communication failed:",
error,
options,
);
// Inform the user about the issue // Inform the user about the issue
alert( alert(
@@ -531,7 +534,7 @@ export default class App extends Vue {
private sendSubscriptionToServer( private sendSubscriptionToServer(
subscription: PushSubscription, subscription: PushSubscription,
): 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", {
method: "POST", method: "POST",
headers: { headers: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -123,9 +123,7 @@ export default class GiftedDialog extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text: err.message || "There was an error retrieving your settings.",
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
}, },
-1, -1,
); );

View File

@@ -105,9 +105,7 @@ export default class OfferDialog extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text: err.message || "There was an error retrieving your settings.",
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
}, },
-1, -1,
); );

View File

@@ -0,0 +1,58 @@
<template>
<div class="text-center text-red-500">{{ message }}</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { AppString } from "@/constants/app";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component
export default class TopMessage extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@Prop selected = "";
message = "";
async mounted() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
if (
settings?.warnIfTestServer &&
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
this.message = "You're linked to a non-prod server, user " + didPrefix;
} else if (
settings?.warnIfProdServer &&
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
this.message =
"You're linked to the production server, user " + didPrefix;
}
} catch (err: unknown) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Detecting Server",
text: JSON.stringify(err),
},
-1,
);
}
}
}
</script>

View File

@@ -4,21 +4,20 @@
* See also ../libs/veramo/setup.ts * See also ../libs/veramo/setup.ts
*/ */
export enum AppString { export enum AppString {
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,
PROD_PUSH_SERVER = "https://timesafari.app", PROD_PUSH_SERVER = "https://timesafari.app",
TEST1_PUSH_SERVER = "https://test.timesafari.app", TEST1_PUSH_SERVER = "https://test.timesafari.app",
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com", TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
DEFAULT_PUSH_SERVER = TEST1_PUSH_SERVER,
} }
export const DEFAULT_ENDORSER_API_SERVER = AppString.TEST_ENDORSER_API_SERVER;
export const DEFAULT_PUSH_SERVER =
window.location.protocol + "//" + window.location.host;
/** /**
* The possible values for "group" and "type" are in App.vue. * The possible values for "group" and "type" are in App.vue.
* From the notiwind package * From the notiwind package

View File

@@ -1,18 +1,20 @@
import BaseDexie, { Table } from "dexie"; import BaseDexie, { Table } from "dexie";
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon"; import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
import { Account, AccountsSchema } from "./tables/accounts"; import { Account, AccountsSchema } from "./tables/accounts";
import { Contact, ContactsSchema } from "./tables/contacts"; import { Contact, ContactSchema } from "./tables/contacts";
import { Log, LogSchema } from "./tables/logs";
import { import {
MASTER_SETTINGS_KEY, MASTER_SETTINGS_KEY,
Settings, Settings,
SettingsSchema, SettingsSchema,
} from "./tables/settings"; } from "./tables/settings";
import { AppString } from "@/constants/app"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
// Define types for tables that hold sensitive and non-sensitive data // Define types for tables that hold sensitive and non-sensitive data
type SensitiveTables = { accounts: Table<Account> }; type SensitiveTables = { accounts: Table<Account> };
type NonsensitiveTables = { type NonsensitiveTables = {
contacts: Table<Contact>; contacts: Table<Contact>;
logs: Table<Log>;
settings: Table<Settings>; settings: Table<Settings>;
}; };
@@ -26,7 +28,11 @@ export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
const SensitiveSchemas = { ...AccountsSchema }; const SensitiveSchemas = { ...AccountsSchema };
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie; export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
const NonsensitiveSchemas = { ...ContactsSchema, ...SettingsSchema }; const NonsensitiveSchemas = {
...ContactSchema,
...LogSchema,
...SettingsSchema,
};
// Manage the encryption key. If not present in localStorage, create and store it. // Manage the encryption key. If not present in localStorage, create and store it.
const secret = const secret =
@@ -38,15 +44,14 @@ encrypted(accountsDB, { secretKey: secret });
// Define the schema for our databases // Define the schema for our databases
accountsDB.version(1).stores(SensitiveSchemas); accountsDB.version(1).stores(SensitiveSchemas);
db.version(1).stores(NonsensitiveSchemas); // v1 was contacts & settings
// v2 added logs
db.version(2).stores(NonsensitiveSchemas);
// Event handler to initialize the non-sensitive database with default settings // Event handler to initialize the non-sensitive database with default settings
db.on("populate", () => { db.on("populate", () => {
db.settings.add({ db.settings.add({
id: MASTER_SETTINGS_KEY, id: MASTER_SETTINGS_KEY,
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER, apiServer: 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,
}); });
}); });

View File

@@ -6,6 +6,6 @@ export interface Contact {
registered?: boolean; registered?: boolean;
} }
export const ContactsSchema = { export const ContactSchema = {
contacts: "&did, name, publicKeyBase64, registered, seesMe", contacts: "&did, name, publicKeyBase64, registered, seesMe",
}; };

10
src/db/tables/logs.ts Normal file
View File

@@ -0,0 +1,10 @@
export interface Log {
message: string;
}
export const LogSchema = {
// Currently keyed by "date" because A) today's log data is what we need so we append, and
// B) we don't want it to grow so we remove everything if this is the first entry today.
// See safari-notifications.js logMessage for the associated logic.
logs: "date, message",
};

View File

@@ -12,15 +12,17 @@ export type BoundingBox = {
* Settings type encompasses user-specific configuration details. * Settings type encompasses user-specific configuration details.
*/ */
export type Settings = { export type Settings = {
id: number; // Only one entry using MASTER_SETTINGS_KEY id: number; // Only one entry, keyed with MASTER_SETTINGS_KEY
activeDid?: string; // Active Decentralized ID activeDid?: string; // Active Decentralized ID
apiServer?: string; // API server URL apiServer?: string; // API server URL
firstName?: string; // User's first name firstName?: string; // User's first name
lastName?: string; // deprecated - put all names in firstName
lastViewedClaimId?: string; // Last viewed claim ID
lastNotifiedClaimId?: string; // Last notified claim ID
isRegistered?: boolean; isRegistered?: boolean;
webPushServer?: string; // Web Push server URL lastName?: string; // deprecated - put all names in firstName
lastNotifiedClaimId?: string; // Last notified claim ID
lastViewedClaimId?: string; // Last viewed claim ID
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
reminderOn?: boolean; // Toggle to enable or disable reminders
// Array of named search boxes defined by bounding boxes // Array of named search boxes defined by bounding boxes
searchBoxes?: Array<{ searchBoxes?: Array<{
@@ -30,8 +32,9 @@ export type Settings = {
showContactGivesInline?: boolean; // Display contact inline or not showContactGivesInline?: boolean; // Display contact inline or not
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders warnIfProdServer?: boolean; // Warn if using a production server
reminderOn?: boolean; // Toggle to enable or disable reminders warnIfTestServer?: boolean; // Warn if using a testing server
webPushServer?: string; // Web Push server URL
}; };
/** /**

View File

@@ -486,6 +486,10 @@ export interface ProjectData {
* URL referencing information about the project * URL referencing information about the project
**/ **/
handleId: string; handleId: string;
/**
* The DID of the issuer
*/
issuerDid: string;
/** /**
* The Identier of the project * The Identier of the project
**/ **/

View File

@@ -13,6 +13,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { import {
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faArrowUpRightFromSquare,
faBan, faBan,
faBitcoinSign, faBitcoinSign,
faBurst, faBurst,
@@ -65,6 +66,7 @@ import {
library.add( library.add(
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faArrowUpRightFromSquare,
faBan, faBan,
faBitcoinSign, faBitcoinSign,
faBurst, faBurst,

View File

@@ -1,5 +1,7 @@
<template> <template>
<QuickNav selected="Profile"></QuickNav> <QuickNav selected="Profile"></QuickNav>
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
@@ -103,26 +105,10 @@
</router-link> </router-link>
<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
for="toggleNotifications" v-if="!notificationMaybeChanged"
class="flex items-center justify-between cursor-pointer" class="flex items-center justify-between cursor-pointer"
@click=" @click="showNotificationChoice()"
!toggleNotifications
? this.$notify(
{
group: 'modal',
type: 'notification-permission',
},
-1,
)
: this.$notify(
{
group: 'modal',
type: 'notification-off',
},
-1,
)
"
> >
<!-- label --> <!-- label -->
<div>App Notifications</div> <div>App Notifications</div>
@@ -131,8 +117,8 @@
<!-- input --> <!-- input -->
<input <input
type="checkbox" type="checkbox"
v-model="toggleNotifications" v-model="isSubscribed"
name="toggleNotifications" name="toggleNotificationsInput"
class="sr-only" class="sr-only"
/> />
<!-- line --> <!-- line -->
@@ -142,7 +128,14 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition" class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div> ></div>
</div> </div>
</label> </div>
<div v-else>
Notification status may have changed. Revisit this page to see the
latest setting.
</div>
<router-link class="px-4 text-sm text-blue-500" to="/help-notifications">
Test your notification setup.
</router-link>
</div> </div>
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3> <h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
@@ -215,7 +208,7 @@
<div v-if="showAdvanced"> <div v-if="showAdvanced">
<p class="text-rose-600 mb-8"> <p class="text-rose-600 mb-8">
Beware: the features here can be confusing and even change data in ways Beware: the features here can be confusing and even change data in ways
you do not expect. But we support your freedoms! you do not expect. But we support your freedom!
</p> </p>
<!-- Deep Identity Details --> <!-- Deep Identity Details -->
@@ -304,7 +297,7 @@
:to="{ name: 'statistics' }" :to="{ name: 'statistics' }"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
> >
See Global Animated History of Giving See Animated Global History of Giving
</router-link> </router-link>
<!-- id used by puppeteer test script --> <!-- id used by puppeteer test script -->
@@ -316,13 +309,6 @@
Switch Identity Switch Identity
</router-link> </router-link>
<button
@click="alertWebPushSubscription()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Show Subscription from Web Push Server
</button>
<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
@@ -357,6 +343,46 @@
</button> </button>
</div> </div>
<label
for="toggleProdWarningMessage"
class="flex items-center justify-between cursor-pointer my-4"
@click="toggleProdWarning"
>
<!-- label -->
<h2>Show warning if on prod server</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="warnIfProdServer" 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>
</label>
<label
for="toggleTestWarningMessage"
class="flex items-center justify-between cursor-pointer my-4"
@click="toggleTestWarning"
>
<!-- label -->
<h2>Show warning if on non-prod server</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="warnIfTestServer" 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>
</label>
<div class="flex py-4"> <div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2"> <h2 class="text-slate-500 text-sm font-bold mb-2">
Notification Push Server Notification Push Server
@@ -408,6 +434,7 @@ import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@@ -432,7 +459,7 @@ interface IAccount {
derivationPath: string; derivationPath: string;
} }
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav, TopMessage } })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
@@ -444,6 +471,8 @@ export default class AccountViewView extends Vue {
derivationPath = ""; derivationPath = "";
givenName = ""; givenName = "";
isRegistered = false; isRegistered = false;
isSubscribed = false;
notificationMaybeChanged = false;
numAccounts = 0; numAccounts = 0;
publicHex = ""; publicHex = "";
publicBase64 = ""; publicBase64 = "";
@@ -462,13 +491,62 @@ export default class AccountViewView extends Vue {
showAdvanced = false; showAdvanced = false;
subscription: PushSubscription | null = null; subscription: PushSubscription | null = null;
warnIfProdServer = false;
warnIfTestServer = false;
private isSubscribed = false; /**
get toggleNotifications() { * Async function executed when the component is created.
return this.isSubscribed; * Initializes the component's state with values from the database,
* handles identity-related tasks, and checks limitations.
*
* @throws Will display specific messages to the user based on different errors.
*/
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Initialize component state with values from the database or defaults
this.initializeState(settings);
// Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.processIdentity(identity);
}
} catch (err: unknown) {
this.handleError(err);
}
} }
set toggleNotifications(value) {
this.isSubscribed = value; async mounted() {
try {
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription();
this.isSubscribed = !!this.subscription;
} catch (error) {
console.error("Mount error:", error);
}
}
/**
* Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/
initializeState(settings: Settings | undefined) {
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || "";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.showContactGives = !!settings?.showContactGivesInline;
this.warnIfProdServer = !!settings?.warnIfProdServer;
this.warnIfTestServer = !!settings?.warnIfTestServer;
this.webPushServer = (settings?.webPushServer as string) || "";
this.webPushServerInput = (settings?.webPushServer as string) || "";
} }
public async getIdentity(activeDid: string): Promise<IIdentifier | null> { public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
@@ -536,6 +614,16 @@ export default class AccountViewView extends Vue {
this.updateShowContactAmounts(); this.updateShowContactAmounts();
} }
toggleProdWarning() {
this.warnIfProdServer = !this.warnIfProdServer;
this.updateWarnIfProdServer(this.warnIfProdServer);
}
toggleTestWarning() {
this.warnIfTestServer = !this.warnIfTestServer;
this.updateWarnIfTestServer(this.warnIfTestServer);
}
readableTime(timeStr: string) { readableTime(timeStr: string) {
return timeStr.substring(0, timeStr.indexOf("T")); return timeStr.substring(0, timeStr.indexOf("T"));
} }
@@ -545,60 +633,6 @@ export default class AccountViewView extends Vue {
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
/**
* Async function executed when the component is created.
* Initializes the component's state with values from the database,
* handles identity-related tasks, and checks limitations.
*
* @throws Will display specific messages to the user based on different errors.
*/
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Initialize component state with values from the database or defaults
this.initializeState(settings);
// Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.processIdentity(identity);
}
} catch (err: unknown) {
this.handleError(err);
}
}
async mounted() {
try {
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription();
this.toggleNotifications = !!this.subscription;
} catch (error) {
console.error("Mount error:", error);
this.toggleNotifications = false;
}
}
/**
* Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/
initializeState(settings: Settings | undefined) {
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || "";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.webPushServer = (settings?.webPushServer as string) || "";
this.webPushServerInput = (settings?.webPushServer as string) || "";
this.showContactGives = !!settings?.showContactGivesInline;
}
/** /**
* Processes the identity and updates the component's state. * Processes the identity and updates the component's state.
* @param {IdentityType} identity - Object containing identity information. * @param {IdentityType} identity - Object containing identity information.
@@ -623,6 +657,31 @@ export default class AccountViewView extends Vue {
} }
} }
async showNotificationChoice() {
if (!this.subscription) {
this.$notify(
{
group: "modal",
type: "notification-permission",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
-1,
);
} else {
this.$notify(
{
group: "modal",
type: "notification-off",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
-1,
);
}
this.notificationMaybeChanged = true;
}
/** /**
* Handles errors and updates the component's state accordingly. * Handles errors and updates the component's state accordingly.
* @param {Error} err - The error object. * @param {Error} err - The error object.
@@ -661,12 +720,58 @@ export default class AccountViewView extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error Updating Contact Setting", title: "Error Updating Contact Setting",
text: "Clear your cache and start over (after data backup).", text: "The setting may not have saved. Try again, maybe after restarting the app.",
}, },
-1, -1,
); );
console.error( console.error(
"Telling user to clear cache after contact setting update because:", "Telling user to try again after contact setting update because:",
err,
);
}
}
public async updateWarnIfProdServer(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
warnIfProdServer: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Prod Warning",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after setting update because:",
err,
);
}
}
public async updateWarnIfTestServer(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Test Warning",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after setting update because:",
err, err,
); );
} }
@@ -939,13 +1044,5 @@ export default class AccountViewView extends Vue {
-1, -1,
); );
} }
alertWebPushSubscription() {
console.log(
"Web push subscription:",
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging
);
alert(JSON.stringify(this.subscription));
}
} }
</script> </script>

View File

@@ -19,9 +19,9 @@
<!-- Details --> <!-- Details -->
<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">
<div> <div>
<div class="block pb-4 flex gap-4 overflow-hidden"> <div class="block flex gap-4 overflow-hidden">
<div class="overflow-hidden"> <div class="overflow-hidden">
<h2 class="text-xl">{{ veriClaim.id }}</h2> <h2 class="text-md font-bold">{{ veriClaim.id }}</h2>
<div class="text-sm"> <div class="text-sm">
<div> <div>
{{ veriClaim.claimType }} {{ veriClaim.claimType }}
@@ -45,7 +45,7 @@
</div> </div>
<div> <div>
<h2 class="font-bold text-2xl">Confirmations</h2> <h2 class="font-bold uppercase text-xl mt-8 mb-2">Confirmations</h2>
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span> <span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
<span v-else-if="totalConfirmers() === 1"> <span v-else-if="totalConfirmers() === 1">
@@ -136,22 +136,24 @@
</div> </div>
<div> <div>
<h2 class="font-bold text-2xl mt-8">Claim</h2> <h2 class="font-bold uppercase text-xl mt-8 mb-2">Claim</h2>
<pre>{{ util.inspect(veriClaim, false, null) }}</pre> <pre class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md">
{{ util.inspect(veriClaim, false, null) }}
</pre>
</div> </div>
<h2 class="font-bold text-2xl mt-8">Full Claim</h2> <h2 class="font-bold uppercase text-xl mt-8 mb-2">Full Claim</h2>
<p> <p class="mb-4">
The full claim includes the claim as it was originally issued, including The full claim includes the claim as it was originally issued, including
the signature (ie. the proof of issuance by that person). the signature (ie. the proof of issuance by that person).
</p> </p>
<div v-if="!fullClaim"> <div v-if="!fullClaim">
<div v-if="fullClaimMessage"> <p v-if="fullClaimMessage" class="mb-4">
{{ fullClaimMessage }} {{ fullClaimMessage }}
</div> </p>
<button <button
v-else v-else
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4" class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
@click="showFullClaim(veriClaim.id)" @click="showFullClaim(veriClaim.id)"
> >
Load Full Claim Details Load Full Claim Details
@@ -161,10 +163,12 @@
<pre>{{ util.inspect(fullClaim, false, null) }}</pre> <pre>{{ util.inspect(fullClaim, false, null) }}</pre>
</div> </div>
<a :href="apiServer + '/api/claim/' + veriClaim.id" target="_blank"> <a
<button class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4"> :href="apiServer + '/api/claim/' + veriClaim.id"
View on the Public Server target="_blank"
</button> class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
>
View on the Public Server
</a> </a>
</section> </section>
</template> </template>

View File

@@ -25,6 +25,13 @@
<span class="justify-around">(Only 50 most recent)</span> <span class="justify-around">(Only 50 most recent)</span>
<span /> <span />
</div> </div>
<div class="flex justify-around">
<span />
<span class="justify-around">
(This does not include claims by them if they're not visible to you.)
</span>
<span />
</div>
<!-- Results List --> <!-- Results List -->
<table <table
@@ -178,6 +185,7 @@ export default class ContactAmountssView extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings or gives.", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -185,7 +193,7 @@ export default class ContactAmountssView extends Vue {
title: "Error", title: "Error",
text: text:
err.userMessage || err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.", "There was an error retrieving your settings and/or contacts and/or gives.",
}, },
-1, -1,
); );

View File

@@ -145,6 +145,7 @@ export default class ContactGiftingView extends Vue {
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings & contacts:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -152,7 +153,7 @@ export default class ContactGiftingView extends Vue {
title: "Error", title: "Error",
text: text:
err.message || err.message ||
"There was an error retrieving the latest sweet, sweet action.", "There was an error retrieving your settings and/or contacts.",
}, },
-1, -1,
); );

View File

@@ -908,13 +908,13 @@ export default class ContactsView extends Vue {
}, },
-1, -1,
); );
} else if (!parseFloat(this.hourInput)) { } else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Input Error", title: "Input Error",
text: "Giving 0 hours does nothing.", text: "Giving no hours or descrption does nothing.",
}, },
-1, -1,
); );

View File

@@ -1,5 +1,6 @@
<template> <template>
<QuickNav selected="Discover"></QuickNav> <QuickNav selected="Discover"></QuickNav>
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
@@ -136,6 +137,7 @@ import { didInfo, ProjectData } from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import TopMessage from "@/components/TopMessage.vue";
interface Notification { interface Notification {
group: string; group: string;
@@ -149,6 +151,7 @@ interface Notification {
QuickNav, QuickNav,
InfiniteScroll, InfiniteScroll,
EntityIcon, EntityIcon,
TopMessage,
}, },
}) })
export default class DiscoverView extends Vue { export default class DiscoverView extends Vue {
@@ -274,8 +277,8 @@ export default class DiscoverView extends Vue {
const plans: ProjectData[] = results.data; const plans: ProjectData[] = results.data;
if (plans) { if (plans) {
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId, rowid } = plan; const { name, description, handleId, issuerDid, rowid } = plan;
this.projects.push({ name, description, handleId, rowid }); this.projects.push({ name, description, handleId, issuerDid, rowid });
} }
this.remoteCount = this.projects.length; this.remoteCount = this.projects.length;
} else { } else {
@@ -357,8 +360,14 @@ export default class DiscoverView extends Vue {
if (beforeId) { if (beforeId) {
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, issuerDid, rowid } = plan;
this.projects.push({ name, description, handleId, rowid }); this.projects.push({
name,
description,
handleId,
issuerDid,
rowid,
});
} }
} else { } else {
this.projects = results.data; this.projects = results.data;

View File

@@ -22,15 +22,104 @@
</div> </div>
<div> <div>
<p>Here are things to try to get notifications working.</p> <p>Here are ways to test notifications and get them working.</p>
<h2 class="text-xl font-semibold">Test</h2> <h2 class="text-xl font-semibold">Full Test</h2>
<p>Somehow call the service-worker self.showNotification</p>
<h2 class="text-xl font-semibold">Check OS-level permissions</h2>
<div> <div>
Walk-throughs & screenshots, maybe for all combinations of OS & <p>
browsers. If this works then you're all set.
<button
@click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Yourself a Test Web Push Message (Through Push Server but
Skipping Client Filter)
</button>
</p>
</div>
<h2 class="text-xl font-semibold">If this app is not installed...</h2>
<div>
<p>
For best results on mobile, install this app on your device (as
opposed to using it inside the broser app). In Chrome, it may prompt
you, and you can also look for the "Install" command in the browser
settings; on the the deskop, look for this icon in the address bar:
<img
src="../assets/help/chrome-install-pwa.png"
alt="Chrome 'install' icon"
class="ml-4"
/>
</p>
</div>
<h2 class="text-xl font-semibold">
If "you must enable notifications"...
</h2>
<div>
<p>
Wait for about 10 seconds (for the service worker to activate), then
<button class="text-blue-500" @click="showNotificationChoice()">
click here.
</button>
</p>
</div>
<h2 class="text-xl font-semibold">Check App Permissions</h2>
<div>
<p>
In Apple iOS, check "Settings" -> "Notifications", look for the Time
Safari app (or the browser you're using), and make sure notifications
are enabled.
</p>
<p>
In Android, hold on to the app icon, then select "App Info", then
"Notifications" and make sure they're enabled. If it's still a problem
then go further:
</p>
<p>
If you installed the app with Chrome, make sure there are no other
tabs with it open. Here are some ways to clear caches that can mess
things up (and note that this clears out data from the installed app
-- which is good to do while the app is installed):
</p>
<ul>
<li class="list-disc ml-4">
Go to Chrome "App Info", then "Storage & Cache" and "Clear Storage".
</li>
<li class="list-disc ml-4">
Go to Chrome "Settings", then "Privacy and Security" and "Clear
"Clear browsing data", then "Cookies and site data". Also make sure
the "Time Range" at the top shows "All time".
</li>
</ul>
<p>
On a Mac, go to "Settings" and check "Notifications".
<img
src="../assets/help/mac-installed-app-settings.png"
alt="Mac app settings"
class="ml-4"
/>
</p>
</div>
<h2 class="text-xl font-semibold">Check Browser Permissions</h2>
<div>
<p>In Apple iOS, check Settings -> Notifications.</p>
<p>In Android, check Settings -> Notifications.</p>
You can find more details about compatibility
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility"
class="text-blue-500"
target="_blank"
>
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div>
<h2 class="text-xl font-semibold">Check OS Permissions</h2>
<div class="px-2">
<div> <div>
<h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3> <h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3>
<div> <div>
@@ -39,39 +128,113 @@
</div> </div>
<h3 class="text-lg font-semibold">Mobile Phone - Google Android</h3> <h3 class="text-lg font-semibold">Mobile Phone - Google Android</h3>
<div>See the browser section.</div> <div>
We recommend Chrome. It must be version 42 or higher. Check your
version under Settings -> About Chrome.
</div>
<h3 class="text-lg font-semibold">Desktop - Mac</h3> <h3 class="text-lg font-semibold">Desktop - Mac</h3>
<div>Requires Mac OS 13.</div> <div>
</div> <span>
</div> Requires Mac OS 13; see your macOS version under Apple -> About
This Mac.
</span>
</div>
<h2 class="text-xl font-semibold">Check browser-level permissions</h2> <h3 class="text-lg font-semibold">Windows desktop</h3>
<p>Walk-throughs & screenshots for browser settings</p> In Windows, check Settings -> Notifications.
<div> <img
src="../assets/help/windows-system-enable-notifications.png"
alt="Windows system settings"
class="ml-4"
/>
</div>
<div> <div>
<h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3> You can find more details about compatibility
<div>Make sure your OS (above) supports it.</div> <a
href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility"
<h3 class="text-lg font-semibold">Mobile Phone - Android</h3> class="text-blue-500"
<div>Chrome requires version 50.</div> target="_blank"
>
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div> </div>
</div> </div>
<h2 class="text-xl font-semibold">Explain full reset to start again</h2> <h2 class="text-xl font-semibold">Reinstall</h2>
<p> <div>
Walk-throughs for clearing everything & subscribing anew to get a <p>
message If all else fails, uninstall the app, ensure all the browser tabs with
</p> it are closed, and clear out caches and storage.
</p>
<ul class="ml-4 list-disc">
<li>
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.)
</li>
<li>
Clear notification permission. (in Chrome, go to
`chrome://settings/content/notifications`; in Firefox, go to
`about:preferences` and search for "notifications".)
</li>
<li>
Unregister service worker. (in Chrome, go to
`chrome://serviceworker-internals/`; in Firefox, go to
`about:serviceworkers`.)
</li>
<li>
Clear Cache Storage. (in Chrome, in dev tools under Application; in
Firefox, in dev tools under Storage.)
</li>
</ul>
<p>Then reinstall the app.</p>
</div>
<h2 class="text-xl font-semibold">Auto-detection</h2> <button
<p>Show results of auto-detection whether they're turned on</p> @click="showTestNotification()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Test Notification Directly to Device (Not Through Push Server)
</button>
<button
@click="alertWebPushSubscription()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Show Web Push Subscription Info
</button>
<button
@click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Yourself a Test Web Push Message (Through Push Server but Skipping
Client Filter)
</button>
<button
@click="sendTestWebPushMessage()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Yourself a Test Web Push Message (Through Push Server and Client
Filter)
</button>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
interface Notification { interface Notification {
group: string; group: string;
@@ -83,5 +246,156 @@ interface Notification {
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class HelpNotificationsView extends Vue { export default class HelpNotificationsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
subscription: PushSubscription | null = null;
async mounted() {
try {
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription();
} catch (error) {
console.error("Mount error:", error);
}
}
alertWebPushSubscription() {
console.log(
"Web push subscription:",
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging
);
alert(JSON.stringify(this.subscription));
}
async sendTestWebPushMessage(skipFilter: boolean = false) {
if (!this.subscription) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not Subscribed",
text: "You must enable notifications before testing the web push.",
},
-1,
);
return;
}
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
if (settings?.webPushServer) {
pushUrl = settings.webPushServer;
}
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
// 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 auth = Buffer.from(this.subscription.getKey("auth"));
const authB64 = auth
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const p256dh = Buffer.from(this.subscription.getKey("p256dh"));
const p256dhB64 = p256dh
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const newPayload = {
endpoint: this.subscription.endpoint,
keys: {
auth: authB64,
p256dh: p256dhB64,
},
message: `Test, where you will see this message ${
skipFilter ? "un" : ""
}filtered.`,
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
};
console.log("Sending a test web push message:", newPayload);
const payloadStr = JSON.stringify(newPayload);
const response = await axios.post(
pushUrl + "/web-push/send-test",
payloadStr,
{
headers: {
"Content-Type": "application/json",
},
},
);
console.log("Got response from web push server:", response);
this.$notify(
{
group: "alert",
type: "success",
title: "Test Web Push Sent",
text: "Check your device for the test web push message, depending on the filtering you chose.",
},
-1,
);
} catch (error) {
console.error("Got an error sending test notification:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Sending Test",
text: "Got an error sending the test web push notification.",
},
-1,
);
}
}
showTestNotification() {
const TEST_NOTIFICATION_TITLE = "It Worked";
navigator.serviceWorker.ready
.then((registration) => {
return registration.showNotification(TEST_NOTIFICATION_TITLE, {
body: "This is your test notification.",
});
})
.then(() => {
this.$notify(
{
group: "alert",
type: "success",
title: "Sent",
text: `A notification was triggered, so one should show on your device entitled '${TEST_NOTIFICATION_TITLE}'.`,
},
5000,
);
})
.catch((error) => {
console.error("Got a notification error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Failed",
text: "Got an error sending a notification.",
},
-1,
);
});
}
showNotificationChoice() {
this.$notify(
{
group: "modal",
type: "notification-permission",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
-1,
);
}
} }
</script> </script>

View File

@@ -30,16 +30,17 @@
<h2 class="text-xl font-semibold">What is the philosophy here?</h2> <h2 class="text-xl font-semibold">What is the philosophy here?</h2>
<p> <p>
We are building networks of people who want to grow a giving society. We are building networks of people who want to grow a giving society.
First of all, you can record ways you've seen people give, and that First of all, you can see what people have given, and also recognize
leaves a permanent record -- one that came from you, and the recipient gifts you've seen, in a way that leaves a permanent record -- one that
can prove it was for them. This is personally gratifying, but it extends came from you, and the recipient can prove it was for them. This is
to broader work: volunteers can get confirmation of activity and personally gratifying, but it extends to broader work: volunteers get
selectively show off their contributions and network. confirmation of activity, and selectively show off their contributions
and network.
</p> </p>
<p> <p>
You can also record projects and plans and invite others to collaborate. You can show giving and also offer help to ideas, based on others'
Soon you'll be able to see when others are interested and see how much willingness to help out, too. You can record your own ideas and invite
they're willing to contribute, even if there are conditions. 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
@@ -181,6 +182,15 @@
different page. different page.
</p> </p>
<h2 class="text-xl font-semibold">
Where do I get help with notifications?
</h2>
<p>
<router-link class="text-blue-500" to="/help-notifications"
>Here.</router-link
>
</p>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I access even more functionality? How do I access even more functionality?
</h2> </h2>

View File

@@ -1,5 +1,7 @@
<template> <template>
<QuickNav selected="Home"></QuickNav> <QuickNav selected="Home"></QuickNav>
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
@@ -8,6 +10,21 @@
<!-- show the actions for recognizing a give --> <!-- show the actions for recognizing a give -->
<div class="mb-8"> <div class="mb-8">
<div
v-if="!isInstalled()"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<p>
You should install this as an app.
<router-link
:to="{ name: 'help-notifications' }"
class="text-blue-500"
>
Go here for instructions.
</router-link>
</p>
</div>
<div <div
v-if="!activeDid" v-if="!activeDid"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4" class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
@@ -146,6 +163,7 @@ import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.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 { 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";
@@ -166,7 +184,13 @@ interface Notification {
} }
@Component({ @Component({
components: { GiftedDialog, QuickNav, EntityIcon, InfiniteScroll }, components: {
GiftedDialog,
QuickNav,
EntityIcon,
InfiniteScroll,
TopMessage,
},
}) })
export default class HomeView extends Vue { export default class HomeView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
@@ -225,6 +249,7 @@ export default class HomeView extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings and/or feed.", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -232,13 +257,25 @@ export default class HomeView extends Vue {
title: "Error", title: "Error",
text: text:
err.userMessage || err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.", "There was an error retrieving your settings and/or the latest activity.",
}, },
-1, -1,
); );
} }
} }
// from https://benborgers.com/posts/pwa-detect-installed
isInstalled() {
// For iOS
if ("standalone" in window.navigator) return true;
// For Android
if (window.matchMedia("(display-mode: standalone)").matches) return true;
// If neither is true, it's not installed
return false;
}
public async buildHeaders() { public async buildHeaders() {
const headers: HeadersInit = { const headers: HeadersInit = {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@@ -1,5 +1,7 @@
<template> <template>
<QuickNav /> <QuickNav />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Breadcrumb --> <!-- Breadcrumb -->
@@ -46,6 +48,7 @@
target="_blank" target="_blank"
class="underline" class="underline"
>Map View >Map View
<fa icon="arrow-up-right-from-square" class="fa-fw" />
</a> </a>
</div> </div>
<div v-if="url"> <div v-if="url">
@@ -282,6 +285,7 @@ import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue"; import OfferDialog from "@/components/OfferDialog.vue";
import TopMessage from "@/components/TopMessage.vue";
import { accountsDB, db } from "@/db/index"; 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";
@@ -306,7 +310,7 @@ interface Notification {
} }
@Component({ @Component({
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav }, components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav, TopMessage },
}) })
export default class ProjectViewView extends Vue { export default class ProjectViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;

View File

@@ -1,5 +1,7 @@
<template> <template>
<QuickNav selected="Projects"></QuickNav> <QuickNav selected="Projects"></QuickNav>
<TopMessage />
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
@@ -79,6 +81,7 @@ import { IIdentifier } from "@veramo/core";
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 EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import TopMessage from "@/components/TopMessage.vue";
import { ProjectData } from "@/libs/endorserServer"; import { ProjectData } from "@/libs/endorserServer";
interface Notification { interface Notification {
@@ -89,7 +92,7 @@ interface Notification {
} }
@Component({ @Component({
components: { InfiniteScroll, QuickNav, EntityIcon }, components: { InfiniteScroll, QuickNav, EntityIcon, TopMessage },
}) })
export default class ProjectsView extends Vue { export default class ProjectsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
@@ -122,8 +125,8 @@ export default class ProjectsView extends Vue {
if (resp.status === 200 || !resp.data.data) { if (resp.status === 200 || !resp.data.data) {
const plans: ProjectData[] = resp.data.data; const plans: ProjectData[] = resp.data.data;
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId, rowid } = plan; const { name, description, handleId, issuerDid, rowid } = plan;
this.projects.push({ name, description, handleId, rowid }); this.projects.push({ name, description, handleId, issuerDid, rowid });
} }
} else { } else {
console.log("Bad server response & data:", resp.status, resp.data); console.log("Bad server response & data:", resp.status, resp.data);

View File

@@ -4,62 +4,121 @@ importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js", "https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
); );
self.addEventListener("install", (event) => { function logConsoleAndDb(message, arg1, arg2) {
console.log("Adding event listener for:", event); // in chrome://serviceworker-internals note that the arg1 and arg2 here will show as "[object Object]" in that page but will show as expandable objects in the console
importScripts( console.log(`${new Date().toISOString()} ${message}`, arg1, arg2);
if (self.appendDailyLog) {
let fullMessage = `${new Date().toISOString()} ${message}`;
if (arg1) {
fullMessage += `\n${JSON.stringify(arg1)}`;
}
if (arg2) {
fullMessage += `\n${JSON.stringify(arg2)}`;
}
self.appendDailyLog(fullMessage);
} else {
// sometimes we get the error: "Uncaught TypeError: self.appendDailyLog is not a function"
console.log("Not logging to DB because self.appendDailyLog doesn't exist.");
}
}
self.addEventListener("install", async (event) => {
console.log("Service worker got install event. Importing scripts...", event);
await importScripts(
"safari-notifications.js", "safari-notifications.js",
"nacl.js", "nacl.js",
"noble-curves.js", "noble-curves.js",
"noble-hashes.js", "noble-hashes.js",
); );
// this should now be available
logConsoleAndDb("Service worker imported all scripts.");
});
self.addEventListener("activate", (event) => {
logConsoleAndDb("Service worker is activating...", event);
// see https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
// and https://web.dev/articles/service-worker-lifecycle#clientsclaim
event.waitUntil(clients.claim());
logConsoleAndDb("Service worker is activated.");
}); });
self.addEventListener("push", function (event) { self.addEventListener("push", function (event) {
let text = null;
if (event.data) {
text = event.data.text();
}
logConsoleAndDb("Service worker received a push event.", text, event);
event.waitUntil( event.waitUntil(
(async () => { (async () => {
try { try {
let payload; let payload;
if (event.data) { if (text) {
payload = JSON.parse(event.data.text()); try {
payload = JSON.parse(text);
} catch (e) {
// don't use payload since it is not JSON
}
}
// This is a special value that tells the service worker to trigger its daily check.
// See https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/c1ed026662e754348a5f91542680bd4f57e5b81e/app.py#L217
const DAILY_UPDATE_TITLE = "DAILY_CHECK";
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// This is shared with the notification-test code and should be a constant. Look for the same name in HelpNotificationsView.vue
// Make sure it is something other than the DAILY_UPDATE_TITLE.
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
let title;
let message = "Got some empty message.";
if (payload && payload.title == DIRECT_PUSH_TITLE) {
// skip any search logic and show the message directly
title = "Direct Notification";
message = payload.message || "No details were provided.";
} else {
// any other title will run through regular filtering logic
if (payload && payload.title === DAILY_UPDATE_TITLE) {
title = "Daily Update";
} else {
title = payload.title || "Update";
}
message = await self.getNotificationCount();
} }
const message = await self.getNotificationCount();
if (message) { if (message) {
console.log("Will notify user:", message);
const title = payload ? payload.title : "Custom Title";
const options = { const options = {
body: message, body: message,
icon: payload ? payload.icon : "icon.png", icon: payload ? payload.icon : "icon.png",
badge: payload ? payload.badge : "badge.png", badge: payload ? payload.badge : "badge.png",
}; };
await self.registration.showNotification(title, options); await self.registration.showNotification(title, options);
logConsoleAndDb("Notified user:", options);
} else { } else {
console.log("No notification message, so will not tell the user."); logConsoleAndDb("No notification message.");
} }
} catch (error) { } catch (error) {
console.error("Error processing the push event:", error); logConsoleAndDb("Error with push event", event, error);
} }
})(), })(),
); );
}); });
self.addEventListener("message", (event) => { self.addEventListener("message", (event) => {
logConsoleAndDb("Service worker got a message...", event);
if (event.data && event.data.type === "SEND_LOCAL_DATA") { if (event.data && event.data.type === "SEND_LOCAL_DATA") {
self.secret = event.data.data; self.secret = event.data.data;
event.ports[0].postMessage({ success: true }); event.ports[0].postMessage({ success: true });
} }
}); logConsoleAndDb("Service worker posted a message.");
self.addEventListener("activate", (event) => {
event.waitUntil(clients.claim());
console.log("Service worker activated", event);
}); });
self.addEventListener("fetch", (event) => { self.addEventListener("fetch", (event) => {
console.log("Got fetch event", event.request); logConsoleAndDb("Service worker got fetch event.", event);
}); });
self.addEventListener("error", (event) => { self.addEventListener("error", (event) => {
console.error("Error in Service Worker:", event.message); logConsoleAndDb("Service worker error", event);
console.error("Full Error:", event);
console.error("Message:", event.message);
console.error("File:", event.filename); console.error("File:", event.filename);
console.error("Line:", event.lineno); console.error("Line:", event.lineno);
console.error("Column:", event.colno); console.error("Column:", event.colno);

View File

@@ -395,12 +395,42 @@ async function setMostRecentNotified(id) {
data["lastNotifiedClaimId"] = id; data["lastNotifiedClaimId"] = id;
await updateRecord(store, data); await updateRecord(store, data);
} else { } else {
console.error("IndexedDB settings record not found."); console.error(
"safari-notifications setMostRecentNotified IndexedDB settings record not found",
);
} }
transaction.oncomplete = () => db.close(); transaction.oncomplete = () => db.close();
} catch (error) { } catch (error) {
console.error("IndexedDB error:", error); console.error(
"safari-notifications setMostRecentNotified IndexedDB error",
error,
);
}
}
async function appendDailyLog(message) {
try {
const db = await openIndexedDB("TimeSafari");
const transaction = db.transaction("logs", "readwrite");
const store = transaction.objectStore("logs");
// will only keep one day's worth of logs
const todayKey = new Date().toDateString();
const previous = await getRecord(store, todayKey);
if (!previous) {
await store.clear(); // clear out anything older than today
}
let fullMessage = (previous && previous.message) || "";
if (fullMessage) {
fullMessage += "\n";
}
fullMessage += message;
await updateRecord(store, { date: todayKey, message: fullMessage });
transaction.oncomplete = () => db.close();
return true;
} catch (error) {
console.error("safari-notifications logMessage IndexedDB error", error);
return false;
} }
} }
@@ -420,6 +450,7 @@ function getRecord(store, key) {
}); });
} }
// Note that this assumes there is only one record in the store.
function updateRecord(store, data) { function updateRecord(store, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = store.put(data); const request = store.put(data);
@@ -430,20 +461,23 @@ function updateRecord(store, data) {
async function fetchAllAccounts() { async function fetchAllAccounts() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let openRequest = indexedDB.open("TimeSafariAccounts"); const openRequest = indexedDB.open("TimeSafariAccounts");
openRequest.onupgradeneeded = function (event) { openRequest.onupgradeneeded = function (event) {
let db = event.target.result; const db = event.target.result;
if (!db.objectStoreNames.contains("accounts")) { if (!db.objectStoreNames.contains("accounts")) {
db.createObjectStore("accounts", { keyPath: "id" }); db.createObjectStore("accounts", { keyPath: "id" });
} }
if (!db.objectStoreNames.contains("worker_log")) {
db.createObjectStore("worker_log");
}
}; };
openRequest.onsuccess = function (event) { openRequest.onsuccess = function (event) {
let db = event.target.result; const db = event.target.result;
let transaction = db.transaction("accounts", "readonly"); const transaction = db.transaction("accounts", "readonly");
let objectStore = transaction.objectStore("accounts"); const objectStore = transaction.objectStore("accounts");
let getAllRequest = objectStore.getAll(); const getAllRequest = objectStore.getAll();
getAllRequest.onsuccess = function () { getAllRequest.onsuccess = function () {
resolve(getAllRequest.result); resolve(getAllRequest.result);
@@ -460,78 +494,77 @@ async function fetchAllAccounts() {
} }
async function getNotificationCount() { async function getNotificationCount() {
let secret = null;
let accounts = []; let accounts = [];
let result = null; let result = null;
if ("secret" in self) { // 1 is our master settings ID; see MASTER_SETTINGS_KEY
secret = self.secret; const settings = await getSettingById(1);
const secretUint8Array = self.decodeBase64(secret); let lastNotifiedClaimId = null;
// 1 is our master settings ID; see MASTER_SETTINGS_KEY if ("lastNotifiedClaimId" in settings) {
const settings = await getSettingById(1); lastNotifiedClaimId = settings["lastNotifiedClaimId"];
let lastNotifiedClaimId = null; }
if ("lastNotifiedClaimId" in settings) { const activeDid = settings["activeDid"];
lastNotifiedClaimId = settings["lastNotifiedClaimId"]; accounts = await fetchAllAccounts();
} let activeAccount = null;
const activeDid = settings["activeDid"]; for (let i = 0; i < accounts.length; i++) {
accounts = await fetchAllAccounts(); if (accounts[i]["did"] == activeDid) {
let did = null; activeAccount = accounts[i];
for (var i = 0; i < accounts.length; i++) { break;
let account = accounts[i];
let did = account["did"];
if (did == activeDid) {
let publicKeyHex = account["publicKeyHex"];
let identity = account["identity"];
const messageWithNonceAsUint8Array = self.decodeBase64(identity);
const nonce = messageWithNonceAsUint8Array.slice(0, 24);
const message = messageWithNonceAsUint8Array.slice(24, identity.length);
const decoder = new TextDecoder("utf-8");
const decrypted = self.secretbox.open(message, nonce, secretUint8Array);
const msg = decoder.decode(decrypted);
const identifier = JSON.parse(JSON.parse(msg));
const headers = {
"Content-Type": "application/json",
};
headers["Authorization"] = "Bearer " + (await accessToken(identifier));
let response = await fetch(
settings["apiServer"] + "/api/v2/report/claims",
{
method: "GET",
headers: headers,
},
);
if (response.status == 200) {
let json = await response.json();
let claims = json["data"];
let newClaims = 0;
for (var i = 0; i < claims.length; i++) {
let claim = claims[i];
if (claim["id"] === lastNotifiedClaimId) {
break;
}
newClaims++;
}
if (newClaims > 0) {
result = `There are ${newClaims} new activities on TimeSafari`;
}
const most_recent_notified = claims[0]["id"];
await setMostRecentNotified(most_recent_notified);
} else {
console.error(
"The service worker got a bad response status when fetching claims:",
response.status,
response,
);
}
break;
}
} }
} }
const headers = {
"Content-Type": "application/json",
};
const identity = activeAccount && activeAccount["identity"];
if (identity && "secret" in self) {
const secret = self.secret;
const secretUint8Array = self.decodeBase64(secret);
const messageWithNonceAsUint8Array = self.decodeBase64(identity);
const nonce = messageWithNonceAsUint8Array.slice(0, 24);
const message = messageWithNonceAsUint8Array.slice(24, identity.length);
const decoder = new TextDecoder("utf-8");
const decrypted = self.secretbox.open(message, nonce, secretUint8Array);
const msg = decoder.decode(decrypted);
const identifier = JSON.parse(JSON.parse(msg));
headers["Authorization"] = "Bearer " + (await accessToken(identifier));
}
const response = await fetch(
settings["apiServer"] + "/api/v2/report/claims",
{
method: "GET",
headers: headers,
},
);
if (response.status == 200) {
const json = await response.json();
const claims = json["data"];
let newClaims = 0;
for (let i = 0; i < claims.length; i++) {
const claim = claims[i];
if (claim["id"] === lastNotifiedClaimId) {
break;
}
newClaims++;
}
if (newClaims > 0) {
result = `There are ${newClaims} new activities on Time Safari`;
}
const most_recent_notified = claims[0]["id"];
await setMostRecentNotified(most_recent_notified);
} else {
console.error(
"safari-notifications getNotificationsCount got a bad response status when fetching claims",
response.status,
response,
);
}
return result; return result;
} }
self.appendDailyLog = appendDailyLog;
self.getNotificationCount = getNotificationCount; self.getNotificationCount = getNotificationCount;
self.decodeBase64 = decodeBase64; self.decodeBase64 = decodeBase64;