Compare commits
29 Commits
0.3.57
...
sw-cleanup
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f0d10d93d | |||
| 6045975b79 | |||
| a6bb036ceb | |||
| 1e2ad85547 | |||
| 4daffe8f40 | |||
| efb1922826 | |||
| c6e10bfdad | |||
| bb122be319 | |||
| 3f436476a2 | |||
| a77d20b572 | |||
| 69a25ddd6c | |||
| a12d7fcc1b | |||
| 69c60e5426 | |||
| 4806acc30e | |||
| 1127d7079b | |||
| 0bbadfec6d | |||
| 276d8b2f19 | |||
| a7fbbbd4cd | |||
| a8d362c14d | |||
| ce5933f645 | |||
| 5cbf917ada | |||
| 7335412145 | |||
| feea1a1d3b | |||
| 7f4d31a79c | |||
| 4041a7d08e | |||
| 681d949098 | |||
| 3bf8fd0c22 | |||
| ee6a344daf | |||
| 65a5edf26b |
@@ -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
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.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +1,29 @@
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
- .1 Make give description text box into something that expands as they type?
|
- show VC details... somehow:
|
||||||
- 04 allow user to download claims, mine + ones I can see about me from others
|
- .5 make a VC details page, or link to endorser.ch (including confirmations)
|
||||||
- .5 customize favicon assignee-group:ui
|
- 01 allow download of each VC (& confirmations, to show that they actually own their data)
|
||||||
- .2 Show a warning if both giver and recipient are the same (but still allow?)
|
- 04 allow user to download VCs, mine + ones I can see about me from others
|
||||||
- 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
|
- add VC confirmation?
|
||||||
- .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 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
|
|
||||||
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
|
|
||||||
- allow download of each VC (to show that they can actually own their data)
|
|
||||||
|
|
||||||
- contacts v+ :
|
|
||||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
|
||||||
- .2 show error to user when adding a duplicate contact
|
|
||||||
- 01 parse input more robustly (with CSV lib and not commas)
|
|
||||||
|
|
||||||
- stats v1 :
|
|
||||||
- 01 show numeric stats
|
|
||||||
- 04 show different graphic for projects vs people (gnome?) on world
|
|
||||||
- 01 link to world for specific stats
|
|
||||||
- .5 don't load another instance of a bush if it already exists
|
|
||||||
- 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)
|
|
||||||
|
|
||||||
- Release Minimum Viable Product :
|
- Release Minimum Viable Product :
|
||||||
- generate new webpush.db entries, data/webpush.db private_key_hex & subscription_info & vapid_claims email
|
- 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)
|
- .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
|
- 08 thorough testing for errors & edge cases
|
||||||
- 01 ensure ability to recover server remotely, and add redundant access
|
- 01 ensure ability to recover server remotely, and add redundant access
|
||||||
@@ -67,8 +35,36 @@ tasks:
|
|||||||
- Test PWA features on Android and iOS.
|
- Test PWA features on Android and iOS.
|
||||||
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
|
||||||
|
|
||||||
|
- 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?
|
||||||
|
- .5 customize favicon assignee-group:ui
|
||||||
|
- .2 Show a warning if both giver and recipient are the same (but still allow?)
|
||||||
|
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
|
||||||
|
- .5 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)
|
||||||
|
- 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
|
||||||
|
- .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+ :
|
||||||
|
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||||
|
- .2 show error to user when adding a duplicate contact
|
||||||
|
- 01 parse input more robustly (with CSV lib and not commas)
|
||||||
|
- stats v1 :
|
||||||
|
- 01 show numeric stats
|
||||||
|
- 04 show different graphic for projects vs people (gnome?) on world
|
||||||
|
- 01 link to world for specific stats
|
||||||
|
- .5 don't load another instance of a bush if it already exists
|
||||||
|
- 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)
|
||||||
|
|
||||||
- .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
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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
Normal file
3
src/libs/util.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const isGlobalUri = (uri: string) => {
|
||||||
|
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
Normal file
65
src/views/HelpNotificationsView.vue
Normal file
@@ -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>
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
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.)
|
||||||
|
|||||||
Reference in New Issue
Block a user