Compare commits

..

1 Commits

59 changed files with 15180 additions and 9044 deletions

View File

@@ -1,7 +0,0 @@
# I tried setting values here and using `vue-cli-service build --mode development`
# but it didn't create some things in "dist":
# - the "css" directory with the CSS extracted from Vue files
# - the sw_scripts-combined* files
#
# ¯\_(ツ)_/¯

View File

@@ -1,4 +0,0 @@
# Only the variables that start with VITE_ are seen in the application process.env in Vue.
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app

View File

@@ -2,7 +2,6 @@ module.exports = {
root: true, root: true,
env: { env: {
node: true, node: true,
es2022: true,
}, },
extends: [ extends: [
"plugin:vue/vue3-essential", "plugin:vue/vue3-essential",
@@ -10,9 +9,9 @@ module.exports = {
"@vue/typescript/recommended", "@vue/typescript/recommended",
"plugin:prettier/recommended", "plugin:prettier/recommended",
], ],
// parserOptions: { parserOptions: {
// ecmaVersion: 2020, ecmaVersion: 2020,
// }, },
rules: { rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",

View File

@@ -6,43 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Changed in DB or environment
- Nothing
## [0.3.6] - 2024.03.24 - 3a07e31d6313ab95711265562d9023c42916e141
### Added
- Button to mirror photo during video
- More detailed onboarding help screen
- Public-data blurb
### Changed in DB or environment
- Nothing
## [0.3.5] - 2024.03.23 - 28754bdfb1e11aa221dd49a5dce4219b69cf6a9d
### Added
- Photo on gift records
### Fixed
- Environment variable for BVC meetings project
- Environment variables and build enhancements for test vs prod
### Changed in DB or environment
- New environment variable for image API server
- Test that a new browser session will get the right default APIs.
- Test that a new browser session will send the right BVC meetings project.
## [0.2.17] - 2024.03.01 - 3612ea42240c5e1b7d7eff29a39ff18f1b869b36
### Added
- Shortcut page for Bountiful Voluntaryist Community
### Changed
- More readable, targeted summaries in home-page feed items
### Changed in DB ### Changed in DB
- Nothing - ?
## [0.2.14] - 2024.02.14 - 5f9edea1167dbfb64e16648764eed8c09b24eaeb ## [0.2.14] - 2024.02.14
### Changed ### Changed
- Combine all service worker scripts into a single file. - Combine all service worker scripts into a single file
### Changed in DB ### Changed in DB
- Nothing - Nothing

View File

@@ -18,11 +18,6 @@ npm install
### Compiles and hot-reloads for development ### Compiles and hot-reloads for development
``` ```
npm run dev
```
### Builds the production app
```
npm run serve npm run serve
``` ```
@@ -37,33 +32,21 @@ npm run lint
* `npx prettier --write ./sw_scripts/` * `npx prettier --write ./sw_scripts/`
* Update the project.task.yaml & CHANGELOG.md & the version in package.json, run `npm install`. * Update the project.task.yaml & CHANGELOG.md & the version in package.json, run `npm install`, and commit.
* Record what version is currently on production. * [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
* Run the correct build ... though maybe you do that after testing and release, since that isn't used in the build (and you often increment a lot during testing).
* Test * If production: change src/constants/app.ts DEFAULT_*_SERVER to be "PROD" and package.json to remove "_Test". Also record what version is on production.
```
# (See .env.development for more details.)
# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
VITE_TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app npm run build
```
* Production * `npm run build`
```
# This picks up values from .env.production
npm run build
```
* Get on the server and back up 3 DBs and the time-safari folder. * Get on the server and back up the time-safari folder.
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari` * `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
* Commit changes. Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production. * Revert src/constants/app.ts and package.json (if that was prod), edit package.json to increment version & add "-beta", `npm install`, and commit. Tag if you didn't before. Also record what version is on production.
* [Tag with the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
@@ -131,7 +114,7 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
* Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data", and also manually remove the IndexedDB data if the DBs still show.) * Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data", and also manually remove the IndexedDB data if the DBs still show.)
* Clear notification permission. (In Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".) * Clear notification permission. (In Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".)
* Unregister service worker. (In Chrome, go to `chrome://serviceworker-internals`; in Firefox, go to `about:serviceworkers`.) * Unregister service worker. (In Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`.)
* Clear Cache Storage manually, possibly deleting the DB. (In Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.) * Clear Cache Storage manually, possibly deleting the DB. (In Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.)
(If you find more, add them to the HelpNotificationsView.vue file.) (If you find more, add them to the HelpNotificationsView.vue file.)

3
babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.ico">
<title>TimeSafari</title>
</head>
<body>
<noscript>
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

19232
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,92 +1,93 @@
{ {
"name": "TimeSafari", "name": "TimeSafari_Test",
"version": "0.3.7-beta", "version": "0.2.15-beta",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "serve": "vue-cli-service serve",
"serve": "vite preview", "build": "vue-cli-service build",
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build", "lint": "vue-cli-service lint"
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
"postbuild": "cp ./sw_scripts-combined.js dist/",
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js"
}, },
"dependencies": { "dependencies": {
"@dicebear/collection": "^5.4.1", "@dicebear/collection": "^5.3.5",
"@dicebear/core": "^5.4.1", "@dicebear/core": "^5.3.5",
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/vue-fontawesome": "^3.0.6", "@fortawesome/vue-fontawesome": "^3.0.3",
"@pvermeer/dexie-encrypted-addon": "^3.0.0", "@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.1.1", "@tweenjs/tween.js": "^21.0.0",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/luxon": "^3.4.2", "@veramo/core": "^5.4.1",
"@veramo/core": "^5.6.0", "@veramo/credential-w3c": "^5.4.1",
"@veramo/credential-w3c": "^5.6.0", "@veramo/data-store": "^5.4.1",
"@veramo/data-store": "^5.6.0", "@veramo/did-manager": "^5.4.1",
"@veramo/did-manager": "^5.6.0", "@veramo/did-provider-ethr": "^5.4.1",
"@veramo/did-provider-ethr": "^5.6.0", "@veramo/did-resolver": "^5.4.1",
"@veramo/did-resolver": "^5.6.0", "@veramo/key-manager": "^5.4.1",
"@veramo/key-manager": "^5.6.0", "@vueuse/core": "^10.4.1",
"@vueuse/core": "^10.9.0",
"@zxing/text-encoding": "^0.9.0", "@zxing/text-encoding": "^0.9.0",
"axios": "^1.6.8", "axios": "^1.5.0",
"buffer": "^6.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"dexie": "^3.2.7", "core-js": "^3.32.1",
"dexie-export-import": "^4.1.1", "dexie": "^3.2.4",
"did-jwt": "^7.4.7", "dexie-export-import": "^4.0.7",
"ethereum-cryptography": "^2.1.3", "did-jwt": "^7.2.7",
"ethereum-cryptography": "^2.1.2",
"ethereumjs-util": "^7.1.5", "ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.1.2", "ethr-did-resolver": "^8.1.2",
"git-describe": "^4.1.1",
"jdenticon": "^3.2.0", "jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"localstorage-slim": "^2.7.0", "localstorage-slim": "^2.5.0",
"lru-cache": "^10.2.0", "luxon": "^3.4.3",
"luxon": "^3.4.4",
"merkletreejs": "^0.3.11", "merkletreejs": "^0.3.11",
"moment": "^2.30.1", "moment": "^2.29.4",
"notiwind": "^2.0.2", "notiwind": "^2.0.2",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pina": "^0.20.2204228", "pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.2.1", "pinia-plugin-persistedstate": "^3.2.0",
"qr-code-generator-vue3": "^1.4.21", "qr-code-generator-vue3": "^1.4.21",
"ramda": "^0.29.1", "ramda": "^0.29.0",
"readable-stream": "^4.5.2", "readable-stream": "^4.4.2",
"reflect-metadata": "^0.1.14", "reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"simple-vue-camera": "^1.1.3",
"three": "^0.156.1", "three": "^0.156.1",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"util": "^0.12.5", "util": "^0.12.5",
"vue": "^3.4.21", "vue": "^3.3.4",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.4", "vue-facing-decorator": "^3.0.2",
"vue-qrcode-reader": "^5.5.3", "vue-qrcode-reader": "^5.4.1",
"vue-router": "^4.3.0", "vue-router": "^4.2.4",
"web-did-resolver": "^2.0.27" "web-did-resolver": "^2.0.27"
}, },
"devDependencies": { "devDependencies": {
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.4",
"@types/ramda": "^0.29.11", "@types/ramda": "^0.29.3",
"@types/three": "^0.155.1", "@types/three": "^0.155.1",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.6.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vue-leaflet/vue-leaflet": "^0.10.1", "@vue-leaflet/vue-leaflet": "^0.10.1",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-pwa": "~5.0.8",
"@vue/cli-plugin-router": "~5.0.8",
"@vue/cli-plugin-typescript": "~5.0.8",
"@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3", "@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.15",
"eslint": "^8.57.0", "eslint": "^8.53.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.23.0", "eslint-plugin-vue": "^9.17.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"postcss": "^8.4.38", "postcss": "^8.4.29",
"prettier": "^3.2.5", "prettier": "^3.1.0",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.3.3",
"typescript": "~5.2.2", "typescript": "~5.2.2"
"vite": "^5.2.0"
} }
} }

View File

@@ -1,56 +1,43 @@
tasks : tasks :
- fix the notification link to the app - 01 release server & client for fix to project-editing
- 01 change scanning flow - allow them to stay on the QR/scanning screen after scanning someone
- 24 contextual tutorials https://docs.google.com/document/d/11C_K3RM0rgo0onih20KFhcIzukZyq_CRWqaWX5om_kM/edit#heading=h.iwiwcydou5hw - .5 fix timeSafari.org cert renewals
- feeds - add "remote" filter, if they choose 'visible' then warn that they won't see any others, cache list & don't reload front page on change
- .1 add shortcut from project (etc?) to the public project page in a browser
- .1 add KindSpring link to ideas
- .1 on feed, don't show "to someone anonymous" if it's to a project
- 16 save data backups in Google
- 16 generate and use passkeys for identities
- .5 show "give" buttons (eg. from anonymous) even if they can't give, greyed out, and give them a warning and instructions
- .2 when adding a claim on home screen, push that claim to the top of the list
- .2 fix give dialog from "more contacts" off home page to allow giving to this user
- .5 stop from seeing an error on the first page when browser doesn't support service workers (which I've seen on iPhone; visible in Firefox private window)
- .2 don't show a warning on a totally new project when the authorized agent is set
- .2 anchor hash into BTC - .2 anchor hash into BTC
- .2 list the "show more" contacts alphabetically - .1 add step 1 to onboarding hints to "install"
- .5 add back the explicit wait for browser subscription timing problems?
- .5 make Time Safari a share_target for images - 01 bookmarks for BVC
- 08 add image on profile
- 01 ask to detect location & record it in settings
- 01 if personal location is set, show potential local affiliations
- 02 refactor the buttons for chosing a search location so that the actions are clear assignee-group:ui
- 24 compelling UI for credential presentations - 24 compelling UI for credential presentations
- discover who in my network has activity on a project - discover who in my network has activity on a project
- 24 compelling UI for statistics (eg. World?) - 24 compelling UI for statistics (eg. World?)
- 01 in the feed, group by project or contact or topic or time/$ (via BC); new projects, offers, search area, etc assignee-group:ui - .5 stop from seeing an error on the first page when browser doesn't support service workers (which I've seen on iPhone; visible in Firefox private window)
- 01 separate not-on-platform vs totally anonymous; terminology "unidentified"? - .2 add links between projects
- .2 add links between projects assignee-group:ui - 32 image on give :
- Show a camera to take a picture
- Scale the image to a reasonable size
- Upload to a public readable place
- check the rate limits
- use CID
- put the image URL in the claim
- Rates - images erased?
- image not associated with JWT ULID since that's assigned later
- 24 make the contact browsing on the front page something that invites more action - 24 make the contact browsing on the front page something that invites more action
- .2 list the "show more" contacts alphabetically
- .5 change server plan & project endpoints to use jwtId as identifier rather than rowid - .5 change server plan & project endpoints to use jwtId as identifier rather than rowid
- 16 edit offers & gives, or revoke allowing re-creation - 16 edit offers & gives, or revoke allowing re-creation
- .1 When available in the server, give message for 'nonAmountGiven' on offers on ProjectsView page. - .1 When available in the server, give message for 'nonAmountGiven' on offers on ProjectsView page.
- .1 Add help instructions for "Encryption key has changed" error. (It is a problem if localStorage is cleared, but the contacts & settings remain and they have to restore their seeds.) - .1 Add help instructions for "Encryption key has changed" error. (It is a problem if localStorage is cleared, but the contacts & settings remain and they have to restore their seeds.)
- .5 add more detail on TimeSafari.org
- .1 show better error when user with no ID goes to the "My Project" page - .1 show better error when user with no ID goes to the "My Project" page
- 01 in front page prompt for ideas for gratitude : - 08 add button to front page to prompt for ideas for gratitude :
- randomize (not show in order) - show previous on "Your" screen
- checkboxes - show non-person-oriented messages, show only contacts, show only projects - checkboxes - randomize vs show in order, show non-person-oriented messages, show only contacts, show only projects
- .5 add a notice on the front page if their notifications are off
- 08 allow user to add a time when they want their daily notification - 08 allow user to add a time when they want their daily notification
- .5 prompt for the name directly when they visit the QR scan page - .5 prompt for the name directly when they visit the QR scan page
@@ -64,9 +51,8 @@ tasks :
- .1 hide project-create button on project page if not registered - .1 hide project-create button on project page if not registered
- .1 hide offer & give buttons on project list page if not registered - .1 hide offer & give buttons on project list page if not registered
- .1 add cursor-pointer on the icons for giving on the project page, and on the list of projects on the discover page - .1 add cursor-pointer on the icons for giving on the project page, and on the list of projects on the discover page
- .2 record when InfiniteScroll hits the end of the list and don't trigger any more loads (feed, project list, give & offer lists) - .2 record when InfiniteScroll hits the end of the list and don't trigger any more loads
- bug (that is hard to reproduce) - got blank screen and errors on iPhone with no bottom tabs
- bug - turning notifications on from the help screen did not stay, though account screen toggle did stay (From Jason on iPhone.) - bug - turning notifications on from the help screen did not stay, though account screen toggle did stay (From Jason on iPhone.)
- refactor - supply the projectId to the OfferDialog just like we do with the GiftedDialog offerId (in the "open" method, maybe as well as an attribute) - refactor - supply the projectId to the OfferDialog just like we do with the GiftedDialog offerId (in the "open" method, maybe as well as an attribute)
- the confirm button on each give on the ProjectViewView page doesn't have all the context of the ClaimView page, so it can show sometimes inappropriately; consider consolidation - the confirm button on each give on the ProjectViewView page doesn't have all the context of the ClaimView page, so it can show sometimes inappropriately; consider consolidation
@@ -78,15 +64,16 @@ tasks :
- 01 show my VCs - most interesting, or via search - 01 show my VCs - most interesting, or via search
- 04 allow user to download & prove chains of VCs, mine + ones I can see about me from others - 04 allow user to download & prove chains of VCs, mine + ones I can see about me from others
- show feed of offers, new projects, etc -- maybe limited to my search area
- revenue to support server operation - revenue to support server operation
- .1 copy button for seed - copy button for seed
- .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
- make server endpoint for full English description of limits - make server endpoint for full English description of limits
- create a help-desk document & add screenshots - create a help-desk document & add screenshots
- .1 update "offer" units to have same functionality as "give" units - .1 update "offer" units to have same functionality as "give" units
- .5 add a link to any 'give' records that fulfill an offer on ClaimView
- 01 on home page, prompt for install check in addition to "supports notifications" check (since they won't get notified if Chrome is closed) - 01 on home page, prompt for install check in addition to "supports notifications" check (since they won't get notified if Chrome is closed)
- 01 on Mac (& Windows?) desktop, add a help blurb so that they can find it again (since it doesn't show in Application list) - 01 on Mac (& Windows?) desktop, add a help blurb so that they can find it again (since it doesn't show in Application list)
- bug (that is hard to reproduce) - got error adding on Firefox user #0 as contact for themselves - bug (that is hard to reproduce) - got error adding on Firefox user #0 as contact for themselves
@@ -98,7 +85,7 @@ tasks :
- .3 check that Android shows "back" buttons on screens without bottom tray - .3 check that Android shows "back" buttons on screens without bottom tray
- .1 Make give description text box into something that expands as they type? - .1 Make give description text box into something that expands as they type?
- .2 Show a warning if both giver and recipient are the same (but still allow?) - .2 Show a warning if both giver and recipient are the same (but still allow?)
- .5 Shrink the buttons on project 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 remove references to localStorage for projectId (now that it's pulling from the path) - .5 remove references to localStorage for projectId (now that it's pulling from the path)
- switch some checks for activeDid to check for isRegistered - switch some checks for activeDid to check for isRegistered
@@ -125,14 +112,10 @@ tasks :
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie) - 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
- .5 show seed phrase in a QR code for transfer to another device - .5 show seed phrase in a QR code for transfer to another device
- .5 on DiscoverView, switch to a filter UI (eg. just from friend) - .5 on DiscoverView, switch to a filter UI (eg. just from friend
- .5 don't show "Offer" on project screen if they aren't registered - .5 don't show "Offer" on project screen if they aren't registered
- 01 especially for iOS, check for new version & update, eg. https://stackoverflow.com/questions/52221805/any-way-yet-to-auto-update-or-just-clear-the-cache-on-a-pwa-on-ios
- 24 allow a person record with interests, including location; purpose? contact methods? enhance other connections the same? (suggestion from Philippines) assignee-group:ui
- 24 brief introduction slides https://docs.google.com/document/d/11C_K3RM0rgo0onih20KFhcIzukZyq_CRWqaWX5om_kM/edit#heading=h.iwiwcydou5hw
- 12 feedback https://docs.google.com/document/d/11C_K3RM0rgo0onih20KFhcIzukZyq_CRWqaWX5om_kM/edit#heading=h.iwiwcydou5hw
- 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 - import project interactions from GitHub/GitLab and manage signing
@@ -146,6 +129,7 @@ tasks :
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances - for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning) - for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
- .5 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui
- .5 Replace Gifted/Give in ContactsView with GiftedDialog - .5 Replace Gifted/Give in ContactsView with GiftedDialog
- Stats : - Stats :

17
public/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -1,7 +1,7 @@
<template> <template>
<router-view /> <router-view />
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind --> <!-- https://github.com/emmanuelsw/notiwind -->
<NotificationGroup group="alert"> <NotificationGroup group="alert">
<div <div
class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end" class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end"
@@ -129,7 +129,6 @@
</div> </div>
</NotificationGroup> </NotificationGroup>
<!-- These are general-purpose messages - except there are some for turning app notifications on and off. -->
<NotificationGroup group="modal"> <NotificationGroup group="modal">
<div class="fixed z-[100] top-0 inset-x-0 w-full"> <div class="fixed z-[100] top-0 inset-x-0 w-full">
<Notification <Notification
@@ -149,39 +148,6 @@
class="w-full" class="w-full"
role="alert" role="alert"
> >
<!-- type "confirm" will post a message and, with onYes function, show a "Yes" button to call that function -->
<div
v-if="notification.type === 'confirm'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<div
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
>
<div class="w-full px-6 py-6 text-slate-900 text-center">
<p class="text-lg mb-4">
{{ notification.title }}
</p>
<button
v-if="notification.onYes"
@click="
notification.onYes();
close(notification.id);
"
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
Yes
</button>
<button
@click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
{{ notification.onYes ? "Cancel" : "Close" }}
</button>
</div>
</div>
</div>
<div <div
v-if="notification.type === 'notification-permission'" v-if="notification.type === 'notification-permission'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50" class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
@@ -191,7 +157,7 @@
> >
<div class="w-full px-6 py-6 text-slate-900 text-center"> <div class="w-full px-6 py-6 text-slate-900 text-center">
<p v-if="serviceWorkerReady" class="text-lg mb-4"> <p v-if="serviceWorkerReady" class="text-lg mb-4">
Would you like to be notified of new activity once a day? Would you like to <b>turn on</b> notifications for this app?
</p> </p>
<p v-else class="text-lg mb-4"> <p v-else class="text-lg mb-4">
Waiting for system initialization, which may take up to 10 Waiting for system initialization, which may take up to 10
@@ -199,42 +165,22 @@
<fa icon="spinner" spin /> <fa icon="spinner" spin />
</p> </p>
<div v-if="serviceWorkerReady"> <button
<span class="flex flex-row justify-center"> v-if="serviceWorkerReady"
<span class="mt-2">Yes, tell me at: </span> class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
<input @click="
type="number" close(notification.id);
class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20" turnOnNotifications();
v-model="hourInput" "
/> >
<span Turn on Notifications
class="rounded-r border border-slate-400 bg-slate-200 text-center text-blue-500 mt-2 px-2 py-2 w-20" </button>
@click="hourAm = !hourAm"
>
<span v-if="hourAm"> AM <fa icon="chevron-down" /> </span>
<span v-else> PM <fa icon="chevron-up" /> </span>
</span>
</span>
<button
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white mt-2 px-2 py-2 rounded-md"
@click="
() => {
if (checkHour()) {
close(notification.id);
turnOnNotifications();
}
}
"
>
Turn on Daily Message
</button>
</div>
<button <button
@click="close(notification.id)" @click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white mt-4 px-2 py-2 rounded-md" class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
> >
No, Not Now Maybe Later
</button> </button>
</div> </div>
</div> </div>
@@ -317,11 +263,8 @@
<style></style> <style></style>
<script lang="ts"> <script lang="ts">
import axios from "axios";
import { Vue, Component } from "vue-facing-decorator"; import { Vue, Component } from "vue-facing-decorator";
import axios from "axios";
import * as libsUtil from "@/libs/util";
interface ServiceWorkerMessage { interface ServiceWorkerMessage {
type: string; type: string;
data: string; data: string;
@@ -345,23 +288,24 @@ interface VapidResponse {
}; };
} }
interface PushSubscriptionWithTime extends PushSubscriptionJSON { import { DEFAULT_PUSH_SERVER } from "@/constants/app";
notifyTime: { utcHour: number };
}
import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { sendTestThroughPushServer } from "@/libs/util"; import { sendTestThroughPushServer } from "@/libs/util";
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: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
b64 = ""; b64 = "";
hourAm = true; serviceWorkerReady = false;
hourInput = "8";
serviceWorkerReady = true;
async mounted() { async mounted() {
try { try {
@@ -372,29 +316,25 @@ export default class App extends Vue {
pushUrl = settings.webPushServer; pushUrl = settings.webPushServer;
} }
if (pushUrl.startsWith("http://localhost")) { await axios
console.log("Not checking for VAPID in this local environment."); .get(pushUrl + "/web-push/vapid")
} else { .then((response: VapidResponse) => {
await axios this.b64 = response.data?.vapidKey || "";
.get(pushUrl + "/web-push/vapid") console.log("Got vapid key:", this.b64);
.then((response: VapidResponse) => { navigator.serviceWorker.addEventListener("controllerchange", () => {
this.b64 = response.data?.vapidKey || ""; console.log("New service worker is now controlling the page");
console.log("Got vapid key:", this.b64);
navigator.serviceWorker.addEventListener("controllerchange", () => {
console.log("New service worker is now controlling the page");
});
}); });
if (!this.b64) { });
this.$notify( if (!this.b64) {
{ this.$notify(
group: "alert", {
type: "danger", group: "alert",
title: "Error Setting Notifications", type: "danger",
text: "Could not set notifications.", title: "Error Setting Notifications",
}, text: "Could not set notifications.",
-1, },
); -1,
} );
} }
} catch (error) { } catch (error) {
if (window.location.host.startsWith("localhost")) { if (window.location.host.startsWith("localhost")) {
@@ -494,48 +434,6 @@ export default class App extends Vue {
}); });
} }
// this allows us to show an error without closing the dialog
checkHour() {
if (!libsUtil.isNumeric(this.hourInput)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not a Number",
text: "The time must be an hour number.",
},
5000,
);
return false;
}
const hourNum = libsUtil.numberOrZero(this.hourInput);
if (!Number.isInteger(hourNum)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not a Whole Number",
text: "The time must be a whole hour number.",
},
5000,
);
return false;
}
if (hourNum < 1 || 12 < hourNum) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not a Whole Number",
text: "The time must be an hour between 1 and 12.",
},
5000,
);
return false;
}
return true;
}
public async turnOnNotifications() { public async turnOnNotifications() {
return this.askPermission() return this.askPermission()
.then((permission) => { .then((permission) => {
@@ -561,25 +459,13 @@ export default class App extends Vue {
}, },
-1, -1,
); );
// we already checked that this is a valid hour number this.sendSubscriptionToServer(subscription);
const rawHourNum = libsUtil.numberOrZero(this.hourInput); return subscription;
const adjHourNum = rawHourNum + (this.hourAm ? 0 : 12);
const hourNum = adjHourNum % 24;
const utcHour =
hourNum + Math.round(new Date().getTimezoneOffset() / 60);
const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24;
const subscriptionWithTime: PushSubscriptionWithTime = {
notifyTime: { utcHour: finalUtcHour },
...subscription.toJSON(),
};
await this.sendSubscriptionToServer(subscriptionWithTime);
return subscriptionWithTime;
} else { } else {
throw new Error("Subscription object is not available."); throw new Error("Subscription object is not available.");
} }
}) })
.then(async (subscription: PushSubscriptionWithTime) => { .then(async (subscription) => {
console.log( console.log(
"Subscription data sent to server and all finished successfully.", "Subscription data sent to server and all finished successfully.",
); );
@@ -670,7 +556,7 @@ export default class App extends Vue {
} }
private sendSubscriptionToServer( private sendSubscriptionToServer(
subscription: PushSubscriptionWithTime, 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", {
@@ -703,7 +589,7 @@ export default class App extends Vue {
} }
}) })
.catch((error) => { .catch((error) => {
console.error("Push provider server communication failed:", error); console.log("Push provider server communication failed:", error);
return false; return false;
}); });
@@ -718,7 +604,7 @@ export default class App extends Vue {
return response.ok; return response.ok;
}) })
.catch((error) => { .catch((error) => {
console.error("Push server communication failed:", error); console.log("Push server communication failed:", error);
return false; return false;
}); });

View File

@@ -1,8 +1,9 @@
@import url('https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap');
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap');
@layer base { @layer base {
html { html {
font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important; font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important;

View File

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

View File

@@ -10,22 +10,23 @@
placeholder="What was received" placeholder="What was received"
v-model="description" v-model="description"
/> />
<div class="flex flex-row justify-center"> <div class="flex flex-row">
<span <span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20" class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center text-blue-500 px-2 py-2"
@click="changeUnitCode()" @click="changeUnitCode()"
> >
{{ libsUtil.UNIT_SHORT[unitCode] }} {{ libsUtil.UNIT_SHORT[unitCode] }}
</span> </span>
<div <div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()" @click="decrement()"
v-if="amountInput !== '0'"
> >
<fa icon="chevron-left" /> <fa icon="chevron-left" />
</div> </div>
<input <input
type="number" type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20" class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
v-model="amountInput" v-model="amountInput"
/> />
<div <div
@@ -35,58 +36,37 @@
<fa icon="chevron-right" /> <fa icon="chevron-right" />
</div> </div>
</div> </div>
<div class="mt-4 flex justify-center"> <div class="mt-2 text-right">
<span v-if="showGivenToUser" class="mr-16">
<input type="checkbox" class="mr-2" v-model="givenToUser" />
<label class="text-sm">Given to you</label>
</span>
<span> <span>
<router-link <input type="checkbox" class="mr-2" v-model="isTrade" />
:to="{ <label class="text-sm">Trade (not a gift)</label>
name: 'gifted-details',
query: {
amountInput,
description,
giverDid: giver?.did,
giverName: giver?.name,
message,
offerId,
projectId,
unitCode,
},
}"
class="text-blue-500"
>
Photo, ...
</router-link>
</span> </span>
</div> </div>
<p class="text-center mb-2 mt-6 italic"> <p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world Sign & Send to publish to the world
<fa
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
/>
</p> </p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> <button
<button class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" @click="confirm"
@click="confirm" >
> Sign &amp; Send
Sign &amp; Send </button>
</button> <button
<button class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" @click="cancel"
@click="cancel" >
> Cancel
Cancel </button>
</button>
</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { import {
createAndSubmitGive, createAndSubmitGive,
didInfo, didInfo,
@@ -95,11 +75,19 @@ import {
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component @Component
export default class GiftedDialog extends Vue { export default class GiftedDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
@Prop message = ""; @Prop message = "";
@Prop projectId = ""; @Prop projectId = "";
@@ -111,9 +99,9 @@ export default class GiftedDialog extends Vue {
apiServer = ""; apiServer = "";
amountInput = "0"; amountInput = "0";
giver?: GiverInputInfo; // undefined means no identified giver agent
description = ""; description = "";
givenToUser = false; givenToUser = false;
giver?: GiverInputInfo; // undefined means no identified giver agent
isTrade = false; isTrade = false;
offerId = ""; offerId = "";
unitCode = "HUR"; unitCode = "HUR";
@@ -202,45 +190,6 @@ export default class GiftedDialog extends Vue {
} }
async confirm() { async confirm() {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identifier before you can record a give.",
},
3000,
);
return;
}
if (parseFloat(this.amountInput) < 0) {
this.$notify(
{
group: "alert",
type: "danger",
text: "You may not send a negative number.",
title: "",
},
2000,
);
return;
}
if (!this.description && !parseFloat(this.amountInput)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `You must enter a description or some number of ${
this.libsUtil.UNIT_LONG[this.unitCode]
}.`,
},
2000,
);
return;
}
this.close(); this.close();
this.$notify( this.$notify(
{ {
@@ -262,6 +211,22 @@ export default class GiftedDialog extends Vue {
}); });
} }
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records for DID ${activeDid} but no identifier was found",
);
}
return identity;
}
/** /**
* *
* @param giverDid may be null * @param giverDid may be null
@@ -275,8 +240,34 @@ export default class GiftedDialog extends Vue {
amountInput: number, amountInput: number,
unitCode: string = "HUR", unitCode: string = "HUR",
) { ) {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identifier before you can record a give.",
},
-1,
);
return;
}
if (!description && !amountInput) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `You must enter a description or some number of ${this.libsUtil.UNIT_LONG[unitCode]}.`,
},
-1,
);
return;
}
try { try {
const identity = await libsUtil.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,
@@ -359,18 +350,6 @@ export default class GiftedDialog extends Vue {
result.response?.data?.error?.message result.response?.data?.error?.message
); );
} }
explainData() {
this.$notify(
{
group: "alert",
type: "success",
title: "Data Sharing",
text: libsUtil.PRIVACY_MESSAGE,
},
-1,
);
}
} }
</script> </script>

View File

@@ -1,368 +0,0 @@
<template>
<div v-if="visible" class="dialog-overlay z-[60]">
<div class="dialog relative">
<div class="text-lg text-center font-light relative z-50">
<div
id="ViewHeading"
class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-2 bg-black/50 text-white leading-none"
>
<span v-if="uploading"> Uploading... </span>
<span v-else-if="blob"> Look Good? </span>
<span v-else> Say "Cheese"! </span>
</div>
<div
class="text-lg text-center p-2 leading-none absolute right-0 top-0 text-white"
@click="close()"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</div>
<div v-if="uploading" class="flex justify-center">
<fa icon="spinner" class="fa-spin fa-3x text-center block" />
</div>
<div v-else-if="blob">
<div
class="flex justify-center gap-2 absolute bottom-[1rem] left-[1rem] right-[1rem] bg-black/50 px-4 py-2"
>
<button
@click="uploadImage"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white font-bold py-2 px-4 rounded-md"
>
<span>Upload</span>
</button>
<button
@click="retryImage"
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white font-bold py-2 px-4 rounded-md"
>
<span>Retry</span>
</button>
</div>
<div class="flex justify-center">
<img :src="URL.createObjectURL(blob)" class="mt-2 rounded" />
</div>
</div>
<div v-else ref="cameraContainer">
<!--
Camera "resolution" doesn't change how it shows on screen but rather stretches the result, eg the following which just stretches it vertically:
:resolution="{ width: 375, height: 812 }"
-->
<camera
facingMode="environment"
autoplay
ref="camera"
@started="cameraStarted()"
>
<div
class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 portrait:pb-2 landscape:right-0 landscape:top-0 landscape:bottom-0 landscape:pr-4 flex landscape:flex-row justify-center items-center"
>
<button
@click="takeImage()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<fa icon="camera" class="w-[1em]"></fa>
</button>
</div>
<div
class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
>
<button
@click="swapMirrorClass()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<fa icon="left-right" class="w-[1em]"></fa>
</button>
</div>
<div v-if="numDevices > 1" class="absolute bottom-2 right-4">
<button
@click="switchCamera()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<fa icon="rotate" class="w-[1em]"></fa>
</button>
</div>
</camera>
</div>
</div>
</div>
</template>
<script lang="ts">
import axios from "axios";
import Camera from "simple-vue-camera";
import { Component, Vue } from "vue-facing-decorator";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { getIdentity } from "@/libs/util";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
@Component({ components: { Camera } })
export default class GiftedPhotoDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDeviceNumber = 0;
activeDid = "";
blob: Blob | null = null;
mirror = false;
numDevices = 0;
setImage: (arg: string) => void = () => {};
uploading = false;
visible = false;
URL = window.URL || window.webkitURL;
async mounted() {
try {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("Error retrieving settings from database:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: err.message || "There was an error retrieving your settings.",
},
-1,
);
}
}
open(setImageFn: (arg: string) => void) {
this.visible = true;
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
if (bottomNav) {
bottomNav.style.display = "none";
}
this.setImage = setImageFn;
}
close() {
this.visible = false;
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
if (bottomNav) {
bottomNav.style.display = "";
}
this.blob = null;
}
async cameraStarted() {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
if (cameraComponent) {
this.numDevices = (await cameraComponent.devices(["videoinput"])).length;
this.mirror = cameraComponent.facingMode === "user";
}
}
async switchCamera() {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
this.activeDeviceNumber = (this.activeDeviceNumber + 1) % this.numDevices;
const devices = await cameraComponent?.devices(["videoinput"]);
cameraComponent?.changeCamera(devices[this.activeDeviceNumber].deviceId);
}
async takeImage(/* payload: MouseEvent */) {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
/**
* This logic to set the image height & width correctly.
* Without it, the portrait orientation ends up with an image that is stretched horizontally.
* Note that it's the same with raw browser Javascript; see the "drawImage" example below.
* Now that I've done it, I can't explain why it works.
*/
let imageHeight = cameraComponent?.resolution?.height;
let imageWidth = cameraComponent?.resolution?.width;
const initialImageRatio = imageWidth / imageHeight;
const windowRatio = window.innerWidth / window.innerHeight;
if (initialImageRatio > 1 && windowRatio < 1) {
// the image is wider than it is tall, and the window is taller than it is wide
// For some reason, mobile in portrait orientation renders a horizontally-stretched image.
// We're gonna force it opposite.
imageHeight = cameraComponent?.resolution?.width;
imageWidth = cameraComponent?.resolution?.height;
} else if (initialImageRatio < 1 && windowRatio > 1) {
// the image is taller than it is wide, and the window is wider than it is tall
// Haven't seen this happen, but we'll do it just in case.
imageHeight = cameraComponent?.resolution?.width;
imageWidth = cameraComponent?.resolution?.height;
}
const newImageRatio = imageWidth / imageHeight;
if (newImageRatio < windowRatio) {
// the image is a taller ratio than the window, so fit the height first
imageHeight = window.innerHeight / 2;
imageWidth = imageHeight * newImageRatio;
} else {
// the image is a wider ratio than the window, so fit the width first
imageWidth = window.innerWidth / 2;
imageHeight = imageWidth / newImageRatio;
}
// The resolution is only necessary because of that mobile portrait-orientation case.
// The mobile emulation in a browser shows something stretched vertically, but real devices work fine.
this.blob = await cameraComponent?.snapshot({
height: imageHeight,
width: imageWidth,
}); // png is default; if that changes, change extension in formData.append
if (!this.blob) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error taking the picture. Please try again.",
},
5000,
);
return;
}
}
async retryImage() {
this.blob = null;
}
/****
Here's an approach to photo capture without a library. It has similar quirks.
Now that we've fixed styling for simple-vue-camera, it's not critical to refactor. Maybe someday.
<button id="start-camera" @click="cameraClicked">Start Camera</button>
<video id="video" width="320" height="240" autoplay></video>
<button id="snap-photo" @click="photoSnapped">Snap Photo</button>
<canvas id="canvas" width="320" height="240"></canvas>
async cameraClicked() {
const video = document.querySelector("#video");
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
});
if (video instanceof HTMLVideoElement) {
video.srcObject = stream;
}
}
photoSnapped() {
const video = document.querySelector("#video");
const canvas = document.querySelector("#canvas");
if (
canvas instanceof HTMLCanvasElement &&
video instanceof HTMLVideoElement
) {
canvas
?.getContext("2d")
?.drawImage(video, 0, 0, canvas.width, canvas.height);
// ... or set the blob:
// canvas?.toBlob(
// (blob) => {
// this.blob = blob;
// },
// "image/jpeg",
// 1,
// );
// data url of the image
const image_data_url = canvas?.toDataURL("image/jpeg");
}
}
****/
async uploadImage() {
this.uploading = true;
const identifier = await getIdentity(this.activeDid);
const token = await accessToken(identifier);
const headers = {
Authorization: "Bearer " + token,
};
const formData = new FormData();
if (!this.blob) {
// yeah, this should never happen, but it helps with subsequent type checking
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error finding the picture. Please try again.",
},
5000,
);
this.uploading = false;
return;
}
formData.append("image", this.blob, "snapshot.png"); // png is set in snapshot()
formData.append("claimType", "GiveAction");
try {
const response = await axios.post(
DEFAULT_IMAGE_API_SERVER + "/image",
formData,
{ headers },
);
this.uploading = false;
this.visible = false;
this.blob = null;
this.setImage(response.data.url as string);
} catch (error) {
console.error("Error uploading the image", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error saving the picture. Please try again.",
},
5000,
);
this.uploading = false;
this.blob = null;
}
}
swapMirrorClass() {
this.mirror = !this.mirror;
if (this.mirror) {
(this.$refs.cameraContainer as HTMLElement).classList.add("mirror-video");
} else {
(this.$refs.cameraContainer as HTMLElement).classList.remove(
"mirror-video",
);
}
}
}
</script>
<style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 700px;
}
.mirror-video {
transform: scaleX(-1);
-webkit-transform: scaleX(-1); /* For Safari */
-moz-transform: scaleX(-1); /* For Firefox */
-ms-transform: scaleX(-1); /* For IE */
-o-transform: scaleX(-1); /* For Opera */
}
</style>

View File

@@ -1,15 +1,7 @@
<template> <template>
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4 relative"> <h1 class="text-xl font-bold text-center mb-4">Here's one:</h1>
Here's one:
<div
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
@click="cancel"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</h1>
<span class="flex justify-between"> <span class="flex justify-between">
<span <span
class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex" class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex"
@@ -41,7 +33,7 @@
<span class="flex justify-between"> <span class="flex justify-between">
<span /> <span />
<button <button
class="text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4" class="text-center bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4"
@click="nextIdeaPastContacts()" @click="nextIdeaPastContacts()"
> >
Skip Contacts <fa icon="forward" /> Skip Contacts <fa icon="forward" />
@@ -60,7 +52,7 @@
</span> </span>
</span> </span>
<button <button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4"
@click="cancel" @click="cancel"
> >
That's it! That's it!
@@ -72,13 +64,20 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component } from "vue-facing-decorator"; import { Vue, Component } from "vue-facing-decorator";
import { AppString, NotificationIface } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component @Component
export default class GivenPrompts extends Vue { export default class GivenPrompts extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
IDEAS = [ IDEAS = [
"Did anyone fix food for you?", "Did anyone fix food for you?",

View File

@@ -50,36 +50,40 @@
<p class="text-center mt-6 mb-2 italic"> <p class="text-center mt-6 mb-2 italic">
Sign & Send to publish to the world Sign & Send to publish to the world
</p> </p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> <button
<button class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" @click="confirm"
@click="confirm" >
> Sign &amp; Send
Sign &amp; Send </button>
</button> <button
<button class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" @click="cancel"
@click="cancel" >
> Cancel
Cancel </button>
</button>
</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { createAndSubmitOffer } from "@/libs/endorserServer"; import { createAndSubmitOffer } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { Account } from "@/db/tables/accounts";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component @Component
export default class OfferDialog extends Vue { export default class OfferDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
@Prop message = ""; @Prop message = "";
@Prop projectId = ""; @Prop projectId = "";
@@ -103,7 +107,7 @@ export default class OfferDialog extends Vue {
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
// 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.error("Error retrieving settings from database:", err); console.log("Error retrieving settings from database:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -174,6 +178,22 @@ export default class OfferDialog extends Vue {
}); });
} }
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
`Attempted to load Offer records for DID ${activeDid} but no identifier was found`,
);
}
return identity;
}
/** /**
* *
* @param description may be an empty string * @param description may be an empty string
@@ -213,7 +233,7 @@ export default class OfferDialog extends Vue {
} }
try { try {
const identity = await libsUtil.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitOffer( const result = await createAndSubmitOffer(
this.axios, this.axios,
this.apiServer, this.apiServer,
@@ -230,7 +250,7 @@ export default class OfferDialog extends Vue {
this.isOfferCreationError(result.response) this.isOfferCreationError(result.response)
) { ) {
const errorMessage = this.getOfferCreationErrorMessage(result); const errorMessage = this.getOfferCreationErrorMessage(result);
console.error("Error with offer creation result:", result); console.log("Error with offer creation result:", result);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -253,7 +273,7 @@ export default class OfferDialog extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
console.error("Error with offer recordation caught:", error); console.log("Error with offer recordation caught:", error);
const message = const message =
error.userMessage || error.userMessage ||
error.response?.data?.error?.message || error.response?.data?.error?.message ||

View File

@@ -4,14 +4,20 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator"; import { Component, Vue, Prop } from "vue-facing-decorator";
import { AppString, NotificationIface } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { AppString } from "@/constants/app";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component @Component
export default class TopMessage extends Vue { export default class TopMessage extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
@Prop selected = ""; @Prop selected = "";

View File

@@ -12,20 +12,10 @@ export enum AppString {
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",
PROD_IMAGE_API_SERVER = "https://image-api.timesafari.app",
TEST_IMAGE_API_SERVER = "https://test-image-api.timesafari.app",
LOCAL_IMAGE_API_SERVER = "http://localhost:3001",
NO_CONTACT_NAME = "(no name)", NO_CONTACT_NAME = "(no name)",
} }
export const DEFAULT_ENDORSER_API_SERVER = export const DEFAULT_ENDORSER_API_SERVER = AppString.TEST_ENDORSER_API_SERVER;
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
AppString.TEST_ENDORSER_API_SERVER;
export const DEFAULT_IMAGE_API_SERVER =
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
AppString.TEST_IMAGE_API_SERVER;
export const DEFAULT_PUSH_SERVER = export const DEFAULT_PUSH_SERVER =
window.location.protocol + "//" + window.location.host; window.location.protocol + "//" + window.location.host;
@@ -39,5 +29,4 @@ export interface NotificationIface {
type: string; // "toast" | "info" | "success" | "warning" | "danger" type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string; title: string;
text: string; text: string;
onYes?: () => Promise<void>;
} }

View File

@@ -16,10 +16,6 @@ export type Settings = {
activeDid?: string; // Active Decentralized ID activeDid?: string; // Active Decentralized ID
apiServer?: string; // API server URL apiServer?: string; // API server URL
filterFeedByNearby?: boolean; // filter by nearby
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden
firstName?: string; // User's first name firstName?: string; // User's first name
isRegistered?: boolean; isRegistered?: boolean;
lastName?: string; // deprecated - put all names in firstName lastName?: string; // deprecated - put all names in firstName
@@ -35,16 +31,14 @@ export type Settings = {
}>; }>;
showContactGivesInline?: boolean; // Display contact inline or not showContactGivesInline?: boolean; // Display contact inline or not
showShortcutBvc?: boolean; // Show shortcut for BVC actions starredProjects?: Array<string>; // Array of starred project IDs
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
warnIfProdServer?: boolean; // Warn if using a production server warnIfProdServer?: boolean; // Warn if using a production server
warnIfTestServer?: boolean; // Warn if using a testing server warnIfTestServer?: boolean; // Warn if using a testing server
webPushServer?: string; // Web Push server URL webPushServer?: string; // Web Push server URL
}; };
export function isAnyFeedFilterOn(settings: Settings): boolean { export const DEFAULT_SETTINGS: Settings = { id: 1 };
return !!(settings.filterFeedByNearby || settings.filterFeedByVisible);
}
/** /**
* Schema for the Settings table in the database. * Schema for the Settings table in the database.

View File

@@ -113,7 +113,7 @@ export const sign = async (privateKeyHex: string) => {
* The SimpleSigner returns a configured function for signing data. * The SimpleSigner returns a configured function for signing data.
* *
* @example * @example
* const signer = SimpleSigner(import.meta.env.PRIVATE_KEY) * const signer = SimpleSigner(process.env.PRIVATE_KEY)
* signer(data, (err, signature) => { * signer(data, (err, signature) => {
* ... * ...
* }) * })

View File

@@ -1,11 +1,9 @@
import { Axios, AxiosResponse, RawAxiosRequestHeaders } from "axios";
import * as didJwt from "did-jwt";
import { LRUCache } from "lru-cache";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Contact } from "@/db/tables/contacts";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import { Axios, AxiosResponse } from "axios";
import { Contact } from "@/db/tables/contacts";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims // the object in RegisterAction claims
@@ -24,7 +22,7 @@ export interface AgreeVerifiableCredential {
"@type": string; "@type": string;
// "any" because arbitrary objects can be subject of agreement // "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
object: Record<string, any>; object: Record<any, any>;
} }
export interface GiverInputInfo { export interface GiverInputInfo {
@@ -48,29 +46,24 @@ export interface ClaimResult {
export interface GenericVerifiableCredential { export interface GenericVerifiableCredential {
"@context": string; "@context": string;
"@type": string; "@type": string;
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
} }
export interface GenericCredWrapper extends GenericVerifiableCredential { export interface GenericServerRecord extends GenericVerifiableCredential {
handleId?: string; handleId?: string;
id: string; id?: string;
issuedAt: string; issuedAt?: string;
issuer: string; issuer?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
claim: Record<string, any>; claim: Record<any, any>;
claimType?: string; claimType?: string;
} }
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper = { export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
"@context": SCHEMA_ORG_CONTEXT, "@context": SCHEMA_ORG_CONTEXT,
"@type": "", "@type": "",
claim: {}, claim: {},
id: "",
issuedAt: "",
issuer: "",
}; };
// a summary record; the VC is found the fullClaim field export interface GiveServerRecord {
export interface GiveSummaryRecord {
agentDid: string; agentDid: string;
amount: number; amount: number;
amountConfirmed: number; amountConfirmed: number;
@@ -84,8 +77,7 @@ export interface GiveSummaryRecord {
unit: string; unit: string;
} }
// a summary record; the VC is found the fullClaim field export interface OfferServerRecord {
export interface OfferSummaryRecord {
amount: number; amount: number;
amountGiven: number; amountGiven: number;
amountGivenConfirmed: number; amountGivenConfirmed: number;
@@ -102,17 +94,16 @@ export interface OfferSummaryRecord {
validThrough: string; validThrough: string;
} }
// a summary record; the VC is not currently part of this record export interface PlanServerRecord {
export interface PlanSummaryRecord {
agentDid?: string; // optional, if the issuer wants someone else to manage as well agentDid?: string; // optional, if the issuer wants someone else to manage as well
description: string; description: string;
endTime?: string; endTime?: string;
fulfillsPlanHandleId: string; fulfillsPlanHandleId: string;
handleId: string;
issuerDid: string; issuerDid: string;
handleId: string;
locLat?: number; locLat?: number;
locLon?: number; locLon?: number;
name?: string; name: string;
startTime?: string; startTime?: string;
url?: string; url?: string;
} }
@@ -126,7 +117,6 @@ export interface GiveVerifiableCredential {
description?: string; description?: string;
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[]; fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
identifier?: string; identifier?: string;
image?: string;
object?: { amountOfThisGood: number; unitCode: string }; object?: { amountOfThisGood: number; unitCode: string };
recipient?: { identifier: string }; recipient?: { identifier: string };
} }
@@ -190,7 +180,7 @@ export interface PlanData {
rowid?: string; rowid?: string;
} }
export interface EndorserRateLimits { export interface RateLimits {
doneClaimsThisWeek: string; doneClaimsThisWeek: string;
doneRegistrationsThisMonth: string; doneRegistrationsThisMonth: string;
maxClaimsPerWeek: string; maxClaimsPerWeek: string;
@@ -199,12 +189,6 @@ export interface EndorserRateLimits {
nextWeekBeginDateTime: string; nextWeekBeginDateTime: string;
} }
export interface ImageRateLimits {
doneImagesThisWeek: string;
maxImagesPerWeek: string;
nextWeekBeginDateTime: string;
}
export interface VerifiableCredential { export interface VerifiableCredential {
"@context": string; "@context": string;
"@type": string; "@type": string;
@@ -243,26 +227,22 @@ export interface ErrorResponse {
}; };
} }
export interface ErrorResult {
type: "error";
error: InternalError;
}
export interface InternalError { export interface InternalError {
error: string; // for system logging error: string; // for system logging
userMessage?: string; // for user display userMessage?: string; // for user display
} }
export interface ErrorResult extends ResultWithType {
type: "error";
error: InternalError;
}
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult; export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
// This is used to check for hidden info. // This is used to check for hidden info.
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6 // See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN"; const HIDDEN_DID = "did:none:HIDDEN";
const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
max: 500,
});
export function isDid(did: string) { export function isDid(did: string) {
return did.startsWith("did:"); return did.startsWith("did:");
} }
@@ -276,7 +256,7 @@ export function isEmptyOrHiddenDid(did?: string) {
} }
/** /**
* @return true for any string within this primitive/object/array where func(input) === true * @return true for any nested string where func(input) === true
* *
* Similar logic is found in endorser-mobile. * Similar logic is found in endorser-mobile.
*/ */
@@ -311,12 +291,6 @@ export function containsHiddenDid(obj: any) {
return testRecursivelyOnStrings(isHiddenDid, obj); return testRecursivelyOnStrings(isHiddenDid, obj);
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const containsNonHiddenDid = (obj: any) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return testRecursivelyOnStrings((s: any) => isDid(s) && !isHiddenDid(s), obj);
};
export function stripEndorserPrefix(claimId: string) { export function stripEndorserPrefix(claimId: string) {
if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) { if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) {
return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length); return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length);
@@ -354,7 +328,7 @@ export function addLastClaimOrHandleAsIdIfMissing(
} }
// return clone of object without any nested *VisibleToDids keys // return clone of object without any nested *VisibleToDids keys
// similar code is also contained in endorser-mobile // similar logic is found in endorser-mobile
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function removeVisibleToDids(input: any): any { export function removeVisibleToDids(input: any): any {
if (input instanceof Object) { if (input instanceof Object) {
@@ -364,6 +338,7 @@ export function removeVisibleToDids(input: any): any {
const result: Record<string, any> = {}; const result: Record<string, any> = {};
for (const key in input) { for (const key in input) {
if (!key.endsWith("VisibleToDids")) { if (!key.endsWith("VisibleToDids")) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result[key] = removeVisibleToDids(R.clone(input[key])); result[key] = removeVisibleToDids(R.clone(input[key]));
} }
} }
@@ -372,62 +347,16 @@ export function removeVisibleToDids(input: any): any {
// it's an array // it's an array
return R.map(removeVisibleToDids, input); return R.map(removeVisibleToDids, input);
} }
return false;
} else { } else {
return input; return input;
} }
} }
export function contactForDid(
did: string | undefined,
contacts: Contact[],
): Contact | undefined {
return isEmptyOrHiddenDid(did)
? undefined
: R.find((c) => c.did === did, contacts);
}
/** /**
* always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
* Similar logic is found in endorser-mobile.
*
* @param did
* @param activeDid
* @param contact
* @param allMyDids
* @return { known: boolean, displayName: string } where known is true if the display name is some easily-recogizable name, false if it's a generic name like "Someone Anonymous"
*/
export function didInfoForContact(
did: string | undefined,
activeDid: string | undefined,
contact?: Contact,
allMyDids: string[] = [],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): { known: boolean; displayName: string } {
if (!did) return { displayName: "Someone Anonymous", known: false };
if (contact) {
return {
displayName: contact.name || "Contact With No Name",
known: !!contact.name,
};
} else if (did === activeDid) {
return { displayName: "You", known: true };
} else {
const myId = R.find(R.equals(did), allMyDids);
return myId
? { displayName: "You (Alt ID)", known: true }
: isHiddenDid(did)
? { displayName: "Someone Totally Outside Your View", known: false }
: {
displayName: "Someone Visible But Outside Your Contact List",
known: false,
};
}
}
/** Similar logic is found in endorser-mobile.
always returns text, maybe something like "unnamed" or "unknown"
Now that we're using more informational didInfoForContact under the covers, we might want to consolidate.
**/ **/
export function didInfo( export function didInfo(
did: string | undefined, did: string | undefined,
@@ -435,73 +364,19 @@ export function didInfo(
allMyDids: string[], allMyDids: string[],
contacts: Contact[], contacts: Contact[],
): string { ): string {
const contact = contactForDid(did, contacts); if (!did) return "Someone Anonymous";
return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
}
async function getHeaders(identity: IIdentifier | null) { const contact = R.find((c) => c.did === did, contacts);
const headers: RawAxiosRequestHeaders = { if (contact) {
"Content-Type": "application/json", return contact.name || "Contact With No Name";
}; } else {
if (identity) { const myId = R.find(R.equals(did), allMyDids);
const token = await accessToken(identity); return myId
headers["Authorization"] = "Bearer " + token; ? `You${myId !== activeDid ? " (Alt ID)" : ""}`
: isHiddenDid(did)
? "Someone Not In Network"
: "Someone Not In Contacts";
} }
return headers;
}
/**
* @param handleId nullable -- which means that "undefined" will be returned
* @param identity nullable -- which means no private info will be returned
* @param axios
* @param apiServer
*/
export async function getPlanFromCache(
handleId: string | null,
identity: IIdentifier | null,
axios: Axios,
apiServer: string,
) {
if (!handleId) {
return undefined;
}
let cred = planCache.get(handleId);
if (!cred) {
const url =
apiServer +
"/api/v2/report/plans?handleId=" +
encodeURIComponent(handleId);
const headers = await getHeaders(identity);
try {
const resp = await axios.get(url, { headers });
if (resp.status === 200 && resp.data?.data?.length > 0) {
cred = resp.data.data[0];
planCache.set(handleId, cred);
} else {
console.log(
"Failed to load plan with handle",
handleId,
" Got data:",
resp.data,
);
}
} catch (error) {
console.log(
"Failed to load plan with handle",
handleId,
" Got error:",
error,
);
}
}
return cred;
}
export async function setPlanInCache(
handleId: string,
planSummary: PlanSummaryRecord,
) {
planCache.set(handleId, planSummary);
} }
/** /**
@@ -525,7 +400,6 @@ export async function createAndSubmitGive(
fulfillsProjectHandleId?: string, fulfillsProjectHandleId?: string,
fulfillsOfferHandleId?: string, fulfillsOfferHandleId?: string,
isTrade: boolean = false, isTrade: boolean = false,
imageUrl?: string,
): Promise<CreateAndSubmitClaimResult> { ): Promise<CreateAndSubmitClaimResult> {
const vcClaim: GiveVerifiableCredential = { const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org", "@context": "https://schema.org",
@@ -552,11 +426,8 @@ export async function createAndSubmitGive(
identifier: fulfillsOfferHandleId, identifier: fulfillsOfferHandleId,
}); });
} }
if (imageUrl) {
vcClaim.image = imageUrl;
}
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericCredWrapper, vcClaim as GenericServerRecord,
identity, identity,
apiServer, apiServer,
axios, axios,
@@ -605,35 +476,13 @@ export async function createAndSubmitOffer(
}; };
} }
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericCredWrapper, vcClaim as GenericServerRecord,
identity, identity,
apiServer, apiServer,
axios, axios,
); );
} }
// similar logic is found in endorser-mobile
export const createAndSubmitConfirmation = async (
identifier: IIdentifier,
claim: GenericVerifiableCredential,
lastClaimId: string, // used to set the lastClaimId
handleId: string | undefined,
apiServer: string,
axios: Axios,
) => {
const goodClaim = removeSchemaContext(
removeVisibleToDids(
addLastClaimOrHandleAsIdIfMissing(claim, lastClaimId, handleId),
),
);
const confirmationClaim: GenericVerifiableCredential = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
};
return createAndSubmitClaim(confirmationClaim, identifier, apiServer, axios);
};
export async function createAndSubmitClaim( export async function createAndSubmitClaim(
vcClaim: GenericVerifiableCredential, vcClaim: GenericVerifiableCredential,
identity: IIdentifier, identity: IIdentifier,
@@ -698,199 +547,12 @@ export async function createAndSubmitClaim(
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // from https://stackoverflow.com/a/175787/845494
export const isAccept = (claim: Record<string, any>) => { //
return ( export function isNumeric(str: string): boolean {
claim && return !isNaN(+str);
claim["@context"] === SCHEMA_ORG_CONTEXT &&
claim["@type"] === "AcceptAction"
);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isOffer = (claim: Record<string, any>) => {
return (
claim &&
claim["@context"] === SCHEMA_ORG_CONTEXT &&
claim["@type"] === "Offer"
);
};
export function currencyShortWordForCode(unitCode: string, single: boolean) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
} }
export function displayAmount(code: string, amt: number) { export function numberOrZero(str: string): number {
return "" + amt + " " + currencyShortWordForCode(code, amt === 1); return isNumeric(str) ? +str : 0;
} }
// insert a space before any capital letters except the initial letter
// (and capitalize initial letter, just in case)
export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
return !text
? ""
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
};
/**
return readable summary of claim, or something generic
similar code is also contained in endorser-mobile
**/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const claimSummary = (claim: Record<string, any>) => {
if (!claim) {
// to differentiate from "something" above
return "something";
}
if (claim.claim) {
// probably a Verified Credential
// eslint-disable-next-line @typescript-eslint/no-explicit-any
claim = claim.claim as Record<string, any>;
}
if (Array.isArray(claim)) {
if (claim.length === 1) {
claim = claim[0];
} else {
return "multiple claims";
}
}
const type = claim["@type"];
if (!type) {
return "a claim";
} else {
let typeExpl = capitalizeAndInsertSpacesBeforeCaps(type);
if (typeExpl === "Person") {
typeExpl += " claim";
}
return "a " + typeExpl;
}
};
/**
return readable description of claim if possible, as a past-tense action
identifiers is a list of objects with a 'did' field, each representing the user
contacts is a list of objects with a 'did' field for others and a 'name' field for their name
similar code is also contained in endorser-mobile
**/
export const claimSpecialDescription = (
record: GenericCredWrapper,
activeDid: string,
identifiers: Array<string>,
contacts: Array<Contact>,
) => {
let claim = record.claim;
if (claim.claim) {
// it's probably a Verified Credential
claim = claim.claim;
}
const issuer = didInfo(record.issuer, activeDid, identifiers, contacts);
const type = claim["@type"] || "UnknownType";
if (type === "AgreeAction") {
return issuer + " agreed with " + claimSummary(claim.object);
} else if (isAccept(claim)) {
return issuer + " accepted " + claimSummary(claim.object);
} else if (type === "GiveAction") {
// agent.did is for legacy data, before March 2023
const giver = claim.agent?.identifier || claim.agent?.did;
const giverInfo = didInfo(giver, activeDid, identifiers, contacts);
let gaveAmount = claim.object?.amountOfThisGood
? displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: "";
if (claim.description) {
if (gaveAmount) {
gaveAmount = gaveAmount + ", and also: ";
}
gaveAmount = gaveAmount + claim.description;
}
if (!gaveAmount) {
gaveAmount = "something not described";
}
// recipient.did is for legacy data, before March 2023
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
const gaveRecipientInfo = gaveRecipientId
? " to " + didInfo(gaveRecipientId, activeDid, identifiers, contacts)
: "";
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
} else if (type === "JoinAction") {
// agent.did is for legacy data, before March 2023
const agent = claim.agent?.identifier || claim.agent?.did;
const contactInfo = didInfo(agent, activeDid, identifiers, contacts);
let eventOrganizer =
claim.event && claim.event.organizer && claim.event.organizer.name;
eventOrganizer = eventOrganizer || "";
let eventName = claim.event && claim.event.name;
eventName = eventName ? " " + eventName : "";
let fullEvent = eventOrganizer + eventName;
fullEvent = fullEvent ? " attended the " + fullEvent : "";
let eventDate = claim.event && claim.event.startTime;
eventDate = eventDate ? " at " + eventDate : "";
return contactInfo + fullEvent + eventDate;
} else if (isOffer(claim)) {
const offerer = claim.offeredBy?.identifier;
const contactInfo = didInfo(offerer, activeDid, identifiers, contacts);
let offering = "";
if (claim.includesObject) {
offering +=
" " +
displayAmount(
claim.includesObject.unitCode,
claim.includesObject.amountOfThisGood,
);
}
if (claim.itemOffered?.description) {
offering += ", saying: " + claim.itemOffered?.description;
}
// recipient.did is for legacy data, before March 2023
const offerRecipientId =
claim.recipient?.identifier || claim.recipient?.did;
const offerRecipientInfo = offerRecipientId
? " to " + didInfo(offerRecipientId, activeDid, identifiers, contacts)
: "";
return contactInfo + " offered" + offering + offerRecipientInfo;
} else if (type === "PlanAction") {
const claimer = claim.agent?.identifier || record.issuer;
const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts);
return claimerInfo + " announced a project: " + claim.name;
} else if (type === "Tenure") {
// party.did is for legacy data, before March 2023
const claimer = claim.party?.identifier || claim.party?.did;
const contactInfo = didInfo(claimer, activeDid, identifiers, contacts);
const polygon = claim.spatialUnit?.geo?.polygon || "";
return (
contactInfo +
" possesses [" +
polygon.substring(0, polygon.indexOf(" ")) +
"...]"
);
} else {
return issuer + " declared " + claimSummary(claim as GenericCredWrapper);
}
};
export const BVC_MEETUPS_PROJECT_CLAIM_ID =
import.meta.env.VITE_BVC_MEETUPS_PROJECT_CLAIM_ID ||
"https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK"; // this won't resolve as a URL on production; it's a URN only found in the test system
export const bvcMeetingJoinClaim = (did: string, startTime: string) => {
return {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "JoinAction",
agent: {
identifier: did,
},
event: {
organizer: {
name: "Bountiful Voluntaryist Community",
},
name: "Saturday Morning Meeting",
startTime: startTime,
},
};
};

View File

@@ -1,19 +1,22 @@
// many of these are also found in endorser-mobile utility.ts // many of these are also found in endorser-mobile utility.ts
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse } from "axios";
import { IIdentifier } from "@veramo/core";
import { useClipboard } from "@vueuse/core";
import { DEFAULT_PUSH_SERVER } from "@/constants/app"; import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer"; import { GenericServerRecord, containsHiddenDid } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import { useClipboard } from "@vueuse/core";
export const PRIVACY_MESSAGE = // eslint-disable-next-line @typescript-eslint/no-var-requires
"The data you send be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to those you allow."; const Buffer = require("buffer/").Buffer;
// If you edit this, check that the numbers still line up on the side in the alert (on mobile, too),
// and make sure they can take all actions while the notification shows.
export const ONBOARD_MESSAGE =
"1) Check that they have entered their name on the profile page in their device. 2) Add them to your Contacts by scanning with the QR icon that is by the input box. 3) Click the person icon to register them. 4) Have them go to their Contact page and scan your QR to add you to their list.";
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
export const UNIT_SHORT: Record<string, string> = { export const UNIT_SHORT: Record<string, string> = {
@@ -52,25 +55,11 @@ export function iconForUnitCode(unitCode: string) {
return UNIT_CODES[unitCode]?.faIcon || "question"; return UNIT_CODES[unitCode]?.faIcon || "question";
} }
// from https://stackoverflow.com/a/175787/845494
// ... though it appears even this isn't precisely right so keep doing "|| 0" or something in sensitive places
//
export function isNumeric(str: string): boolean {
// This ignore commentary is because typescript complains when you pass a string to isNaN.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return !isNaN(str) && !isNaN(parseFloat(str));
}
export function numberOrZero(str: string): number {
return isNumeric(str) ? +str : 0;
}
export const isGlobalUri = (uri: string) => { export const isGlobalUri = (uri: string) => {
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/)); return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
}; };
export const giveIsConfirmable = (veriClaim: GenericCredWrapper) => { export const giveIsConfirmable = (veriClaim: GenericServerRecord) => {
return veriClaim.claimType === "GiveAction"; return veriClaim.claimType === "GiveAction";
}; };
@@ -86,7 +75,7 @@ export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
* @param veriClaim is expected to have fields: claim, claimType, and issuer * @param veriClaim is expected to have fields: claim, claimType, and issuer
*/ */
export const isGiveRecordTheUserCanConfirm = ( export const isGiveRecordTheUserCanConfirm = (
veriClaim: GenericCredWrapper, veriClaim: GenericServerRecord,
activeDid: string, activeDid: string,
confirmerIdList: string[] = [], confirmerIdList: string[] = [],
) => { ) => {
@@ -102,9 +91,9 @@ export const isGiveRecordTheUserCanConfirm = (
* @returns the DID of the person who offered, or undefined if hidden * @returns the DID of the person who offered, or undefined if hidden
* @param veriClaim is expected to have fields: claim and issuer * @param veriClaim is expected to have fields: claim and issuer
*/ */
export const offerGiverDid: (arg0: GenericCredWrapper) => string | undefined = ( export const offerGiverDid: (
veriClaim, arg0: GenericServerRecord,
) => { ) => string | undefined = (veriClaim) => {
let giver; let giver;
if ( if (
veriClaim.claim.offeredBy?.identifier && veriClaim.claim.offeredBy?.identifier &&
@@ -121,7 +110,7 @@ export const offerGiverDid: (arg0: GenericCredWrapper) => string | undefined = (
* @returns true if the user can fulfill the offer * @returns true if the user can fulfill the offer
* @param veriClaim is expected to have fields: claim, claimType, and issuer * @param veriClaim is expected to have fields: claim, claimType, and issuer
*/ */
export const canFulfillOffer = (veriClaim: GenericCredWrapper) => { export const canFulfillOffer = (veriClaim: GenericServerRecord) => {
return !!(veriClaim.claimType === "Offer" && offerGiverDid(veriClaim)); return !!(veriClaim.claimType === "Offer" && offerGiverDid(veriClaim));
}; };
@@ -191,22 +180,6 @@ export function findAllVisibleToDids(
* *
**/ **/
export const getIdentity = async (activeDid: string): Promise<IIdentifier> => {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
`Attempted to load Offer records for DID ${activeDid} but no identifier was found`,
);
}
return identity;
};
/** /**
* Generates a new identity, saves it to the database, and sets it as the active identity. * Generates a new identity, saves it to the database, and sets it as the active identity.
* @return {Promise<string>} with the DID of the new identity * @return {Promise<string>} with the DID of the new identity
@@ -238,7 +211,7 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
}; };
export const sendTestThroughPushServer = async ( export const sendTestThroughPushServer = async (
subscriptionJSON: PushSubscriptionJSON, subscription: PushSubscription,
skipFilter: boolean, skipFilter: boolean,
): Promise<AxiosResponse> => { ): Promise<AxiosResponse> => {
await db.open(); await db.open();
@@ -253,11 +226,28 @@ export const sendTestThroughPushServer = async (
// Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213 // Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION"; const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
const auth = Buffer.from(subscription.getKey("auth"));
const authB64 = auth
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const p256dh = Buffer.from(subscription.getKey("p256dh"));
const p256dhB64 = p256dh
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const newPayload = { const newPayload = {
// eslint-disable-next-line prettier/prettier endpoint: subscription.endpoint,
message: `Test, where you will see this message ${ skipFilter ? "un" : "" }filtered.`, keys: {
auth: authB64,
p256dh: p256dhB64,
},
message: `Test, where you will see this message ${
skipFilter ? "un" : ""
}filtered.`,
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push", title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
...subscriptionJSON,
}; };
console.log("Sending a test web push message:", newPayload); console.log("Sending a test web push message:", newPayload);
const payloadStr = JSON.stringify(newPayload); const payloadStr = JSON.stringify(newPayload);

View File

@@ -18,12 +18,8 @@ import {
faBitcoinSign, faBitcoinSign,
faBurst, faBurst,
faCalendar, faCalendar,
faCamera,
faCheck,
faChevronDown,
faChevronLeft, faChevronLeft,
faChevronRight, faChevronRight,
faChevronUp,
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleInfo, faCircleInfo,
@@ -47,7 +43,6 @@ import {
faHand, faHand,
faHandHoldingHeart, faHandHoldingHeart,
faHouseChimney, faHouseChimney,
faLeftRight,
faLocationDot, faLocationDot,
faLongArrowAltLeft, faLongArrowAltLeft,
faLongArrowAltRight, faLongArrowAltRight,
@@ -66,6 +61,7 @@ import {
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus, faSquarePlus,
faStar,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,
faUser, faUser,
@@ -81,12 +77,8 @@ library.add(
faBitcoinSign, faBitcoinSign,
faBurst, faBurst,
faCalendar, faCalendar,
faCamera,
faCheck,
faChevronDown,
faChevronLeft, faChevronLeft,
faChevronRight, faChevronRight,
faChevronUp,
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleInfo, faCircleInfo,
@@ -110,7 +102,6 @@ library.add(
faHand, faHand,
faHandHoldingHeart, faHandHoldingHeart,
faHouseChimney, faHouseChimney,
faLeftRight,
faLocationDot, faLocationDot,
faLongArrowAltLeft, faLongArrowAltLeft,
faLongArrowAltRight, faLongArrowAltRight,
@@ -129,6 +120,7 @@ library.add(
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus, faSquarePlus,
faStar,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,
faUser, faUser,
@@ -137,11 +129,9 @@ library.add(
); );
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import Camera from "simple-vue-camera";
createApp(App) createApp(App)
.component("fa", FontAwesomeIcon) .component("fa", FontAwesomeIcon)
.component("camera", Camera)
.use(createPinia()) .use(createPinia())
.use(VueAxios, axios) .use(VueAxios, axios)
.use(router) .use(router)

View File

@@ -2,7 +2,7 @@
import { register } from "register-service-worker"; import { register } from "register-service-worker";
if (import.meta.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
register("/sw_scripts-combined.js", { register("/sw_scripts-combined.js", {
ready() { ready() {
console.log( console.log(

View File

@@ -28,162 +28,192 @@ const enterOrStart = async (
}; };
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "home",
component: () =>
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
},
{ {
path: "/account", path: "/account",
name: "account", name: "account",
component: () => import("../views/AccountViewView.vue"), component: () =>
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
}, },
{ {
path: "/claim/:id?", path: "/claim/:id?",
name: "claim", name: "claim",
component: () => import("../views/ClaimView.vue"), component: () =>
import(/* webpackChunkName: "claim" */ "../views/ClaimView.vue"),
}, },
{ {
path: "/confirm-contact", path: "/confirm-contact",
name: "confirm-contact", name: "confirm-contact",
component: () => import("../views/ConfirmContactView.vue"), component: () =>
import(
/* webpackChunkName: "confirm-contact" */ "../views/ConfirmContactView.vue"
),
}, },
{ {
path: "/contact-amounts", path: "/contact-amounts",
name: "contact-amounts", name: "contact-amounts",
component: () => import("../views/ContactAmountsView.vue"), component: () =>
import(
/* webpackChunkName: "contact-amounts" */ "../views/ContactAmountsView.vue"
),
}, },
{ {
path: "/contact-gives", path: "/contact-gives",
name: "contact-gives", name: "contact-gives",
component: () => import("../views/ContactGiftingView.vue"), component: () =>
import(
/* webpackChunkName: "contact-gives" */ "../views/ContactGiftingView.vue"
),
}, },
{ {
path: "/contact-qr", path: "/contact-qr",
name: "contact-qr", name: "contact-qr",
component: () => import("../views/ContactQRScanShowView.vue"), component: () =>
import(
/* webpackChunkName: "contact-qr" */ "../views/ContactQRScanShowView.vue"
),
}, },
{ {
path: "/contacts", path: "/contacts",
name: "contacts", name: "contacts",
component: () => import("../views/ContactsView.vue"), component: () =>
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
}, },
{ {
path: "/discover", path: "/discover",
name: "discover", name: "discover",
component: () => import("../views/DiscoverView.vue"), component: () =>
}, import(/* webpackChunkName: "discover" */ "../views/DiscoverView.vue"),
{
path: "/gifted-details",
name: "gifted-details",
component: () => import("../views/GiftedDetails.vue"),
}, },
{ {
path: "/help", path: "/help",
name: "help", name: "help",
component: () => import("../views/HelpView.vue"), component: () =>
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
}, },
{ {
path: "/help-notifications", path: "/help-notifications",
name: "help-notifications", name: "help-notifications",
component: () => import("../views/HelpNotificationsView.vue"), component: () =>
}, import(
{ /* webpackChunkName: "help-notifications" */ "../views/HelpNotificationsView.vue"
path: "/help-onboarding", ),
name: "help-onboarding",
component: () => import("../views/HelpOnboardingView.vue"),
},
{
path: "/",
name: "home",
component: () => import("../views/HomeView.vue"),
}, },
{ {
path: "/identity-switcher", path: "/identity-switcher",
name: "identity-switcher", name: "identity-switcher",
component: () => import("../views/IdentitySwitcherView.vue"), component: () =>
import(
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
),
}, },
{ {
path: "/import-account", path: "/import-account",
name: "import-account", name: "import-account",
component: () => import("../views/ImportAccountView.vue"), component: () =>
import(
/* webpackChunkName: "import-account" */ "../views/ImportAccountView.vue"
),
}, },
{ {
path: "/import-derive", path: "/import-derive",
name: "import-derive", name: "import-derive",
component: () => import("../views/ImportDerivedAccountView.vue"), component: () =>
import(
/* webpackChunkName: "import-derive" */ "../views/ImportDerivedAccountView.vue"
),
}, },
{ {
path: "/new-edit-account", path: "/new-edit-account",
name: "new-edit-account", name: "new-edit-account",
component: () => import("../views/NewEditAccountView.vue"), component: () =>
import(
/* webpackChunkName: "new-edit-account" */ "../views/NewEditAccountView.vue"
),
}, },
{ {
path: "/new-edit-project", path: "/new-edit-project",
name: "new-edit-project", name: "new-edit-project",
component: () => import("../views/NewEditProjectView.vue"), component: () =>
import(
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
),
}, },
{ {
path: "/new-identifier", path: "/new-identifier",
name: "new-identifier", name: "new-identifier",
component: () => import("../views/NewIdentifierView.vue"), component: () =>
import(
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
),
}, },
{ {
path: "/project/:id?", path: "/project/:id?",
name: "project", name: "project",
component: () => import("../views/ProjectViewView.vue"), component: () =>
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
}, },
{ {
path: "/projects", path: "/projects",
name: "projects", name: "projects",
component: () => import("../views/ProjectsView.vue"), component: () =>
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
beforeEnter: enterOrStart, beforeEnter: enterOrStart,
}, },
{
path: "/quick-action-bvc",
name: "quick-action-bvc",
component: () => import("../views/QuickActionBvcView.vue"),
},
{
path: "/quick-action-bvc-begin",
name: "quick-action-bvc-begin",
component: () => import("../views/QuickActionBvcBeginView.vue"),
},
{
path: "/quick-action-bvc-end",
name: "quick-action-bvc-end",
component: () => import("../views/QuickActionBvcEndView.vue"),
},
{ {
path: "/scan-contact", path: "/scan-contact",
name: "scan-contact", name: "scan-contact",
component: () => import("../views/ContactScanView.vue"), component: () =>
import(
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
),
}, },
{ {
path: "/search-area", path: "/search-area",
name: "search-area", name: "search-area",
component: () => import("../views/SearchAreaView.vue"), component: () =>
import(
/* webpackChunkName: "search-area" */ "../views/SearchAreaView.vue"
),
}, },
{ {
path: "/seed-backup", path: "/seed-backup",
name: "seed-backup", name: "seed-backup",
component: () => import("../views/SeedBackupView.vue"), component: () =>
import(
/* webpackChunkName: "seed-backup" */ "../views/SeedBackupView.vue"
),
}, },
{ {
path: "/start", path: "/start",
name: "start", name: "start",
component: () => import("../views/StartView.vue"), component: () =>
import(/* webpackChunkName: "start" */ "../views/StartView.vue"),
}, },
{ {
path: "/statistics", path: "/statistics",
name: "statistics", name: "statistics",
component: () => import("../views/StatisticsView.vue"), component: () =>
import(
/* webpackChunkName: "statistics" */ "../views/StatisticsView.vue"
),
}, },
{ {
path: "/test", path: "/test",
name: "test", name: "test",
component: () => import("../views/TestView.vue"), component: () =>
import(/* webpackChunkName: "test" */ "../views/TestView.vue"),
}, },
]; ];
/** @type {*} */ /** @type {*} */
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(process.env.BASE_URL),
routes, routes,
}); });

1
src/util.d.ts vendored
View File

@@ -1,5 +1,4 @@
// from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/util.d.ts // from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/util.d.ts
/* eslint-disable */
/** /**
* The `node:util` module supports the needs of Node.js internal APIs. Many of the * The `node:util` module supports the needs of Node.js internal APIs. Many of the
* utilities are useful for application and module developers as well. To access * utilities are useful for application and module developers as well. To access

View File

@@ -1,5 +1,5 @@
<template> <template>
<QuickNav selected="Profile" /> <QuickNav selected="Profile"></QuickNav>
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
@@ -37,7 +37,7 @@
<span> <span>
<router-link <router-link
:to="{ name: 'help' }" :to="{ name: 'help' }"
class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1" class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
> >
Help Help
</router-link> </router-link>
@@ -55,7 +55,7 @@
</p> </p>
<router-link <router-link
:to="{ name: 'start' }" :to="{ name: 'start' }"
class="inline-block text-md uppercase bg-gradient-to-b from-amber-400 to-amber-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
> >
Create An Identifier Create An Identifier
</router-link> </router-link>
@@ -96,7 +96,7 @@
<!-- Registration notice --> <!-- Registration notice -->
<!-- We won't show any loading indicator because it usually doesn't change anything. We'll just pop the message in only if we discover that they need it. --> <!-- We won't show any loading indicator because it usually doesn't change anything. We'll just pop the message in only if we discover that they need it. -->
<div <div
v-if="!loadingLimits && !endorserLimits?.nextWeekBeginDateTime" v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4" class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
> >
<p class="mb-4"> <p class="mb-4">
@@ -105,15 +105,13 @@
</p> </p>
<router-link <router-link
:to="{ name: 'contact-qr' }" :to="{ name: 'contact-qr' }"
class="inline-block text-md uppercase bg-gradient-to-b from-amber-400 to-amber-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
> >
Share Your Info Share Your Info
</router-link> </router-link>
</div> </div>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
<!-- label -->
<div class="mb-2 font-bold">Settings</div>
<div <div
v-if="!notificationMaybeChanged" v-if="!notificationMaybeChanged"
class="flex items-center justify-between cursor-pointer" class="flex items-center justify-between cursor-pointer"
@@ -142,38 +140,16 @@
Notification status may have changed. Refresh this page to see the Notification status may have changed. Refresh this page to see the
latest setting. latest setting.
</div> </div>
<router-link class="pl-4 text-sm text-blue-500" to="/help-notifications"> <router-link class="px-4 text-sm text-blue-500" to="/help-notifications">
Troubleshoot your notification setup. Troubleshoot your notification setup.
</router-link> </router-link>
<router-link
:to="{ name: 'search-area' }"
v-if="activeDid"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-6"
>
Set Search Area…
<!-- If already set, change button label to "Change Search Area" -->
</router-link>
<div class="text-slate-500 text-sm font-bold mt-6 mb-2">
Topics of Interest
</div>
<textarea
class="block w-full rounded border border-slate-400 px-3 py-2"
rows="3"
value="longing, rusted, seventeen, daybreak, furnace, nine, benign, homecoming, one, freight car"
>
</textarea>
<div class="text-slate-500 text-sm mt-2 mb-2">
Separate topics with a comma.
</div>
</div> </div>
<div <div
v-if="activeDid" v-if="activeDid"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8" class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
> >
<div class="mb-2 font-bold">Usage Limits</div> <div class="mb-2">Usage Limits</div>
<!-- show spinner if loading limits --> <!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="text-center"> <div v-if="loadingLimits" class="text-center">
Checking&hellip; <fa icon="spinner" class="fa-spin"></fa> Checking&hellip; <fa icon="spinner" class="fa-spin"></fa>
@@ -181,42 +157,31 @@
<div> <div>
{{ limitsMessage }} {{ limitsMessage }}
</div> </div>
<div v-if="!!endorserLimits?.nextWeekBeginDateTime"> <div v-if="!!limits?.nextWeekBeginDateTime">
<p class="text-sm"> <p class="mb-3 text-sm">
You have done You have done <b>{{ limits.doneClaimsThisWeek }}</b> claims out of
<b>{{ endorserLimits.doneClaimsThisWeek }} claims</b> out of <b>{{ limits.maxClaimsPerWeek }}</b> for this week. Your claims
<b>{{ endorserLimits.maxClaimsPerWeek }}</b> for this week. Your counter resets at
claims counter resets at
<b class="whitespace-nowrap">{{ <b class="whitespace-nowrap">{{
readableDate(endorserLimits.nextWeekBeginDateTime) readableTime(limits.nextWeekBeginDateTime)
}}</b> }}</b>
</p> </p>
<p class="mt-3 text-sm"> <p class="text-sm">
You have done You have done
<b>{{ endorserLimits.doneRegistrationsThisMonth }} registrations</b> <b>{{ limits.doneRegistrationsThisMonth }}</b> registrations out of
out of <b>{{ endorserLimits.maxRegistrationsPerMonth }}</b> for this <b>{{ limits.maxRegistrationsPerMonth }}</b> for this month.
month.
<i <i
>(You can register nobody on your first day, and after that only one >(You can register nobody on your first day, and after that only one
a day in your first month.)</i a day in your first month.)</i
> >
Your registration counter resets at Your registration counter resets at
<b class="whitespace-nowrap"> <b class="whitespace-nowrap">
{{ readableDate(endorserLimits.nextMonthBeginDateTime) }} {{ readableTime(limits.nextMonthBeginDateTime) }}
</b> </b>
</p> </p>
<p class="mt-3 text-sm" v-if="!!imageLimits">
You have uploaded
<b>{{ imageLimits?.doneImagesThisWeek }} images</b> out of
<b>{{ imageLimits?.maxImagesPerWeek }}</b> for this week. Your image
counter resets at
<b class="whitespace-nowrap">{{
readableDate(imageLimits?.nextWeekBeginDateTime)
}}</b>
</p>
</div> </div>
<button <button
class="block float-right w-fit text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mt-2" class="block float-right w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2"
@click="checkLimits()" @click="checkLimits()"
> >
Recheck Limits Recheck Limits
@@ -224,18 +189,18 @@
</div> </div>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
<div class="mb-2 font-bold">Data Export</div> <div>Data Export</div>
<router-link <router-link
:to="{ name: 'seed-backup' }" :to="{ name: 'seed-backup' }"
v-if="activeDid" v-if="activeDid"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2 mt-2"
> >
Backup Identifier Seed Backup Identifier Seed
</router-link> </router-link>
<button <button
v-bind:class="computedStartDownloadLinkClassNames()" v-bind:class="computedStartDownloadLinkClassNames()"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="exportDatabase()" @click="exportDatabase()"
> >
Download Settings & Contacts Download Settings & Contacts
@@ -245,7 +210,7 @@
<a <a
ref="downloadLink" ref="downloadLink"
v-bind:class="computedDownloadLinkClassNames()" v-bind:class="computedDownloadLinkClassNames()"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6" class="block w-full text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md"
> >
If no download happened yet, click again here to download now. If no download happened yet, click again here to download now.
</a> </a>
@@ -326,7 +291,7 @@
<router-link <router-link
id="switch-identity-link" id="switch-identity-link"
:to="{ name: 'identity-switcher' }" :to="{ name: 'identity-switcher' }"
class="block w-fit text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2" class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
> >
Switch Identifier Switch Identifier
</router-link> </router-link>
@@ -334,7 +299,7 @@
<label <label
for="toggleShowAmounts" for="toggleShowAmounts"
class="flex items-center justify-between cursor-pointer my-4" class="flex items-center justify-between cursor-pointer my-4"
@click="toggleShowContactAmounts" @click="handleChange"
> >
<!-- label --> <!-- label -->
<span class="text-slate-500 text-sm font-bold">Contacts Display</span> <span class="text-slate-500 text-sm font-bold">Contacts Display</span>
@@ -474,34 +439,6 @@
{{ DEFAULT_PUSH_SERVER }} {{ DEFAULT_PUSH_SERVER }}
</span> </span>
<div class="mt-2">
<span class="text-slate-500 text-sm font-bold">Image Server URL</span>
&nbsp;
<span class="text-sm">{{ DEFAULT_IMAGE_API_SERVER }}</span>
</div>
<label
for="toggleShowShortcutBvc"
class="flex items-center justify-between cursor-pointer mt-4"
@click="toggleShowShortcutBvc"
>
<!-- label -->
<span class="text-slate-500 text-sm font-bold"
>Show BVC Shortcut on Home Page</span
>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="showShortcutBvc" 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="mt-4"> <div class="mt-4">
<h2 class="text-slate-500 text-sm font-bold"> <h2 class="text-slate-500 text-sm font-bold">
Contacts & Settings Database Contacts & Settings Database
@@ -512,7 +449,7 @@
<input type="file" @change="uploadFile" class="ml-2" /> <input type="file" @change="uploadFile" class="ml-2" />
<div v-if="showContactImport()"> <div v-if="showContactImport()">
<button <button
class="block text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6" class="block text-center text-md uppercase bg-blue-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="submitFile()" @click="submitFile()"
> >
Import Settings & Contacts Import Settings & Contacts
@@ -527,7 +464,7 @@
<button> <button>
<router-link <router-link
:to="{ name: 'statistics' }" :to="{ name: 'statistics' }"
class="block w-fit text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2" class="block w-fit 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 Global Animated History of Giving
</router-link> </router-link>
@@ -548,22 +485,22 @@ import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { import { AppString, DEFAULT_PUSH_SERVER } from "@/constants/app";
AppString,
DEFAULT_IMAGE_API_SERVER,
DEFAULT_PUSH_SERVER,
NotificationIface,
} 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";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
ErrorResponse,
EndorserRateLimits, // eslint-disable-next-line @typescript-eslint/no-var-requires
ImageRateLimits, const Buffer = require("buffer/").Buffer;
} from "@/libs/endorserServer";
import { Buffer } from "buffer/"; interface Notification {
group: string;
type: string;
title: string;
text: string;
}
interface IAccount { interface IAccount {
did: string; did: string;
@@ -574,24 +511,19 @@ interface IAccount {
const inputFileNameRef = ref<Blob>(); const inputFileNameRef = ref<Blob>();
@Component({ @Component({ components: { QuickNav, TopMessage } })
components: { QuickNav, TopMessage },
})
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
AppConstants = AppString; AppConstants = AppString;
DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER; DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER;
DEFAULT_IMAGE_API_SERVER = DEFAULT_IMAGE_API_SERVER;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
apiServerInput = ""; apiServerInput = "";
derivationPath = ""; derivationPath = "";
downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
endorserLimits: EndorserRateLimits | null = null;
givenName = ""; givenName = "";
imageLimits: ImageRateLimits | null = null;
isRegistered = false; isRegistered = false;
isSubscribed = false; isSubscribed = false;
notificationMaybeChanged = false; notificationMaybeChanged = false;
@@ -599,6 +531,7 @@ export default class AccountViewView extends Vue {
publicBase64 = ""; publicBase64 = "";
webPushServer = ""; webPushServer = "";
webPushServerInput = ""; webPushServerInput = "";
limits: RateLimits | null = null;
limitsMessage = ""; limitsMessage = "";
loadingLimits = false; loadingLimits = false;
showContactGives = false; showContactGives = false;
@@ -607,7 +540,6 @@ export default class AccountViewView extends Vue {
showB64Copy = false; showB64Copy = false;
showPubCopy = false; showPubCopy = false;
showAdvanced = false; showAdvanced = false;
showShortcutBvc = false;
subscription: PushSubscription | null = null; subscription: PushSubscription | null = null;
warnIfProdServer = false; warnIfProdServer = false;
warnIfTestServer = false; warnIfTestServer = false;
@@ -667,7 +599,6 @@ export default class AccountViewView extends Vue {
(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.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
this.showShortcutBvc = !!settings?.showShortcutBvc;
this.warnIfProdServer = !!settings?.warnIfProdServer; this.warnIfProdServer = !!settings?.warnIfProdServer;
this.warnIfTestServer = !!settings?.warnIfTestServer; this.warnIfTestServer = !!settings?.warnIfTestServer;
this.webPushServer = (settings?.webPushServer as string) || ""; this.webPushServer = (settings?.webPushServer as string) || "";
@@ -725,7 +656,7 @@ export default class AccountViewView extends Vue {
.then(() => setTimeout(fn, 2000)); .then(() => setTimeout(fn, 2000));
} }
toggleShowContactAmounts() { handleChange() {
this.showContactGives = !this.showContactGives; this.showContactGives = !this.showContactGives;
this.updateShowContactAmounts(); this.updateShowContactAmounts();
} }
@@ -740,12 +671,7 @@ export default class AccountViewView extends Vue {
this.updateWarnIfTestServer(this.warnIfTestServer); this.updateWarnIfTestServer(this.warnIfTestServer);
} }
toggleShowShortcutBvc() { readableTime(timeStr: string) {
this.showShortcutBvc = !this.showShortcutBvc;
this.updateShowShortcutBvc(this.showShortcutBvc);
}
readableDate(timeStr: string) {
return timeStr.substring(0, timeStr.indexOf("T")); return timeStr.substring(0, timeStr.indexOf("T"));
} }
@@ -840,7 +766,7 @@ export default class AccountViewView extends Vue {
-1, -1,
); );
console.error( console.error(
"Telling user to try again after contact-amounts setting update because:", "Telling user to try again after contact setting update because:",
err, err,
); );
} }
@@ -863,7 +789,7 @@ export default class AccountViewView extends Vue {
-1, -1,
); );
console.error( console.error(
"Telling user to try again after prod-server-warning setting update because:", "Telling user to try again after setting update because:",
err, err,
); );
} }
@@ -886,30 +812,7 @@ export default class AccountViewView extends Vue {
-1, -1,
); );
console.error( console.error(
"Telling user to try again after test-server-warning setting update because:", "Telling user to try again after setting update because:",
err,
);
}
}
public async updateShowShortcutBvc(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
showShortcutBvc: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating BVC Shortcut Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after BVC-shortcut setting update because:",
err, err,
); );
} }
@@ -931,11 +834,11 @@ export default class AccountViewView extends Vue {
// Trigger the download // Trigger the download
this.downloadDatabaseBackup(this.downloadUrl); this.downloadDatabaseBackup(this.downloadUrl);
// Revoke the temporary URL -- not yet because of DuckDuckGo download failure
//URL.revokeObjectURL(this.downloadUrl);
// Notify the user that the download has started // Notify the user that the download has started
this.notifyDownloadStarted(); this.notifyDownloadStarted();
// Revoke the temporary URL -- after a pause to avoid DuckDuckGo download failure
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} catch (error) { } catch (error) {
this.handleExportError(error); this.handleExportError(error);
} }
@@ -974,13 +877,13 @@ export default class AccountViewView extends Vue {
public computedStartDownloadLinkClassNames() { public computedStartDownloadLinkClassNames() {
return { return {
hidden: this.downloadUrl, invisible: this.downloadUrl,
}; };
} }
public computedDownloadLinkClassNames() { public computedDownloadLinkClassNames() {
return { return {
hidden: !this.downloadUrl, invisible: !this.downloadUrl,
}; };
} }
@@ -1086,11 +989,11 @@ export default class AccountViewView extends Vue {
this.limitsMessage = ""; this.limitsMessage = "";
try { try {
const resp = await this.fetchEndorserRateLimits(identity); const resp = await this.fetchRateLimits(identity);
if (resp.status === 200) { if (resp.status === 200) {
this.endorserLimits = resp.data; this.limits = resp.data;
if (!this.isRegistered) { if (!this.isRegistered) {
// the user was not known to be registered, but now they are (because we got no error) so let's record it // the user is not known to be registered, but they are so let's record it
try { try {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
@@ -1110,10 +1013,6 @@ export default class AccountViewView extends Vue {
); );
} }
} }
const imageResp = await this.fetchImageRateLimits(identity);
if (imageResp.status === 200) {
this.imageLimits = imageResp.data;
}
} }
} catch (error) { } catch (error) {
this.handleRateLimitsError(error); this.handleRateLimitsError(error);
@@ -1134,29 +1033,17 @@ export default class AccountViewView extends Vue {
} }
/** /**
* Fetches rate limits from the Endorser server. * Fetches rate limits from the server.
* *
* @param {IIdentifier} identity - The identity object to check rate limits for. * @param {IIdentifier} identity - The identity object to check rate limits for.
* @returns {Promise<AxiosResponse>} The Axios response object. * @returns {Promise<AxiosResponse>} The Axios response object.
*/ */
private async fetchEndorserRateLimits(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 } as AxiosRequestConfig); return await this.axios.get(url, { headers } as AxiosRequestConfig);
} }
/**
* Fetches rate limits from the image server.
*
* @param {IIdentifier} identity - The identity object to check rate limits for.
* @returns {Promise<AxiosResponse>} The Axios response object.
*/
private async fetchImageRateLimits(identity: IIdentifier) {
const url = DEFAULT_IMAGE_API_SERVER + "/image-limits";
const headers = await this.getHeaders(identity);
return await this.axios.get(url, { headers } as AxiosRequestConfig);
}
/** /**
* Handles errors that occur while fetching rate limits. * Handles errors that occur while fetching rate limits.
* *
@@ -1168,9 +1055,9 @@ export default class AccountViewView extends Vue {
this.limitsMessage = this.limitsMessage =
(data?.error?.message as string) || "Bad server response."; (data?.error?.message as string) || "Bad server response.";
console.error( console.error(
"Got bad response retrieving limits, which usually means user isn't registered.", "Got bad response retrieving limits, which usually means user isn't registered:",
error,
); );
//console.error(error);
} else { } else {
this.limitsMessage = "Got an error retrieving limits."; this.limitsMessage = "Got an error retrieving limits.";
console.error("Got some error retrieving limits:", error); console.error("Got some error retrieving limits:", error);

View File

@@ -123,7 +123,7 @@
<div class="columns-3"> <div class="columns-3">
<button <button
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" class="col-span-1 bg-blue-600 text-white px-4 py-2 rounded-md"
v-if=" v-if="
libsUtil.isGiveRecordTheUserCanConfirm( libsUtil.isGiveRecordTheUserCanConfirm(
veriClaim, veriClaim,
@@ -140,7 +140,7 @@
<button <button
v-if="libsUtil.canFulfillOffer(veriClaim)" v-if="libsUtil.canFulfillOffer(veriClaim)"
@click="openFulfillGiftDialog()" @click="openFulfillGiftDialog()"
class="col-span-1 block w-fit text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" class="col-span-1 block w-fit text-center text-md bg-blue-600 text-white px-1.5 py-2 rounded-md"
> >
Affirm Delivery Affirm Delivery
<fa icon="hand-holding-heart" class="ml-2 text-white cursor-pointer" /> <fa icon="hand-holding-heart" class="ml-2 text-white cursor-pointer" />
@@ -377,7 +377,7 @@
</p> </p>
<button <button
v-else v-else
class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2" 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 as string)" @click="showFullClaim(veriClaim.id as string)"
> >
Load Full Claim Details Load Full Claim Details
@@ -390,7 +390,7 @@
<a <a
:href="apiServer + '/api/claim/' + veriClaim.id" :href="apiServer + '/api/claim/' + veriClaim.id"
target="_blank" target="_blank"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2" 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 View on the Public Server
</a> </a>
@@ -407,7 +407,6 @@ import { useClipboard } from "@vueuse/core";
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 { NotificationIface } from "@/constants/app";
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";
@@ -419,11 +418,18 @@ import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { GiverInputInfo } from "@/libs/endorserServer"; import { GiverInputInfo } from "@/libs/endorserServer";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav }, components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav },
}) })
export default class ClaimView extends Vue { export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
accountIdentityStr: string = "null"; accountIdentityStr: string = "null";
activeDid = ""; activeDid = "";
@@ -730,7 +736,10 @@ export default class ClaimView extends Vue {
), ),
), ),
); );
const confirmationClaim: serverUtil.GenericVerifiableCredential = { const confirmationClaim: serverUtil.GenericVerifiableCredential & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: any;
} = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "AgreeAction", "@type": "AgreeAction",
object: goodClaim, object: goodClaim,

View File

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

View File

@@ -105,33 +105,39 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import { Component, Vue } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
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 } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import { import {
AgreeVerifiableCredential, AgreeVerifiableCredential,
GiveSummaryRecord, GiveServerRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
SCHEMA_ORG_CONTEXT, SCHEMA_ORG_CONTEXT,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as didJwt from "did-jwt";
import { AxiosError } from "axios";
import QuickNav from "@/components/QuickNav.vue";
import { IIdentifier } from "@veramo/core";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class ContactAmountssView extends Vue { export default class ContactAmountssView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
contact: Contact | null = null; contact: Contact | null = null;
giveRecords: Array<GiveSummaryRecord> = []; giveRecords: Array<GiveServerRecord> = [];
numAccounts = 0; numAccounts = 0;
async beforeCreate() { async beforeCreate() {
@@ -179,7 +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.error("Error retrieving settings or gives.", err); console.log("Error retrieving settings or gives.", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -197,7 +203,7 @@ export default class ContactAmountssView extends Vue {
async loadGives(activeDid: string, contact: Contact) { async loadGives(activeDid: string, contact: Contact) {
try { try {
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
let result: Array<GiveSummaryRecord> = []; let result: Array<GiveServerRecord> = [];
const url = const url =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
@@ -252,7 +258,7 @@ export default class ContactAmountssView extends Vue {
); );
} }
const sortedResult: Array<GiveSummaryRecord> = R.sort( const sortedResult: Array<GiveServerRecord> = R.sort(
(a, b) => (a, b) =>
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(), new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
result, result,
@@ -271,7 +277,7 @@ export default class ContactAmountssView extends Vue {
} }
} }
async confirm(record: GiveSummaryRecord) { async confirm(record: GiveServerRecord) {
// Make claim // Make claim
// I use clone here because otherwise it gets a Proxy object. // I use clone here because otherwise it gets a Proxy object.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -32,7 +32,7 @@
<button <button
type="button" type="button"
@click="openDialog()" @click="openDialog()"
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md" class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md"
> >
<fa icon="gift" class="fa-fw"></fa> <fa icon="gift" class="fa-fw"></fa>
</button> </button>
@@ -57,7 +57,7 @@
<button <button
type="button" type="button"
@click="openDialog(contact)" @click="openDialog(contact)"
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md" class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md"
> >
<fa icon="gift" class="fa-fw"></fa> <fa icon="gift" class="fa-fw"></fa>
</button> </button>
@@ -76,24 +76,29 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { IIdentifier } from "@veramo/core";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account, AccountsSchema } from "@/db/tables/accounts"; import { Account, AccountsSchema } from "@/db/tables/accounts";
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 { GiverInputInfo } from "@/libs/endorserServer"; import { GiverInputInfo } from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { IIdentifier } from "@veramo/core";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { GiftedDialog, QuickNav, EntityIcon }, components: { GiftedDialog, QuickNav, EntityIcon },
}) })
export default class ContactGiftingView extends Vue { export default class ContactGiftingView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@@ -113,13 +118,13 @@ export default class ContactGiftingView 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.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.orderBy("name").toArray(); this.allContacts = await db.contacts.toArray();
localStorage.removeItem("projectId"); localStorage.removeItem("projectId");
// 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.error("Error retrieving settings & contacts:", err); console.log("Error retrieving settings & contacts:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

View File

@@ -18,22 +18,19 @@
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
Your Contact Info Your Contact Info
</h1> </h1>
<p <p v-if="!givenName" class="text-center mt-2">
v-if="!givenName"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<span class="text-red">Beware!</span> <span class="text-red">Beware!</span>
You aren't sharing your name, so quickly You aren't sharing your name, so hurry and
<router-link <router-link
:to="{ name: 'new-edit-account' }" :to="{ name: 'new-edit-account' }"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md" class="bg-blue-500 text-white px-1.5 py-1 rounded-md"
> >
click here to set it for them. go here to set it for them.
</router-link> </router-link>
</p> </p>
</div> </div>
<div @click="onCopyToClipboard()" v-if="activeDid" class="text-center"> <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
@@ -44,7 +41,9 @@
:dotsOptions="{ type: 'square' }" :dotsOptions="{ type: 'square' }"
class="flex justify-center" class="flex justify-center"
/> />
<span> Click QR to copy your contact URL to your clipboard. </span> <span class="flex justify-center">
Click QR to copy your contact URL to your clipboard.
</span>
</div> </div>
<div class="text-center" v-else> <div class="text-center" v-else>
You have no identitifiers yet, so You have no identitifiers yet, so
@@ -78,7 +77,6 @@ import { Component, Vue } from "vue-facing-decorator";
import { QrcodeStream } from "vue-qrcode-reader"; import { QrcodeStream } from "vue-qrcode-reader";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, nextDerivationPath, SimpleSigner } from "@/libs/crypto"; import { deriveAddress, nextDerivationPath, SimpleSigner } from "@/libs/crypto";
@@ -88,7 +86,16 @@ import {
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
ENDORSER_JWT_URL_LOCATION, ENDORSER_JWT_URL_LOCATION,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Buffer } from "buffer/";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { components: {
@@ -98,7 +105,7 @@ import { Buffer } from "buffer/";
}, },
}) })
export default class ContactQRScanShow extends Vue { export default class ContactQRScanShow extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
@@ -178,6 +185,7 @@ export default class ContactQRScanShow extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanDetect(content: any) { onScanDetect(content: any) {
if (content[0]?.rawValue) { if (content[0]?.rawValue) {
//console.log("onDetect", content[0].rawValue);
localStorage.setItem("contactEndorserUrl", content[0].rawValue); localStorage.setItem("contactEndorserUrl", content[0].rawValue);
this.$router.push({ name: "contacts" }); this.$router.push({ name: "contacts" });
} else { } else {
@@ -195,7 +203,7 @@ export default class ContactQRScanShow extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanError(error: any) { onScanError(error: any) {
console.error("Scan was invalid:", error); console.log("Scan was invalid:", error);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

View File

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

View File

@@ -10,9 +10,8 @@
<span /> <span />
<span> <span>
<a <a
href="/help-onboarding" @click="showHintsForOnboarding()"
target="_blank" class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1"
> >
Onboarding Guide Onboarding Guide
</a> </a>
@@ -23,7 +22,7 @@
<div class="mt-4 mb-4 flex items-stretch"> <div class="mt-4 mb-4 flex items-stretch">
<router-link <router-link
:to="{ name: 'contact-qr' }" :to="{ name: 'contact-qr' }"
class="flex items-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md" class="flex items-center bg-slate-500 text-white px-1.5 py-1 mr-1 rounded-md"
> >
<fa icon="qrcode" class="fa-fw text-2xl" /> <fa icon="qrcode" class="fa-fw text-2xl" />
</router-link> </router-link>
@@ -76,7 +75,7 @@
<br /> <br />
(Only most recent hours included. To see more, click (Only most recent hours included. To see more, click
<span <span
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md" class="text-sm uppercase bg-slate-500 text-white px-1 py-1 rounded-md"
> >
<fa icon="file-lines" class="fa-fw" /> <fa icon="file-lines" class="fa-fw" />
</span> </span>
@@ -101,7 +100,7 @@
></EntityIcon> ></EntityIcon>
{{ contact.name || AppString.NO_CONTACT_NAME }} {{ contact.name || AppString.NO_CONTACT_NAME }}
<button <button
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md" class="text-sm uppercase bg-slate-500 text-white px-1 rounded-md"
@click=" @click="
contactEdit = contact; contactEdit = contact;
contactNewName = contact.name; contactNewName = contact.name;
@@ -138,7 +137,7 @@
<div v-if="activeDid"> <div v-if="activeDid">
<button <button
v-if="contact.seesMe" v-if="contact.seesMe"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md" class="text-sm uppercase bg-slate-500 text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="setVisibility(contact, false, true)" @click="setVisibility(contact, false, true)"
title="They can see you" title="They can see you"
> >
@@ -146,14 +145,14 @@
</button> </button>
<button <button
v-else v-else
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md" class="text-sm uppercase bg-slate-500 text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="setVisibility(contact, true, true)" @click="setVisibility(contact, true, true)"
title="They cannot see you" title="They cannot see you"
> >
<fa icon="eye-slash" class="fa-fw" /> <fa icon="eye-slash" class="fa-fw" />
</button> </button>
<button <button
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md" class="text-sm uppercase bg-slate-500 text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="checkVisibility(contact)" @click="checkVisibility(contact)"
title="Check Visibility" title="Check Visibility"
v-if="activeDid" v-if="activeDid"
@@ -162,7 +161,7 @@
</button> </button>
<button <button
@click="register(contact)" @click="register(contact)"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 px-2 py-1.5 rounded-md" class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
v-if="activeDid" v-if="activeDid"
title="Registration" title="Registration"
> >
@@ -177,7 +176,7 @@
<button <button
@click="deleteContact(contact)" @click="deleteContact(contact)"
class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-24 px-2 py-1.5 rounded-md" class="text-sm uppercase bg-red-600 text-white ml-24 px-2 py-1.5 rounded-md"
title="Delete" title="Delete"
> >
<fa icon="trash-can" class="fa-fw" /> <fa icon="trash-can" class="fa-fw" />
@@ -188,7 +187,7 @@
class="ml-auto flex gap-1.5" class="ml-auto flex gap-1.5"
> >
<button <button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-l-md" class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-md"
@click="onClickAddGive(activeDid, contact.did)" @click="onClickAddGive(activeDid, contact.did)"
:title="givenByMeDescriptions[contact.did] || ''" :title="givenByMeDescriptions[contact.did] || ''"
> >
@@ -230,7 +229,7 @@
name: 'contact-amounts', name: 'contact-amounts',
query: { contactDid: contact.did }, query: { contactDid: contact.did },
}" }"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md" class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
title="See more given activity" title="See more given activity"
> >
<fa icon="file-lines" class="fa-fw" /> <fa icon="file-lines" class="fa-fw" />
@@ -285,7 +284,6 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { IndexableType } from "dexie";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
@@ -303,7 +301,7 @@ import {
import { import {
CONTACT_CSV_HEADER, CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
GiveSummaryRecord, GiveServerRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
isDid, isDid,
RegisterVerifiableCredential, RegisterVerifiableCredential,
@@ -313,8 +311,10 @@ import * as libsUtil from "@/libs/util";
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 { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { IndexableType } from "dexie";
import { Buffer } from "buffer/"; // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
@Component({ @Component({
components: { QuickNav, EntityIcon }, components: { QuickNav, EntityIcon },
@@ -364,7 +364,11 @@ export default class ContactsView extends Vue {
if (this.showGiveNumbers) { if (this.showGiveNumbers) {
this.loadGives(); this.loadGives();
} }
this.contacts = await db.contacts.orderBy("name").toArray(); const allContacts = await db.contacts.toArray();
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts,
);
if (this.contactEndorserUrl) { if (this.contactEndorserUrl) {
await this.addContactFromScan(this.contactEndorserUrl); await this.addContactFromScan(this.contactEndorserUrl);
@@ -409,7 +413,7 @@ export default class ContactsView extends Vue {
} }
const handleResponse = ( const handleResponse = (
resp: { status: number; data: { data: GiveSummaryRecord[] } }, resp: { status: number; data: { data: GiveServerRecord[] } },
descriptions: Record<string, string>, descriptions: Record<string, string>,
confirmed: Record<string, number>, confirmed: Record<string, number>,
unconfirmed: Record<string, number>, unconfirmed: Record<string, number>,
@@ -497,7 +501,7 @@ export default class ContactsView extends Vue {
this.givenToMeConfirmed = givenToMeConfirmed; this.givenToMeConfirmed = givenToMeConfirmed;
this.givenToMeUnconfirmed = givenToMeUnconfirmed; this.givenToMeUnconfirmed = givenToMeUnconfirmed;
} catch (error) { } catch (error) {
console.error("Error loading gives", error); console.log("Error loading gives", error);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -510,6 +514,18 @@ export default class ContactsView extends Vue {
} }
} }
showHintsForOnboarding() {
this.$notify(
{
group: "alert",
type: "info",
title: "Onboard Someone",
text: libsUtil.ONBOARD_MESSAGE,
},
-1,
);
}
async onClickNewContact(): Promise<void> { async onClickNewContact(): Promise<void> {
if (!this.contactInput) { if (!this.contactInput) {
this.$notify( this.$notify(
@@ -519,7 +535,7 @@ export default class ContactsView extends Vue {
title: "No Contact", title: "No Contact",
text: "There was no contact info to add.", text: "There was no contact info to add.",
}, },
3000, -1,
); );
return; return;
} }
@@ -547,7 +563,7 @@ export default class ContactsView extends Vue {
title: "Contacts Added", title: "Contacts Added",
text: "Each contact was added. Nothing was sent to the server.", text: "Each contact was added. Nothing was sent to the server.",
}, },
3000, // keeping it up so that the "visibility" message is seen -1, // keeping it up so that the "visibility" message is seen
); );
} catch (e) { } catch (e) {
this.$notify( this.$notify(
@@ -560,7 +576,11 @@ export default class ContactsView extends Vue {
-1, -1,
); );
} }
this.contacts = await db.contacts.orderBy("name").toArray(); const allContacts = await db.contacts.toArray();
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts,
);
return; return;
} }
@@ -652,7 +672,7 @@ export default class ContactsView extends Vue {
title: "No Contact Info", title: "No Contact Info",
text: "The contact info could not be parsed.", text: "The contact info could not be parsed.",
}, },
3000, -1,
); );
return; return;
} else { } else {
@@ -674,7 +694,7 @@ export default class ContactsView extends Vue {
title: "Incomplete Contact", title: "Incomplete Contact",
text: "Cannot add a contact without a DID.", text: "Cannot add a contact without a DID.",
}, },
5000, -1,
); );
return; return;
} }
@@ -686,7 +706,7 @@ export default class ContactsView extends Vue {
title: "Invalid DID", title: "Invalid DID",
text: "The DID is not valid. It must begin with 'did:'", text: "The DID is not valid. It must begin with 'did:'",
}, },
5000, -1,
); );
return; return;
} }
@@ -725,7 +745,7 @@ export default class ContactsView extends Vue {
title: "Contact Added", title: "Contact Added",
text: addedMessage, text: addedMessage,
}, },
3000, -1, // keeping it up so that the "visibility" message is seen
); );
}) })
.catch((err) => { .catch((err) => {
@@ -841,7 +861,7 @@ export default class ContactsView extends Vue {
title: "Registration Still Unknown", title: "Registration Still Unknown",
text: message, text: message,
}, },
5000, -1,
); );
} else if (resp.data?.success?.handleId) { } else if (resp.data?.success?.handleId) {
contact.registered = true; contact.registered = true;
@@ -856,7 +876,7 @@ export default class ContactsView extends Vue {
(contact.name || "That unnamed person") + (contact.name || "That unnamed person") +
" has been registered.", " has been registered.",
}, },
5000, -1,
); );
} }
} catch (error) { } catch (error) {
@@ -880,7 +900,7 @@ export default class ContactsView extends Vue {
title: "Registration Error", title: "Registration Error",
text: userMessage, text: userMessage,
}, },
5000, -1,
); );
} }
} }
@@ -921,7 +941,7 @@ export default class ContactsView extends Vue {
(visibility ? "" : "not ") + (visibility ? "" : "not ") +
"see your activity.", "see your activity.",
}, },
3000, -1,
); );
} }
contact.seesMe = visibility; contact.seesMe = visibility;
@@ -941,7 +961,7 @@ export default class ContactsView extends Vue {
title: "Error Setting Visibility", title: "Error Setting Visibility",
text: message, text: message,
}, },
5000, -1,
); );
} }
} catch (err) { } catch (err) {
@@ -953,7 +973,7 @@ export default class ContactsView extends Vue {
title: "Error Setting Visibility", title: "Error Setting Visibility",
text: "Check connectivity and try again.", text: "Check connectivity and try again.",
}, },
5000, -1,
); );
} }
} }
@@ -985,10 +1005,10 @@ export default class ContactsView extends Vue {
(visibility ? "" : "not ") + (visibility ? "" : "not ") +
"see your activity.", "see your activity.",
}, },
3000, -1,
); );
} else { } else {
console.error("Got bad server response checking visibility:", resp); console.log("Got bad server response when checking visibility: ", resp);
const message = resp.data.error?.message || "Got bad server response."; const message = resp.data.error?.message || "Got bad server response.";
this.$notify( this.$notify(
{ {
@@ -997,11 +1017,11 @@ export default class ContactsView extends Vue {
title: "Error Checking Visibility", title: "Error Checking Visibility",
text: message, text: message,
}, },
5000, -1,
); );
} }
} catch (err) { } catch (err) {
console.error("Caught error from request to check visibility:", err); console.log("Caught error from request to check visibility:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -1009,11 +1029,17 @@ export default class ContactsView extends Vue {
title: "Error Checking Visibility", title: "Error Checking Visibility",
text: "Check connectivity and try again.", text: "Check connectivity and try again.",
}, },
3000, -1,
); );
} }
} }
// from https://stackoverflow.com/a/175787/845494
//
private isNumeric(str: string): boolean {
return !isNaN(+str);
}
private nameForDid(contacts: Array<Contact>, did: string): string { private nameForDid(contacts: Array<Contact>, did: string): string {
const contact = R.find((con) => con.did == did, contacts); const contact = R.find((con) => con.did == did, contacts);
return this.nameForContact(contact); return this.nameForContact(contact);
@@ -1049,7 +1075,7 @@ export default class ContactsView extends Vue {
return; return;
} }
} }
if (!libsUtil.isNumeric(this.hourInput)) { if (!this.isNumeric(this.hourInput)) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -1057,7 +1083,7 @@ export default class ContactsView extends Vue {
title: "Input Error", title: "Input Error",
text: "This is not a valid number of hours: " + this.hourInput, text: "This is not a valid number of hours: " + this.hourInput,
}, },
3000, -1,
); );
} else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) { } else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) {
this.$notify( this.$notify(
@@ -1067,7 +1093,7 @@ export default class ContactsView extends Vue {
title: "Input Error", title: "Input Error",
text: "Giving no hours or description does nothing.", text: "Giving no hours or description does nothing.",
}, },
3000, -1,
); );
} else if (!identity) { } else if (!identity) {
this.$notify( this.$notify(
@@ -1077,7 +1103,7 @@ export default class ContactsView extends Vue {
title: "Status Error", title: "Status Error",
text: "No identifier is available.", text: "No identifier is available.",
}, },
3000, -1,
); );
} else { } else {
// ask to confirm amount // ask to confirm amount
@@ -1172,7 +1198,7 @@ export default class ContactsView extends Vue {
title: "Done", title: "Done",
text: "Successfully logged time to the server.", text: "Successfully logged time to the server.",
}, },
5000, -1,
); );
if (fromDid === identity.did) { if (fromDid === identity.did) {
@@ -1186,7 +1212,7 @@ export default class ContactsView extends Vue {
} }
} }
} catch (error) { } catch (error) {
console.error("Error in createAndSubmitContactGive: ", error); console.log("Error in createAndSubmitContactGive: ", error);
let userMessage = "There was an error. See logs for more info."; let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError; const serverError = error as AxiosError;
if (serverError) { if (serverError) {
@@ -1206,7 +1232,7 @@ export default class ContactsView extends Vue {
title: "Error Sending Give", title: "Error Sending Give",
text: userMessage, text: userMessage,
}, },
5000, -1,
); );
} }
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<QuickNav selected="Discover"></QuickNav> <QuickNav selected="Discover" />
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
@@ -28,6 +28,20 @@
<!-- Result Tabs --> <!-- Result Tabs -->
<div class="text-center text-slate-500 border-b border-slate-300"> <div class="text-center text-slate-500 border-b border-slate-300">
<ul class="flex flex-wrap justify-center gap-4 -mb-px"> <ul class="flex flex-wrap justify-center gap-4 -mb-px">
<li>
<a
href="#"
@click="
projects = [];
isRemoteActive = false;
isLocalActive = false;
isStarActive = true;
"
v-bind:class="computedStarTabClassNames()"
>
<fa icon="star" class="fa-fw" />
</a>
</li>
<li> <li>
<a <a
href="#" href="#"
@@ -35,6 +49,7 @@
projects = []; projects = [];
isLocalActive = true; isLocalActive = true;
isRemoteActive = false; isRemoteActive = false;
isStarActive = false;
searchLocal(); searchLocal();
" "
v-bind:class="computedLocalTabClassNames()" v-bind:class="computedLocalTabClassNames()"
@@ -55,6 +70,7 @@
projects = []; projects = [];
isRemoteActive = true; isRemoteActive = true;
isLocalActive = false; isLocalActive = false;
isStarActive = false;
searchAll(); searchAll();
" "
v-bind:class="computedRemoteTabClassNames()" v-bind:class="computedRemoteTabClassNames()"
@@ -71,8 +87,11 @@
</ul> </ul>
</div> </div>
<div v-if="isStarActive && projects.length == 0">
<div class="mt-4 text-center">You have not starred any projects.</div>
</div>
<div v-if="isLocalActive"> <div v-if="isLocalActive">
<div> <div class="mt-2 text-center">
<button <button
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500" class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="$router.push({ name: 'search-area' })" @click="$router.push({ name: 'search-area' })"
@@ -129,17 +148,28 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import {
BoundingBox,
DEFAULT_SETTINGS,
MASTER_SETTINGS_KEY,
Settings,
} from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { didInfo, PlanData, PlanServerRecord } 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 ProjectIcon from "@/components/ProjectIcon.vue"; import ProjectIcon from "@/components/ProjectIcon.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; interface Notification {
import { Contact } from "@/db/tables/contacts"; group: string;
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; type: string;
import { accessToken } from "@/libs/crypto"; title: string;
import { didInfo, PlanData } from "@/libs/endorserServer"; text: string;
}
@Component({ @Component({
components: { components: {
@@ -151,7 +181,7 @@ import { didInfo, PlanData } from "@/libs/endorserServer";
}, },
}) })
export default class DiscoverView extends Vue { export default class DiscoverView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@@ -162,19 +192,23 @@ export default class DiscoverView extends Vue {
isLoading = false; isLoading = false;
isLocalActive = true; isLocalActive = true;
isRemoteActive = false; isRemoteActive = false;
isStarActive = false;
localCount = -1; localCount = -1;
remoteCount = -1; remoteCount = -1;
searchBox: { name: string; bbox: BoundingBox } | null = null; searchBox: { name: string; bbox: BoundingBox } | null = null;
starredProjects: Array<string> = [];
// make this function available to the Vue template // make this function available to the Vue template
didInfo = didInfo; didInfo = didInfo;
async mounted() { async mounted() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings: Settings =
(await db.settings.get(MASTER_SETTINGS_KEY)) || DEFAULT_SETTINGS;
this.activeDid = (settings?.activeDid as string) || ""; this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || ""; this.apiServer = (settings?.apiServer as string) || "";
this.searchBox = settings?.searchBoxes?.[0] || null; this.searchBox = settings?.searchBoxes?.[0] || null;
this.starredProjects = settings?.starredProjects || [];
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
@@ -182,7 +216,9 @@ export default class DiscoverView extends Vue {
const allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did); this.allMyDids = allAccounts.map((acc) => acc.did);
if (this.searchBox) { if (this.starredProjects.length > 0) {
await this.searchStars();
} else if (this.searchBox) {
await this.searchLocal(); await this.searchLocal();
} else { } else {
this.isLocalActive = false; this.isLocalActive = false;
@@ -197,7 +233,9 @@ export default class DiscoverView extends Vue {
} }
public async searchSelected() { public async searchSelected() {
if (this.isLocalActive) { if (this.isStarActive) {
await this.searchStars();
} else if (this.isLocalActive) {
await this.searchLocal(); await this.searchLocal();
} else { } else {
await this.searchAll(); await this.searchAll();
@@ -254,7 +292,7 @@ export default class DiscoverView extends Vue {
if (response.status !== 200) { if (response.status !== 200) {
const details = await response.text(); const details = await response.text();
console.error("Problem with full search:", details); console.log("Problem with full search:", details);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -282,7 +320,7 @@ export default class DiscoverView extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) { } catch (e: any) {
console.error("Error with feed load:", e); console.log("Error with feed load:", e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -337,7 +375,7 @@ export default class DiscoverView extends Vue {
if (response.status !== 200) { if (response.status !== 200) {
const details = await response.text(); const details = await response.text();
console.error("Problem with nearby search:", details); console.log("Problem with nearby search:", details);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -374,7 +412,72 @@ export default class DiscoverView extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) { } catch (e: any) {
console.error("Error with feed load:", e); console.log("Error with feed load:", e);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: e.userMessage || "There was a problem retrieving projects.",
},
-1,
);
} finally {
this.isLoading = false;
}
}
public async searchStars() {
this.resetCounts();
if (this.starredProjects.length == 0) {
this.projects = [];
return;
}
try {
this.isLoading = true;
const response = await fetch(
this.apiServer +
"/api/v2/report/plans?handleIds=" +
encodeURIComponent(JSON.stringify(this.starredProjects)),
{
method: "GET",
headers: await this.buildHeaders(),
},
);
if (response.status !== 200) {
const details = await response.text();
console.log("Problem with full search:", details);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `There was a problem accessing the server. Try again later.`,
},
-1,
);
throw details;
}
const results = await response.json();
const plans: PlanServerRecord[] = results.data;
if (plans) {
for (const plan of plans) {
const { name, description, handleId, issuerDid } = plan;
this.projects.push({ name, description, handleId, issuerDid });
}
this.remoteCount = this.projects.length;
} else {
throw JSON.stringify(results);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
console.log("Error with feed load:", e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -416,6 +519,24 @@ export default class DiscoverView extends Vue {
this.$router.push(route); this.$router.push(route);
} }
public computedStarTabClassNames() {
return {
"inline-block": true,
"py-3": true,
"rounded-t-lg": true,
"border-b-2": true,
active: this.isStarActive,
"text-black": this.isStarActive,
"border-black": this.isStarActive,
"font-semibold": this.isStarActive,
"text-blue-600": !this.isStarActive,
"border-transparent": !this.isStarActive,
"hover:border-slate-400": !this.isStarActive,
};
}
public computedLocalTabClassNames() { public computedLocalTabClassNames() {
return { return {
"inline-block": true, "inline-block": true,

View File

@@ -1,459 +0,0 @@
<template>
<QuickNav />
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- 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="cancel()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1>
<h1 class="text-xl font-bold text-center mb-4">
{{ message }} {{ giverName || "somebody not named" }}
</h1>
<textarea
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received"
v-model="description"
/>
<div class="flex flex-row justify-center">
<span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
@click="changeUnitCode()"
>
{{ libsUtil.UNIT_SHORT[unitCode] }}
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<fa icon="chevron-left" />
</div>
<input
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
v-model="amountInput"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<fa icon="chevron-right" />
</div>
</div>
<div class="flex justify-center mt-4">
<span v-if="imageUrl" class="flex justify-between">
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
<img :src="imageUrl" class="h-24 rounded-xl" />
</a>
<fa
icon="trash-can"
@click="confirmDeleteImage"
class="text-red-500 fa-fw ml-8 mt-10"
/>
</span>
<span v-else>
<fa
icon="camera"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
@click="openPhotoDialog"
/>
</span>
</div>
<GiftedPhotoDialog ref="photoDialog" />
<div v-if="projectId" class="mt-4">
<fa
icon="check"
class="bg-slate-500 text-white h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
/>
<label class="text-sm">This is given to a project</label>
</div>
<div v-if="!projectId" class="mt-4">
<input type="checkbox" class="h-6 w-6 mr-2" v-model="givenToUser" />
<label class="text-sm">Given to you</label>
</div>
<div class="mt-4">
<input type="checkbox" class="h-6 w-6 mr-2" v-model="isTrade" />
<label class="text-sm">Trade (not a gift)</label>
</div>
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<fa
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
/>
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="cancel"
>
Cancel
</button>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { createAndSubmitGive } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { accessToken } from "@/libs/crypto";
import GiftedDialog from "@/components/GiftedDialog.vue";
import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue";
@Component({
components: {
GiftedDialog,
GiftedPhotoDialog,
QuickNav,
TopMessage,
},
})
export default class GiftedDetails extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
apiServer = "";
amountInput = "0";
description = "";
givenToUser = false;
giverDid: string | undefined;
giverName = "";
imageUrl = "";
isTrade = false;
message = "";
offerId = "";
projectId = "";
unitCode = "HUR";
libsUtil = libsUtil;
async mounted() {
this.amountInput = this.$route.query.amountInput as string;
this.description = this.$route.query.description as string;
this.giverDid = this.$route.query.giverDid as string;
this.giverName = this.$route.query.giverName as string;
this.message = this.$route.query.message as string;
this.offerId = this.$route.query.offerId as string;
this.projectId = this.$route.query.projectId as string;
this.unitCode = this.$route.query.unitCode as string;
this.imageUrl = localStorage.getItem("imageUrl") || "";
this.givenToUser = !this.projectId;
try {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("Error retrieving settings from database:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: err.message || "There was an error retrieving your settings.",
},
-1,
);
}
}
changeUnitCode() {
const units = Object.keys(this.libsUtil.UNIT_SHORT);
const index = units.indexOf(this.unitCode);
this.unitCode = units[(index + 1) % units.length];
}
increment() {
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
}
decrement() {
this.amountInput = `${Math.max(
0,
(parseFloat(this.amountInput) || 1) - 1,
)}`;
}
cancel() {
this.deleteImage(); // not awaiting, so they'll go back immediately
this.$router.back();
}
openPhotoDialog() {
(this.$refs.photoDialog as GiftedPhotoDialog).open((imgUrl) => {
this.imageUrl = imgUrl;
});
}
confirmDeleteImage() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Are you sure you want to delete the image?",
text: "",
onYes: this.deleteImage,
},
-1,
);
}
async deleteImage() {
if (!this.imageUrl) {
return;
}
try {
const identity = await libsUtil.getIdentity(this.activeDid);
const token = await accessToken(identity);
const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER +
"/image/" +
encodeURIComponent(this.imageUrl),
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (response.status === 204) {
// don't bother with a notification
// (either they'll simply continue or they're canceling and going back)
} else {
console.error("Non-success deleting image:", response);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem deleting the image.",
},
5000,
);
// keep the imageUrl in localStorage so the user can try again if they want
return;
}
localStorage.removeItem("imageUrl");
this.imageUrl = "";
} catch (error) {
console.error("Error deleting image:", error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((error as any).response.status === 404) {
console.log("The image was already deleted:", error);
localStorage.removeItem("imageUrl");
this.imageUrl = "";
// it already doesn't exist so we won't say anything to the user
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error deleting the image.",
},
5000,
);
}
}
}
async confirm() {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identifier before you can record a give.",
},
2000,
);
return;
}
if (parseFloat(this.amountInput) < 0) {
this.$notify(
{
group: "alert",
type: "danger",
text: "You may not send a negative number.",
title: "",
},
2000,
);
return;
}
if (!this.description && !parseFloat(this.amountInput)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `You must enter a description or some number of ${
this.libsUtil.UNIT_LONG[this.unitCode]
}.`,
},
2000,
);
return;
}
this.$notify(
{
group: "alert",
type: "toast",
text: "Recording the give...",
title: "",
},
1000,
);
// this is asynchronous, but we don't need to wait for it to complete
await this.recordGive();
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param amountInput may be 0
* @param unitCode may be omitted, defaults to "HUR"
*/
public async recordGive() {
try {
const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
this.giverDid,
this.givenToUser ? this.activeDid : undefined,
this.description,
parseFloat(this.amountInput),
this.unitCode,
this.projectId,
this.offerId,
this.isTrade,
this.imageUrl,
);
if (
result.type === "error" ||
this.isGiveCreationError(result.response)
) {
const errorMessage = this.getGiveCreationErrorMessage(result);
console.error("Error with give creation result:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: errorMessage || "There was an error creating the give.",
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: `That ${this.isTrade ? "trade" : "gift"} was recorded.`,
},
5000,
);
localStorage.removeItem("imageUrl");
this.$router.back();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error with give recordation caught:", error);
const message =
error.userMessage ||
error.response?.data?.error?.message ||
"There was an error recording the give.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: message,
},
-1,
);
}
}
// Helper functions for readability
/**
* @param result response "data" from the server
* @returns true if the result indicates an error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isGiveCreationError(result: any) {
return result.status !== 201 || result.data?.error;
}
/**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getGiveCreationErrorMessage(result: any) {
return (
result.error?.userMessage ||
result.error?.error ||
result.response?.data?.error?.message
);
}
explainData() {
this.$notify(
{
group: "alert",
type: "success",
title: "Data Sharing",
text: libsUtil.PRIVACY_MESSAGE,
},
-1,
);
}
}
</script>

View File

@@ -31,7 +31,7 @@
If this works then you're all set. If this works then you're all set.
<button <button
@click="sendTestWebPushMessage(true)" @click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2" class="block w-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 Send Yourself a Test Web Push Message (Through Push Server but
Skipping Client Filter) Skipping Client Filter)
@@ -233,7 +233,7 @@
<h2 class="text-xl font-semibold mt-4">Tests</h2> <h2 class="text-xl font-semibold mt-4">Tests</h2>
<button <button
@click="showTestNotification()" @click="showTestNotification()"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4 mb-2" class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
> >
Send Test Notification Directly to Device (Not Through Push Server) Send Test Notification Directly to Device (Not Through Push Server)
</button> </button>
@@ -246,7 +246,7 @@
<button <button
@click="alertWebPushSubscription()" @click="alertWebPushSubscription()"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4 mb-2" class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
> >
Show Web Push Subscription Info Show Web Push Subscription Info
</button> </button>
@@ -259,7 +259,7 @@
<button <button
@click="sendTestWebPushMessage(true)" @click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4 mb-2" class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
> >
Send Yourself a Test Web Push Message (Through Push Server but Skipping Send Yourself a Test Web Push Message (Through Push Server but Skipping
Client Filter) Client Filter)
@@ -272,7 +272,7 @@
<button <button
@click="sendTestWebPushMessage()" @click="sendTestWebPushMessage()"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4 mb-2" class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
> >
Send Yourself a Test Web Push Message (Through Push Server and Client Send Yourself a Test Web Push Message (Through Push Server and Client
Filter) Filter)
@@ -294,20 +294,25 @@
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 { NotificationIface } from "@/constants/app";
import { sendTestThroughPushServer } from "@/libs/util"; import { sendTestThroughPushServer } from "@/libs/util";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class HelpNotificationsView extends Vue { export default class HelpNotificationsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
subscriptionJSON?: PushSubscriptionJSON; subscription: PushSubscription | null = null;
async mounted() { async mounted() {
try { try {
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker.ready;
const fullSub = await registration.pushManager.getSubscription(); this.subscription = await registration.pushManager.getSubscription();
this.subscriptionJSON = fullSub?.toJSON();
} catch (error) { } catch (error) {
console.error("Mount error:", error); console.error("Mount error:", error);
} }
@@ -316,13 +321,13 @@ export default class HelpNotificationsView extends Vue {
alertWebPushSubscription() { alertWebPushSubscription() {
console.log( console.log(
"Web push subscription:", "Web push subscription:",
JSON.parse(JSON.stringify(this.subscriptionJSON)), // gives more info than plain console logging JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging
); );
alert(JSON.stringify(this.subscriptionJSON)); alert(JSON.stringify(this.subscription));
} }
async sendTestWebPushMessage(skipFilter: boolean = false) { async sendTestWebPushMessage(skipFilter: boolean = false) {
if (!this.subscriptionJSON) { if (!this.subscription) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -337,7 +342,7 @@ export default class HelpNotificationsView extends Vue {
} }
try { try {
await sendTestThroughPushServer(this.subscriptionJSON, skipFilter); await sendTestThroughPushServer(this.subscription, skipFilter);
this.$notify( this.$notify(
{ {

View File

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

View File

@@ -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"
@click="$router.back()" @click="$router.back()"
> >
<fa icon="chevron-left" class="fa-fw" /> <fa icon="chevron-left" class="fa-fw"></fa>
</h1> </h1>
</div> </div>
@@ -39,22 +39,22 @@
and network. and network.
</p> </p>
<p> <p>
You highlight giving and also offer help to ideas -- which could be You can show giving and also offer help to ideas, based on others'
conditional on others' willingness to help, too. willingness to help out, too. You can record your own ideas and invite
You can record your own ideas and invite others to collaborate. others to collaborate.
</p> </p>
<p> <p>
This app uses the power of cryptography to build a reputation, recording This app uses the power of cryptography to build a reputation, recording
activity that you can share at your discretion. You put some activity activity that you can share at your discretion. You put some activity
public, but these services don't share your ID with others without explicit consent. public, but your sensitive information is not shared with anyone,
This is in contrast to Meta and Google, who hold including our services. This is in contrast to Meta and Google, who hold
your data and allow you use it while they manage sharing... your data and allow you use it. Those services are useful, but they have
those services are useful but they have the control, whereas this app gives you the control. the control; this app gives you the control.
</p> </p>
<h2 class="text-xl font-semibold">How do I get started?</h2> <h2 class="text-xl font-semibold">How do I get started?</h2>
<p> <p>
You need someone to register you, like the person who told you You need someone to register you -- usually the person who told you
about this app, on the Contacts about this app, on the Contacts
<fa icon="users" class="fa-fw" /> page. After they register you, you can <fa icon="users" class="fa-fw" /> page. After they register you, you can
select any contact on the home page (or "anonymous") and record your select any contact on the home page (or "anonymous") and record your
@@ -83,9 +83,9 @@
<h2 class="text-xl font-semibold">How do I add someone else?</h2> <h2 class="text-xl font-semibold">How do I add someone else?</h2>
<p> <p>
<a href="/help-onboarding" target="_blank" class="text-blue-500"> <button class="text-blue-500" @click="showOnboardInfo">
Click here to show an alert with the steps. Click here to show an alert with the steps.
</a> </button>
To start scanning, go To start scanning, go
<router-link class="text-blue-500" to="/contact-qr">here.</router-link> <router-link class="text-blue-500" to="/contact-qr">here.</router-link>
</p> </p>
@@ -198,7 +198,9 @@
<ul> <ul>
<li class="list-disc list-outside ml-4"> <li class="list-disc list-outside ml-4">
Chrome: Chrome:
Clear at chrome://settings/content/all and <a href="chrome://settings/content/all" class="text-blue-500"
>clear here</a
>
also clear under dev tools Application also clear under dev tools Application
</li> </li>
<li class="list-disc list-outside ml-4"> <li class="list-disc list-outside ml-4">
@@ -215,28 +217,6 @@
</ul> </ul>
<p>To erase your data from our servers, contact us (below).</p> <p>To erase your data from our servers, contact us (below).</p>
<h2 class="text-xl font-semibold">
How do I get higher limits?
</h2>
<p>
Let's talk. Contact us (below).
</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"> <h2 class="text-xl font-semibold">
I know there is a record from someone, so why can't I see that info? I know there is a record from someone, so why can't I see that info?
</h2> </h2>
@@ -260,65 +240,35 @@
</h2> </h2>
<p> <p>
<router-link class="text-blue-500" to="/help-notifications" <router-link class="text-blue-500" to="/help-notifications"
>Here.</router-link >Here.</router-link
> >
</p> </p>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
My app is misbehaving, like showing me a blank screen or failing to show a feed. How do I get higher limits?
What can I do?
</h2> </h2>
<p> <p>
First, note that clearing the cache will clear all your identity and contact info, Let's talk. Contact us (below).
so we recommend doing other things first (unless you know you have your backups ready).
</p> </p>
<ul class="list-disc list-outside ml-4">
<li> <h2 class="text-xl font-semibold">
Drag down on the screen to refresh it; do that multiple times, because How do I access even more functionality?
it sometimes takes multiple tries for the app to refresh to the current version. </h2>
You can see the version information at the bottom of this page; the best
way to determine the current version is to open this page in an incognito
browser window and look at the version there.
</li>
<li>
Close all tabs that have Time Safari open; it can be difficult to find them all,
and you may have to close all your tabs. In addition, it may be running as an
installed app, so look for any Time Safari app that may be running outside a browser.
</li>
<li>
It can help to reregister the service worker:
<ul>
<li>
In Chrome, open a tab to
"chrome://serviceworker-internals",
find "timesafari.app", and click "Unregister".</li>
<li>
In Firefox,
open a tab to "about:serviceworkers",
find "timesafari.app", and click "Unregister".
</li>
<li>
<a href="https://duckduckgo.com/?q=unregister+service+worker" class="text-blue-500">Search</a>
for instructions for other browsers.</li>
</ul>
Then reload Time Safari.
</li>
<li>
Restart your device.
</li>
</ul>
<p> <p>
If you still have problems, you can clear the cache (see "erase my data" above) There is an "Advanced" section at the bottom of the Account
and even uninstall and reinstall the app <fa icon="circle-user" /> page.
-- just be sure to have your backups ready or be </p>
prepared to restart with a new identity and recreate your network. <p>
Nobody else has access to your identity or contact information because There is a even more functionality in a mobile app (and more
this app is designed to give you full control over your data. documentation) at
<a href="https://endorser.ch" class="text-blue-500">
EndorserSearch.com
</a>
</p> </p>
<h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2> <h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2>
<p style="display:inline; align-items: center"> <p style="display:inline; align-items: center">
This work is public domain, governed by This work is marked with
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer"> <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
<span class="text-blue-500 mr-1">CC0 1.0</span> <span class="text-blue-500 mr-1">CC0 1.0</span>
<img <img
@@ -375,13 +325,32 @@ import { Component, Vue } from "vue-facing-decorator";
import * as Package from "../../package.json"; import * as Package from "../../package.json";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app"; import { ONBOARD_MESSAGE } from "@/libs/util";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class Help extends Vue { export default class Help extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
package = Package; package = Package;
commitHash = import.meta.env.VITE_GIT_HASH; commitHash = process.env.VUE_APP_GIT_HASH;
showOnboardInfo() {
this.$notify(
{
group: "alert",
type: "info",
title: "Onboard Someone",
text: ONBOARD_MESSAGE,
},
-1,
);
}
} }
</script> </script>

View File

@@ -1,10 +1,10 @@
<template> <template>
<QuickNav selected="Home" /> <QuickNav selected="Home"></QuickNav>
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-2 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Time Safari Time Safari
</h1> </h1>
@@ -30,7 +30,7 @@
and go click on that new app. and go click on that new app.
</span> </span>
<span <span
v-else-if="userAgentInfo.getBrowser()?.name?.startsWith('Chrome')" v-else-if="userAgentInfo.getBrowser().name.startsWith('Chrome')"
> >
You should see a prompt to install, or you can click on the You should see a prompt to install, or you can click on the
top-right dots top-right dots
@@ -59,15 +59,6 @@
</div> </div>
</div> </div>
<div v-if="showShortcutBvc" class="mb-4">
<router-link
:to="{ name: 'quick-action-bvc' }"
class="block text-center text-md font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
Bountiful Voluntaryist Community Actions</router-link
>
</div>
<!-- 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="isCreatingIdentifier"> <div v-if="isCreatingIdentifier">
@@ -85,7 +76,7 @@
</p> </p>
<router-link <router-link
:to="{ name: 'start' }" :to="{ name: 'start' }"
class="block text-center text-md font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md" class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
> >
Create An Identifier</router-link Create An Identifier</router-link
> >
@@ -99,7 +90,7 @@
giving. giving.
<router-link <router-link
:to="{ name: 'contact-qr' }" :to="{ name: 'contact-qr' }"
class="block text-center text-md font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md" class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
> >
Show Them Your Identifier Info</router-link Show Them Your Identifier Info</router-link
> >
@@ -114,13 +105,17 @@
<div v-else> <div v-else>
<!-- activeDid && isRegistered --> <!-- activeDid && isRegistered -->
<div class="mb-4"> <div class="flex justify-between mb-4">
<h2 class="text-xl font-bold">Record Something Given By:</h2> <h2 class="text-xl font-bold">Record Something Given</h2>
<button
@click="openGiftedPrompts()"
class="block text-center text-md font-bold bg-blue-500 text-white px-2 py-3 rounded-md"
>
Ideas...
</button>
</div> </div>
<ul <ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5"
>
<li @click="openDialog()"> <li @click="openDialog()">
<img <img
src="../assets/blank-square.svg" src="../assets/blank-square.svg"
@@ -150,20 +145,21 @@
</li> </li>
</ul> </ul>
<div class="flex justify-between"> <!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
<router-link <router-link
v-if="allContacts.length >= 7" v-if="allContacts.length >= 7"
:to="{ name: 'contact-gives' }" :to="{ name: 'contact-gives' }"
class="block text-center text-md font-bold uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
> >
Choose From All Contacts Show More Contacts&hellip;
</router-link> </router-link>
<button
@click="openGiftedPrompts()" <!-- If there are no contacts, show this instead: -->
class="block text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" <div
> class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500"
Ideas... v-if="allContacts.length === 0"
</button> >
(No contacts to show.)
</div> </div>
</div> </div>
</div> </div>
@@ -174,29 +170,10 @@
showGivenToUser="true" showGivenToUser="true"
/> />
<GiftedPrompts ref="giftedPrompts" /> <GiftedPrompts ref="giftedPrompts" />
<FeedFilters ref="feedFilters" />
<!-- Results List --> <!-- Results List -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<div class="flex items-center mb-4"> <h2 class="text-xl font-bold mb-4">Latest Activity</h2>
<h2 class="text-xl font-bold">Latest Activity</h2>
<button @click="openFeedFilters()" class="block text-center ml-auto">
<span class="text-sm uppercase text-white">
<span
v-if="resultsAreFiltered()"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md"
>
Filtered
</span>
<span
v-else
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md"
>
Unfiltered
</span>
</span>
</button>
</div>
<InfiniteScroll @reached-bottom="loadMoreGives"> <InfiniteScroll @reached-bottom="loadMoreGives">
<ul class="border-t border-slate-300"> <ul class="border-t border-slate-300">
<li <li
@@ -208,48 +185,36 @@
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 == feedLastViewedClaimId" v-if="record.jwtId == feedLastViewedClaimId"
> >
You've already seen all the following You've seen all the following before
</div> </div>
<div class="grid grid-cols-12"> <div class="grid grid-cols-12">
<span class="col-span-1 justify-self-start"> <span class="col-span-11 justify-self-start">
<span> <fa
<fa icon="gift"
v-if="record.giver.known || record.receiver.known" class="col-span-1 pt-1 pr-2 text-slate-500"
icon="circle-user" ></fa>
class="pt-1 text-slate-500" {{ this.giveDescription(record) }}
/>
<fa v-else icon="gift" class="pt-1 pl-3 text-slate-500" />
</span>
</span>
<span class="col-span-10 justify-self-stretch">
<span class="pl-2">
{{ giveDescription(record) }}
</span>
<a @click="onClickLoadClaim(record.jwtId)"> <a @click="onClickLoadClaim(record.jwtId)">
<fa <fa
icon="file-lines" icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer" class="pl-2 text-blue-500 cursor-pointer"
></fa> ></fa>
</a> </a>
</span> </span>
<span class="col-span-1 justify-self-end"> <span class="col-span-1 justify-self-end shrink">
<router-link <router-link
v-if="record.fulfillsPlanHandleId" v-if="record.fulfillsPlanHandleId"
:to=" :to="
'/project/' + '/project/' +
encodeURIComponent(record.fulfillsPlanHandleId) encodeURIComponent(record.fulfillsPlanHandleId)
" "
class="justify-end"
> >
<fa icon="hammer" class="text-blue-500"></fa> <fa icon="hammer" class="ml-4 pl-2 text-blue-500"></fa>
</router-link> </router-link>
</span> </span>
</div> </div>
<div v-if="record.image" class="flex justify-center">
<a :href="record.image" target="_blank">
<img :src="record.image" class="h-24 mt-2 rounded-xl" />
</a>
</div>
</li> </li>
</ul> </ul>
</InfiniteScroll> </InfiniteScroll>
@@ -258,67 +223,44 @@
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip; <fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip;
</p> </p>
</div> </div>
<div v-if="!isFeedLoading && feedData.length === 0">
<p class="text-slate-500 text-center italic mt-4 mb-4">
No claims match your filters.
</p>
</div>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import * as R from "ramda";
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import GiftedPrompts from "@/components/GiftedPrompts.vue"; import GiftedPrompts from "@/components/GiftedPrompts.vue";
import FeedFilters from "@/components/FeedFilters.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
BoundingBox,
isAnyFeedFilterOn,
MASTER_SETTINGS_KEY,
Settings,
} from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import {
contactForDid, didInfo,
containsNonHiddenDid,
didInfoForContact,
getPlanFromCache,
GiverInputInfo, GiverInputInfo,
GiveSummaryRecord, GiveServerRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { IIdentifier } from "@veramo/core";
import { generateSaveAndActivateIdentity } from "@/libs/util"; import { generateSaveAndActivateIdentity } from "@/libs/util";
interface GiveRecordWithContactInfo extends GiveSummaryRecord { interface Notification {
giver: { group: string;
displayName: string; type: string;
known: boolean; title: string;
}; text: string;
image: string;
recipientProjectName: string | undefined;
receiver: {
displayName: string;
known: boolean;
};
} }
@Component({ @Component({
components: { components: {
GiftedDialog, GiftedDialog,
GiftedPrompts, GiftedPrompts,
FeedFilters,
QuickNav, QuickNav,
EntityIcon, EntityIcon,
InfiniteScroll, InfiniteScroll,
@@ -326,26 +268,18 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
}, },
}) })
export default class HomeView extends Vue { export default class HomeView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
feedData: GiveRecordWithContactInfo[] = []; feedData: GiveServerRecord[] = [];
feedPreviousOldestId?: string; feedPreviousOldestId?: string;
feedLastViewedClaimId?: string; feedLastViewedClaimId?: string;
isAnyFeedFilterOn: boolean;
isCreatingIdentifier = false; isCreatingIdentifier = false;
isFeedFilteredByVisible = false;
isFeedFilteredByNearby = false;
isFeedLoading = true; isFeedLoading = true;
isRegistered = false; isRegistered = false;
searchBoxes: Array<{
name: string;
bbox: BoundingBox;
}> = [];
showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string) {
@@ -367,7 +301,7 @@ export default class HomeView extends Vue {
return headers; return headers;
} }
async mounted() { async created() {
try { try {
await accountsDB.open(); await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
@@ -379,13 +313,7 @@ export default class HomeView extends Vue {
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedClaimId = settings?.lastViewedClaimId; this.feedLastViewedClaimId = settings?.lastViewedClaimId;
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.searchBoxes = settings?.searchBoxes || [];
this.showShortcutBvc = !!settings?.showShortcutBvc;
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
if (this.allMyDids.length === 0) { if (this.allMyDids.length === 0) {
this.isCreatingIdentifier = true; this.isCreatingIdentifier = true;
@@ -400,7 +328,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.error("Error retrieving settings or feed.", err); console.log("Error retrieving settings or feed.", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -415,10 +343,6 @@ export default class HomeView extends Vue {
} }
} }
resultsAreFiltered() {
return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
}
notificationsSupported() { notificationsSupported() {
return "Notification" in window; return "Notification" in window;
} }
@@ -428,34 +352,27 @@ export default class HomeView extends Vue {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
const identity = await this.getIdentity(this.activeDid);
if (this.activeDid) { if (this.activeDid) {
if (identity) { await accountsDB.open();
headers["Authorization"] = "Bearer " + (await accessToken(identity)); const allAccounts = await accountsDB.accounts.toArray();
} else { const account = allAccounts.find(
(acc) => acc.did === this.activeDid,
) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error( throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.", "An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
); );
} }
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else { } else {
// it's OK without auth... we just won't get any identifiers // it's OK without auth... we just won't get any identifiers
} }
return headers; return headers;
} }
// only called when a setting was changed
async reloadFeedOnChange() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
this.feedData = [];
this.feedPreviousOldestId = undefined;
this.updateAllFeed();
}
/** /**
* Data loader used by infinite scroller * Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load * @param payload is the flag from the InfiniteScroll indicating if it should load
@@ -466,92 +383,12 @@ export default class HomeView extends Vue {
} }
} }
latLongInAnySearchBox(lat: number, long: number) {
for (const boxInfo of this.searchBoxes) {
if (
boxInfo.bbox.westLong <= long &&
long <= boxInfo.bbox.eastLong &&
boxInfo.bbox.minLat <= lat &&
lat <= boxInfo.bbox.maxLat
) {
return true;
}
}
}
public async updateAllFeed() { public async updateAllFeed() {
this.isFeedLoading = true; this.isFeedLoading = true;
let endOfResults = true;
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId) await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
.then(async (results) => { .then(async (results) => {
if (results.data.length > 0) { if (results.data.length > 0) {
endOfResults = false; this.feedData = this.feedData.concat(results.data);
// include the descriptions of the giver and receiver
const identity = await this.getIdentity(this.activeDid);
const newFeedData: Array<Promise<GiveRecordWithContactInfo>> =
results.data.map(async (record: GiveSummaryRecord) => {
// similar code is in endorser-mobile utility.ts
// claim.claim happen for some claims wrapped in a Verifiable Credential
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const claim = (record.fullClaim as any).claim || record.fullClaim;
// agent.did is for legacy data, before March 2023
const giverDid =
claim.agent?.identifier || (claim.agent as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
// recipient.did is for legacy data, before March 2023
const recipientDid =
claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
const plan = await getPlanFromCache(
record.fulfillsPlanHandleId,
identity,
this.axios,
this.apiServer,
);
// check if the record should be filtered out
let anyMatch = false;
if (
this.isFeedFilteredByVisible &&
containsNonHiddenDid(record)
) {
// has a visible DID so it's a keeper
anyMatch = true;
}
if (!anyMatch && this.isFeedFilteredByNearby) {
// check if the associated project has a location inside user's search box
if (record.fulfillsPlanHandleId) {
if (plan?.locLat && plan?.locLon) {
if (this.latLongInAnySearchBox(plan.locLat, plan.locLon)) {
anyMatch = true;
}
}
}
}
if (this.isAnyFeedFilterOn && !anyMatch) {
return null;
}
return {
...record,
giver: didInfoForContact(
giverDid,
this.activeDid,
contactForDid(giverDid, this.allContacts),
this.allMyDids,
),
image: claim.image,
recipientProjectName: plan?.name,
receiver: didInfoForContact(
recipientDid,
this.activeDid,
contactForDid(recipientDid, this.allContacts),
this.allMyDids,
),
};
});
const allNewFeedData: GiveRecordWithContactInfo[] =
await Promise.all(newFeedData);
const filteredFeedData = allNewFeedData.filter(R.isNotNil);
this.feedData = this.feedData.concat(filteredFeedData);
this.feedPreviousOldestId = this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId; results.data[results.data.length - 1].jwtId;
// The following update is only done on the first load. // The following update is only done on the first load.
@@ -567,7 +404,7 @@ export default class HomeView extends Vue {
} }
}) })
.catch((e) => { .catch((e) => {
console.error("Error with feed load:", e); console.log("Error with feed load:", e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -578,10 +415,6 @@ export default class HomeView extends Vue {
-1, -1,
); );
}); });
if (this.feedData.length === 0 && !endOfResults) {
// repeat until there's at least some data
this.updateAllFeed();
}
this.isFeedLoading = false; this.isFeedLoading = false;
} }
@@ -603,7 +436,7 @@ export default class HomeView extends Vue {
}, },
); );
if (!response.ok) { if (response.status !== 200) {
throw await response.text(); throw await response.text();
} }
@@ -616,68 +449,46 @@ export default class HomeView extends Vue {
} }
} }
giveDescription(giveRecord: GiveRecordWithContactInfo) { giveDescription(giveRecord: GiveServerRecord) {
// similar code is in endorser-mobile utility.ts // similar code is in endorser-mobile utility.ts
// claim.claim happen for some claims wrapped in a Verifiable Credential // claim.claim happen for some claims wrapped in a Verifiable Credential
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim; const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim;
// agent.did is for legacy data, before March 2023
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const giverDid = claim.agent?.identifier || (claim.agent as any)?.did;
const giverInfo = didInfo(
giverDid,
this.activeDid,
this.allMyDids,
this.allContacts,
);
let gaveAmount = claim.object?.amountOfThisGood let gaveAmount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood) ? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: ""; : "";
if (claim.description) { if (claim.description) {
if (gaveAmount) { if (gaveAmount) {
gaveAmount = " (and " + gaveAmount + ")"; gaveAmount = gaveAmount + ", and also: ";
} }
gaveAmount = claim.description + gaveAmount; gaveAmount = gaveAmount + claim.description;
} }
if (!gaveAmount) { if (!gaveAmount) {
gaveAmount = "something not described"; gaveAmount = "something not described";
} }
// recipient.did is for legacy data, before March 2023
/** const gaveRecipientId =
* Only show giver and/or receiver info first if they're named. // eslint-disable-next-line @typescript-eslint/no-explicit-any
* - If only giver is named, show "... gave" claim.recipient?.identifier || (claim.recipient as any)?.did;
* - If only receiver is named, show "... received" const gaveRecipientInfo = gaveRecipientId
*/ ? " to " +
didInfo(
const giverInfo = giveRecord.giver; gaveRecipientId,
const recipientInfo = giveRecord.receiver; this.activeDid,
if (giverInfo.known && recipientInfo.known) { this.allMyDids,
// both giver and recipient are named this.allContacts,
return `${giverInfo.displayName} gave to ${recipientInfo.displayName}: ${gaveAmount}`; )
} else if (giverInfo.known) { : "";
// giver is named but recipient is not return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
// show the project name if to one
if (giveRecord.recipientProjectName) {
// retrieve the project name
return `${giverInfo.displayName} gave: ${gaveAmount} (to the project ${giveRecord.recipientProjectName})`;
}
// it's not to a project
return `${giverInfo.displayName} gave: ${gaveAmount} (to ${recipientInfo.displayName})`;
} else if (recipientInfo.known) {
// recipient is named but giver is not
return `${recipientInfo.displayName} received: ${gaveAmount} (from ${giverInfo.displayName})`;
} else {
// neither giver nor recipient are named
// show the project name if to one
if (giveRecord.recipientProjectName) {
// retrieve the project name
return `${gaveAmount} (to the project ${giveRecord.recipientProjectName})`;
}
// it's not to a project
let peopleInfo;
if (giverInfo.displayName === recipientInfo.displayName) {
peopleInfo = `between two who are ${giverInfo.displayName}`;
} else {
peopleInfo = `from ${giverInfo.displayName} to ${recipientInfo.displayName}`;
}
return gaveAmount + " (" + peopleInfo + ")";
}
} }
onClickLoadClaim(jwtId: string) { onClickLoadClaim(jwtId: string) {
@@ -695,16 +506,12 @@ export default class HomeView extends Vue {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
} }
openDialog(giver?: GiverInputInfo) { openDialog(giver: GiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(giver); (this.$refs.customDialog as GiftedDialog).open(giver);
} }
openGiftedPrompts() { openGiftedPrompts() {
(this.$refs.giftedPrompts as GiftedPrompts).open(); (this.$refs.giftedPrompts as GiftedPrompts).open();
} }
openFeedFilters() {
(this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange);
}
} }
</script> </script>

View File

@@ -65,13 +65,13 @@
<router-link <router-link
id="start-link" id="start-link"
:to="{ name: 'start' }" :to="{ name: 'start' }"
class="block text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
> >
Add Another Identity&hellip; Add Another Identity&hellip;
</router-link> </router-link>
<a <a
href="#" href="#"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-8" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-8"
@click="switchAccount('0')" @click="switchAccount('0')"
> >
No Identity No Identity
@@ -80,16 +80,22 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app";
import { AppString, NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts"; import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class IdentitySwitcherView extends Vue { export default class IdentitySwitcherView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
Constants = AppString; Constants = AppString;
public accounts: typeof AccountsSchema; public accounts: typeof AccountsSchema;

View File

@@ -56,36 +56,39 @@
</div> </div>
<div class="mt-8"> <div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> <button
<button @click="fromMnemonic()"
@click="fromMnemonic()" class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" >
> Import
Import </button>
</button> <button
<button @click="onCancelClick()"
@click="onCancelClick()" type="button"
type="button" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" >
> Cancel
Cancel </button>
</button>
</div>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { import {
DEFAULT_ROOT_DERIVATION_PATH, DEFAULT_ROOT_DERIVATION_PATH,
deriveAddress, deriveAddress,
newIdentifier, newIdentifier,
} from "@/libs/crypto"; } from "../libs/crypto";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: {}, components: {},
@@ -93,7 +96,7 @@ import {
export default class ImportAccountView extends Vue { export default class ImportAccountView extends Vue {
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; // for legacy imports, likely never used UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; // for legacy imports, likely never used
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
mnemonic = ""; mnemonic = "";
address = ""; address = "";

View File

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

View File

@@ -22,23 +22,21 @@
/> />
<div class="mt-8"> <div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> <button
<button type="button"
type="button" class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" @click="onClickSaveChanges()"
@click="onClickSaveChanges()" >
> Save Changes
Save Changes </button>
</button> <!-- SHOW ME instead while processing saving changes -->
<!-- SHOW ME instead while processing saving changes --> <button
<button type="button"
type="button" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" @click="onClickCancel()"
@click="onClickCancel()" >
> Cancel
Cancel </button>
</button>
</div>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -73,17 +73,16 @@
/> />
<label for="includeLocation">Include Location</label> <label for="includeLocation">Include Location</label>
</div> </div>
<div v-if="includeLocation" class="mb-4 aspect-video"> <div v-if="includeLocation" style="height: 600px; width: 800px">
<p class="text-sm mb-2 text-slate-500"> <div class="px-2 py-2">
For your security, choose a location nearby but not exactly at the For your security, choose a location nearby but not exactly at the
place. place.
</p> </div>
<l-map <l-map
ref="map" ref="map"
v-model:zoom="zoom" v-model:zoom="zoom"
:center="[0, 0]" :center="[0, 0]"
class="!z-40 rounded-md"
@click=" @click="
(event) => { (event) => {
latitude = event.latlng.lat; latitude = event.latlng.lat;
@@ -105,30 +104,28 @@
</div> </div>
<div class="mt-8"> <div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> <button
<button :disabled="isHiddenSave"
:disabled="isHiddenSave" class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" @click="onSaveProjectClick()"
@click="onSaveProjectClick()" >
> <!-- SHOW if in idle state -->
<!-- SHOW if in idle state --> <span :class="{ hidden: isHiddenSave }">Save Project</span>
<span :class="{ hidden: isHiddenSave }">Save Project</span>
<!-- SHOW if in saving state; DISABLE button while in saving state --> <!-- SHOW if in saving state; DISABLE button while in saving state -->
<span :class="{ hidden: isHiddenSpinner }"> <span :class="{ hidden: isHiddenSpinner }">
<!-- icon no worky? --> <!-- icon no worky? -->
<i class="fa-solid fa-spinner fa-spin-pulse"></i> <i class="fa-solid fa-spinner fa-spin-pulse"></i>
Saving...</span Saving...</span
>
</button>
<button
type="button"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="onCancelClick()"
> >
Cancel </button>
</button> <button
</div> type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="onCancelClick()"
>
Cancel
</button>
</div> </div>
</section> </section>
</template> </template>
@@ -141,7 +138,6 @@ import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
@@ -149,11 +145,18 @@ import { useAppStore } from "@/store/app";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { PlanVerifiableCredential } from "@/libs/endorserServer"; import { PlanVerifiableCredential } from "@/libs/endorserServer";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { LMap, LMarker, LTileLayer, QuickNav }, components: { LMap, LMarker, LTileLayer, QuickNav },
}) })
export default class NewEditProjectView extends Vue { export default class NewEditProjectView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
agentDid = ""; agentDid = "";
@@ -268,8 +271,6 @@ export default class NewEditProjectView extends Vue {
vcClaim.agent = { vcClaim.agent = {
identifier: this.agentDid, identifier: this.agentDid,
}; };
} else {
delete vcClaim.agent;
} }
if (this.includeLocation) { if (this.includeLocation) {
vcClaim.location = { vcClaim.location = {
@@ -279,8 +280,6 @@ export default class NewEditProjectView extends Vue {
longitude: this.longitude, longitude: this.longitude,
}, },
}; };
} else {
delete vcClaim.location;
} }
// Make a payload for the claim // Make a payload for the claim
const vcPayload = { const vcPayload = {

View File

@@ -98,14 +98,14 @@
</div> </div>
<a @click="onClickLoadClaim(projectId)" class="cursor-pointer"> <a @click="onClickLoadClaim(projectId)" class="cursor-pointer">
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" /> <fa icon="circle-info" class="pl-2 pt-1 text-blue-500" />
</a> </a>
</div> </div>
<button <button
v-if="activeDid === issuer || activeDid === agentDid" v-if="activeDid === issuer || activeDid === agentDid"
type="button" type="button"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="onEditClick()" @click="onEditClick()"
> >
Edit Edit
@@ -116,7 +116,7 @@
<div class="text-center"> <div class="text-center">
<button <button
@click="openOfferDialog()" @click="openOfferDialog()"
class="block w-full text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
> >
Offer (maybe with conditions)... Offer (maybe with conditions)...
</button> </button>
@@ -134,9 +134,7 @@
<div class="text-center"> <div class="text-center">
<p class="mt-2 mb-4 text-center">Record a contribution from:</p> <p class="mt-2 mb-4 text-center">Record a contribution from:</p>
</div> </div>
<ul <ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5"
>
<li @click="openGiftDialog({ name: 'you', did: activeDid })"> <li @click="openGiftDialog({ name: 'you', did: activeDid })">
<fa icon="hand" class="fa-fw text-slate-400 text-5xl" /> <fa icon="hand" class="fa-fw text-slate-400 text-5xl" />
<h3 <h3
@@ -178,7 +176,7 @@
<a <a
v-if="allContacts.length >= 7" v-if="allContacts.length >= 7"
@click="onClickAllContactsGifting()" @click="onClickAllContactsGifting()"
class="block text-center text-md font-bold uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
> >
Show More Contacts&hellip; Show More Contacts&hellip;
</a> </a>
@@ -232,7 +230,7 @@
@click="onClickLoadClaim(offer.jwtId as string)" @click="onClickLoadClaim(offer.jwtId as string)"
class="cursor-pointer" class="cursor-pointer"
> >
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" /> <fa icon="circle-info" class="pl-2 pt-1 text-blue-500" />
</a> </a>
<a <a
v-if="checkIsFulfillable(offer)" v-if="checkIsFulfillable(offer)"
@@ -291,7 +289,7 @@
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<a @click="onClickLoadClaim(give.jwtId)"> <a @click="onClickLoadClaim(give.jwtId)">
<fa icon="file-lines" class="text-blue-500 cursor-pointer" /> <fa icon="circle-info" class="text-blue-500 cursor-pointer" />
</a> </a>
<a v-if="checkIsConfirmable(give)" @click="confirmClaim(give)"> <a v-if="checkIsConfirmable(give)" @click="confirmClaim(give)">
<fa icon="circle-check" class="text-blue-500 cursor-pointer" /> <fa icon="circle-check" class="text-blue-500 cursor-pointer" />
@@ -350,25 +348,31 @@ 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 TopMessage from "@/components/TopMessage.vue";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import ProjectIcon from "@/components/ProjectIcon.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { import {
BLANK_GENERIC_SERVER_RECORD, BLANK_GENERIC_SERVER_RECORD,
GenericCredWrapper, GenericServerRecord,
GiverInputInfo, GiverInputInfo,
GiveSummaryRecord, GiveServerRecord,
OfferSummaryRecord, OfferServerRecord,
PlanSummaryRecord, PlanServerRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import ProjectIcon from "@/components/ProjectIcon.vue";
import { Account } from "@/db/tables/accounts";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { components: {
@@ -381,7 +385,7 @@ import * as serverUtil from "@/libs/endorserServer";
}, },
}) })
export default class ProjectViewView extends Vue { export default class ProjectViewView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
agentDid = ""; agentDid = "";
@@ -390,14 +394,14 @@ export default class ProjectViewView extends Vue {
apiServer = ""; apiServer = "";
description = ""; description = "";
expanded = false; expanded = false;
fulfilledByThis: PlanSummaryRecord | null = null; fulfilledByThis: PlanServerRecord | null = null;
fulfillersToThis: Array<PlanSummaryRecord> = []; fulfillersToThis: Array<PlanServerRecord> = [];
givesToThis: Array<GiveSummaryRecord> = []; givesToThis: Array<GiveServerRecord> = [];
issuer = ""; issuer = "";
latitude = 0; latitude = 0;
longitude = 0; longitude = 0;
name = ""; name = "";
offersToThis: Array<OfferSummaryRecord> = []; offersToThis: Array<OfferServerRecord> = [];
projectId = localStorage.getItem("projectId") || ""; // handle ID projectId = localStorage.getItem("projectId") || ""; // handle ID
showDidCopy = false; showDidCopy = false;
timeSince = ""; timeSince = "";
@@ -439,6 +443,15 @@ export default class ProjectViewView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
onEditClick() { onEditClick() {
localStorage.setItem("projectId", this.projectId as string); localStorage.setItem("projectId", this.projectId as string);
const route = { const route = {
@@ -720,8 +733,8 @@ export default class ProjectViewView extends Vue {
this.$router.push(route); this.$router.push(route);
} }
checkIsFulfillable(offer: OfferSummaryRecord) { checkIsFulfillable(offer: OfferServerRecord) {
const offerRecord: GenericCredWrapper = { const offerRecord: GenericServerRecord = {
...BLANK_GENERIC_SERVER_RECORD, ...BLANK_GENERIC_SERVER_RECORD,
claim: offer.fullClaim, claim: offer.fullClaim,
claimType: "Offer", claimType: "Offer",
@@ -730,8 +743,8 @@ export default class ProjectViewView extends Vue {
return libsUtil.canFulfillOffer(offerRecord); return libsUtil.canFulfillOffer(offerRecord);
} }
onClickFulfillGiveToOffer(offer: OfferSummaryRecord) { onClickFulfillGiveToOffer(offer: OfferServerRecord) {
const offerRecord: GenericCredWrapper = { const offerRecord: GenericServerRecord = {
...BLANK_GENERIC_SERVER_RECORD, ...BLANK_GENERIC_SERVER_RECORD,
claim: offer.fullClaim, claim: offer.fullClaim,
issuer: offer.offeredByDid, issuer: offer.offeredByDid,
@@ -770,8 +783,8 @@ export default class ProjectViewView extends Vue {
} }
} }
checkIsConfirmable(give: GiveSummaryRecord) { checkIsConfirmable(give: GiveServerRecord) {
const giveDetails: GenericCredWrapper = { const giveDetails: GenericServerRecord = {
...BLANK_GENERIC_SERVER_RECORD, ...BLANK_GENERIC_SERVER_RECORD,
claim: give.fullClaim, claim: give.fullClaim,
claimType: "GiveAction", claimType: "GiveAction",
@@ -781,7 +794,7 @@ export default class ProjectViewView extends Vue {
} }
// similar code is found in ClaimView // similar code is found in ClaimView
async confirmClaim(give: GiveSummaryRecord) { async confirmClaim(give: GiveServerRecord) {
if (confirm("Do you personally confirm that this is true?")) { if (confirm("Do you personally confirm that this is true?")) {
// similar logic is found in endorser-mobile // similar logic is found in endorser-mobile
const goodClaim = serverUtil.removeSchemaContext( const goodClaim = serverUtil.removeSchemaContext(
@@ -793,7 +806,10 @@ export default class ProjectViewView extends Vue {
), ),
), ),
); );
const confirmationClaim: serverUtil.GenericVerifiableCredential = { const confirmationClaim: serverUtil.GenericVerifiableCredential & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: any;
} = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "AgreeAction", "@type": "AgreeAction",
object: goodClaim, object: goodClaim,

View File

@@ -167,7 +167,7 @@
<a @click="onClickLoadClaim(offer.jwtId)"> <a @click="onClickLoadClaim(offer.jwtId)">
<fa <fa
icon="file-lines" icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer" class="pl-2 text-blue-500 cursor-pointer"
></fa> ></fa>
</a> </a>
@@ -213,7 +213,6 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
@@ -223,21 +222,28 @@ import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import ProjectIcon from "@/components/ProjectIcon.vue"; import ProjectIcon from "@/components/ProjectIcon.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { OfferSummaryRecord, PlanData } from "@/libs/endorserServer"; import { OfferServerRecord, PlanData } from "@/libs/endorserServer";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { EntityIcon, InfiniteScroll, QuickNav, ProjectIcon, TopMessage }, components: { EntityIcon, InfiniteScroll, QuickNav, ProjectIcon, TopMessage },
}) })
export default class ProjectsView extends Vue { export default class ProjectsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
apiServer = ""; apiServer = "";
projects: PlanData[] = []; projects: PlanData[] = [];
currentIid: IIdentifier; currentIid: IIdentifier;
isLoading = false; isLoading = false;
numAccounts = 0; numAccounts = 0;
offers: OfferSummaryRecord[] = []; offers: OfferServerRecord[] = [];
showOffers = true; showOffers = true;
showProjects = false; showProjects = false;

View File

@@ -1,220 +0,0 @@
<template>
<QuickNav />
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- 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 px-4 mb-4">
Beginning of BVC Saturday Meeting
</h1>
<div>
<h2 class="text-2xl m-2">You're Here</h2>
<div class="m-2 flex">
<input type="checkbox" v-model="attended" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Attended</span>
</div>
<div class="m-2 flex">
<input type="checkbox" v-model="gaveTime" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Spent Time</span>
<span v-if="gaveTime">
<input
type="text"
placeholder="How much time"
v-model="hoursStr"
size="1"
class="border border-slate-400 h-6 px-2"
/>
hour(s)
</span>
<!-- This is to match input height to avoid shifting when hiding & showing. -->
<span v-else class="h-6" />
</div>
</div>
<div
v-if="attended || (gaveTime && hoursStr && hoursStr != '0')"
class="flex justify-center mt-4"
>
<button
@click="record()"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md w-56"
>
Sign & Send
</button>
</div>
<div v-else class="flex justify-center mt-4">
<button
class="block text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md w-56"
>
Select Your Actions
</button>
</div>
</section>
</template>
<script lang="ts">
import axios from "axios";
import { DateTime } from "luxon";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
bvcMeetingJoinClaim,
createAndSubmitClaim,
createAndSubmitGive,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@Component({
components: {
QuickNav,
TopMessage,
},
})
export default class QuickActionBvcBeginView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
attended = true;
gaveTime = true;
hoursStr = "1";
todayOrPreviousStartDate = "";
async mounted() {
let currentOrPreviousSat = DateTime.now().setZone("America/Denver");
if (currentOrPreviousSat.weekday < 6) {
// it's not Saturday or Sunday,
// so move back one week before setting to the Saturday
currentOrPreviousSat = currentOrPreviousSat.minus({ week: 1 });
}
const eventStartDateObj = currentOrPreviousSat
.set({ weekday: 6 })
.set({ hour: 9 })
.startOf("hour");
// Hack, but full ISO pushes the length to 340 which crashes verifyJWT!
this.todayOrPreviousStartDate =
eventStartDateObj.toISO({
suppressMilliseconds: true,
}) || "";
}
async record() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
const activeDid = settings?.activeDid || "";
const apiServer = settings?.apiServer || "";
try {
const hoursNum = libsUtil.numberOrZero(this.hoursStr);
const identity = await libsUtil.getIdentity(activeDid);
// first send the claim for time given
let timeSuccess = false;
if (this.gaveTime && hoursNum > 0) {
const timeResult = await createAndSubmitGive(
axios,
apiServer,
identity,
activeDid,
undefined,
undefined,
hoursNum,
"HUR",
BVC_MEETUPS_PROJECT_CLAIM_ID,
);
if (timeResult.type === "success") {
timeSuccess = true;
} else {
console.error("Error sending time:", timeResult);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
timeResult?.error?.userMessage ||
"There was an error sending the time.",
},
-1,
);
}
}
// now send the claim for attendance
let attendedSuccess = false;
if (this.attended) {
const attendResult = await createAndSubmitClaim(
bvcMeetingJoinClaim(activeDid, this.todayOrPreviousStartDate),
identity,
apiServer,
axios,
);
if (attendResult.type === "success") {
attendedSuccess = true;
} else {
console.error("Error sending attendance:", attendResult);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
attendResult?.error?.userMessage ||
"There was an error sending the attendance.",
},
-1,
);
}
}
if (timeSuccess || attendedSuccess) {
const actions =
timeSuccess && attendedSuccess
? "Your attendance and time have been recorded."
: timeSuccess
? "Your time has been recorded."
: "Your attendance has been recorded.";
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: actions,
},
-1,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error sending claims.", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: error.userMessage || "There was an error sending claims.",
},
-1,
);
}
}
}
</script>

View File

@@ -1,377 +0,0 @@
<template>
<QuickNav />
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- 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 px-4 mb-4">
End of BVC Saturday Meeting
</h1>
<div>
<h2 class="text-2xl m-2">Confirm</h2>
<div v-if="loadingConfirms" class="flex justify-center">
<fa icon="spinner" class="animate-spin" />
</div>
<div v-else-if="claimsToConfirm.length === 0">
There are no claims yet today for you to confirm.
</div>
<ul class="border-t border-slate-300 m-2">
<li
class="border-b border-slate-300 py-2"
v-for="record in claimsToConfirm"
:key="record.id"
>
<div class="grid grid-cols-12">
<span class="col-span-11 justify-self-start">
<span>
<input
type="checkbox"
:checked="claimsToConfirmSelected.includes(record.id)"
@click="
claimsToConfirmSelected.includes(record.id)
? claimsToConfirmSelected.splice(
claimsToConfirmSelected.indexOf(record.id),
1,
)
: claimsToConfirmSelected.push(record.id)
"
class="mr-2 h-6 w-6"
/>
</span>
{{
claimSpecialDescription(
record,
activeDid,
allMyDids,
allContacts,
)
}}
<a @click="onClickLoadClaim(record.id)">
<fa
icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer"
/>
</a>
</span>
</div>
</li>
</ul>
</div>
<div v-if="claimCountWithHidden > 0" class="border-b border-slate-300 pb-2">
<span>
{{
claimCountWithHidden === 1
? "There is 1 other claim with hidden details,"
: `There are ${claimCountWithHidden} other claims with hidden details,`
}}
so if you expected but do not see details from someone then ask them to
check that their activity is visible to you on their Contacts
<fa icon="users" class="text-slate-500" />
page.
</span>
</div>
<div>
<h2 class="text-2xl m-2">Anything else?</h2>
<div class="m-2 flex">
<input type="checkbox" v-model="someoneGave" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Someone else gave</span>
<span v-if="someoneGave">
<input
type="text"
v-model="description"
size="20"
class="border border-slate-400 h-6 px-2"
/>
<br />
(Everyone likes personalized messages! 😁)
</span>
<!-- This is to match input height to avoid shifting when hiding & showing. -->
<span v-else class="h-6">...</span>
</div>
</div>
<div
v-if="claimsToConfirmSelected.length || (someoneGave && description)"
class="flex justify-center mt-4"
>
<button
@click="record()"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md w-56"
>
Sign & Send
</button>
</div>
<div v-else class="flex justify-center mt-4">
<button
class="block text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md w-56"
>
Choose What To Confirm
</button>
</div>
</section>
</template>
<script lang="ts">
import axios from "axios";
import { DateTime } from "luxon";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
claimSpecialDescription,
containsHiddenDid,
createAndSubmitConfirmation,
createAndSubmitGive,
ErrorResult,
GenericCredWrapper,
GenericVerifiableCredential,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
@Component({
methods: { claimSpecialDescription },
components: {
QuickNav,
TopMessage,
},
})
export default class QuickActionBvcBeginView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = "";
claimCountWithHidden = 0;
claimsToConfirm: GenericCredWrapper[] = [];
claimsToConfirmSelected: string[] = [];
description = "breakfast";
loadingConfirms = true;
someoneGave = false;
async created() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
}
async mounted() {
this.loadingConfirms = true;
let currentOrPreviousSat = DateTime.now().setZone("America/Denver");
if (currentOrPreviousSat.weekday < 6) {
// it's not Saturday or Sunday,
// so move back one week before setting to the Saturday
currentOrPreviousSat = currentOrPreviousSat.minus({ week: 1 });
}
const eventStartDateObj = currentOrPreviousSat
.set({ weekday: 6 })
.set({ hour: 9 })
.startOf("hour");
// Hack, but full ISO pushes the length to 340 which crashes verifyJWT!
const todayOrPreviousStartDate =
eventStartDateObj.toISO({
suppressMilliseconds: true,
}) || "";
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
const account: Account | undefined = await accountsDB.accounts
.where("did")
.equals(this.activeDid)
.first();
const identity: IIdentifier = JSON.parse(
(account?.identity as string) || "null",
);
const headers = {
Authorization: "Bearer " + (await accessToken(identity)),
};
try {
const response = await fetch(
this.apiServer +
"/api/claim/?" +
"issuedAt_greaterThanOrEqualTo=" +
encodeURIComponent(todayOrPreviousStartDate) +
"&excludeConfirmations=true",
{ headers },
);
if (!response.ok) {
console.log("Bad response", response);
throw new Error("Bad response when retrieving claims.");
}
await response.json().then((data) => {
const dataByOthers = R.reject(
(claim: GenericCredWrapper) => claim.issuer === this.activeDid,
data,
);
const dataByOthersWithoutHidden = R.reject(
containsHiddenDid,
dataByOthers,
);
this.claimsToConfirm = dataByOthersWithoutHidden;
this.claimCountWithHidden =
dataByOthers.length - dataByOthersWithoutHidden.length;
});
} catch (error) {
console.error("Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error retrieving today's claims to confirm.",
},
-1,
);
}
this.loadingConfirms = false;
}
onClickLoadClaim(jwtId: string) {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
};
this.$router.push(route);
}
async record() {
try {
const identity = await libsUtil.getIdentity(this.activeDid);
// in parallel, make a confirmation for each selected claim and send them all to the server
const confirmResults = await Promise.allSettled(
this.claimsToConfirmSelected.map(async (jwtId) => {
const record = this.claimsToConfirm.find(
(claim) => claim.id === jwtId,
);
if (!record) {
return { type: "error", error: "Record not found." };
}
const identity = await libsUtil.getIdentity(this.activeDid);
return createAndSubmitConfirmation(
identity,
record.claim as GenericVerifiableCredential,
record.id,
record.handleId,
this.apiServer,
axios,
);
}),
);
// check for any rejected confirmations
const confirmsSucceeded = confirmResults.filter(
(result) =>
result.status === "fulfilled" && result.value.type === "success",
);
if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) {
console.error("Error sending confirmations:", confirmResults);
const howMany = confirmsSucceeded.length === 0 ? "all" : "some";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `There was an error sending ${howMany} of the confirmations.`,
},
-1,
);
}
// now send the give for the description
let giveSucceeded = false;
if (this.someoneGave) {
const giveResult = await createAndSubmitGive(
axios,
this.apiServer,
identity,
undefined,
this.activeDid,
this.description,
undefined,
undefined,
BVC_MEETUPS_PROJECT_CLAIM_ID,
);
giveSucceeded = giveResult.type === "success";
if (!giveSucceeded) {
console.error("Error sending give:", giveResult);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
(giveResult as ErrorResult)?.error?.userMessage ||
"There was an error sending that give.",
},
-1,
);
}
}
if (confirmsSucceeded.length > 0 || giveSucceeded) {
const confirms =
confirmsSucceeded.length === 1 ? "confirmation" : "confirmations";
const actions =
confirmsSucceeded.length > 0 && giveSucceeded
? `Your ${confirms} and that give have been recorded.`
: giveSucceeded
? "That give has been recorded."
: "Your " +
confirms +
" " +
(confirmsSucceeded.length === 1 ? "has" : "have") +
" been recorded.";
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: actions,
},
-1,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error sending claims.", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: error.userMessage || "There was an error sending claims.",
},
-1,
);
}
}
}
</script>

View File

@@ -1,52 +0,0 @@
<template>
<QuickNav />
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- 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 px-4 mb-4">
Bountiful Voluntaryist Community Actions
</h1>
<div>
<router-link
:to="{ name: 'quick-action-bvc-begin' }"
class="block text-center text-md font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
Beginning of Meeting
</router-link>
<router-link
:to="{ name: 'quick-action-bvc-end' }"
class="block text-center text-md font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
End of Meeting
</router-link>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
@Component({
components: {
QuickNav,
TopMessage,
},
})
export default class QuickActionBvcView extends Vue {}
</script>

View File

@@ -64,11 +64,10 @@
</div> </div>
</div> </div>
<div class="mb-4 aspect-video"> <div style="height: 600px; width: 800px">
<l-map <l-map
ref="map" ref="map"
:center="[localCenterLat, localCenterLong]" :center="[localCenterLat, localCenterLong]"
class="!z-40 rounded-md"
v-model:zoom="localZoom" v-model:zoom="localZoom"
@click="setMapPoint" @click="setMapPoint"
> >
@@ -106,15 +105,21 @@ import {
LTileLayer, LTileLayer,
} from "@vue-leaflet/vue-leaflet"; } from "@vue-leaflet/vue-leaflet";
import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue";
const DEFAULT_LAT_LONG_DIFF = 0.01; const DEFAULT_LAT_LONG_DIFF = 0.01;
const WORLD_ZOOM = 2; const WORLD_ZOOM = 2;
const DEFAULT_ZOOM = 2; const DEFAULT_ZOOM = 2;
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { components: {
QuickNav, QuickNav,
@@ -125,7 +130,7 @@ const DEFAULT_ZOOM = 2;
}, },
}) })
export default class DiscoverView extends Vue { export default class DiscoverView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
isChoosingSearchBox = false; isChoosingSearchBox = false;
isNewMarkerSet = false; isNewMarkerSet = false;
@@ -209,9 +214,9 @@ export default class DiscoverView extends Vue {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Saved", title: "Saved",
text: "That has been saved in your preferences. You can now filter by it on your home screen feed.", text: "That has been saved in your preferences.",
}, },
7000, -1,
); );
this.$router.back(); this.$router.back();
} catch (err) { } catch (err) {
@@ -247,7 +252,6 @@ export default class DiscoverView extends Vue {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: [], searchBoxes: [],
filterFeedByNearby: false,
}); });
this.searchBox = null; this.searchBox = null;
this.localCenterLat = 0; this.localCenterLat = 0;

View File

@@ -1,5 +1,5 @@
<template> <template>
<QuickNav selected="Profile" /> <QuickNav selected="Profile"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <!-- Back -->
@@ -22,7 +22,7 @@
<span> <span>
<router-link <router-link
:to="{ name: 'help' }" :to="{ name: 'help' }"
class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1" class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
> >
Help Help
</router-link> </router-link>
@@ -48,7 +48,7 @@
<div class="bg-slate-100 rounded-md overflow-hidden p-4 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden p-4 mb-4">
<button <button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="showSeedPhrase" @click="showSeedPhrase"
> >
Reveal my Seed Phrase Reveal my Seed Phrase
@@ -65,20 +65,25 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import * as R from "ramda";
import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import * as R from "ramda";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue";
interface Account { interface Account {
mnemonic: string; mnemonic: string;
} }
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class SeedBackupView extends Vue { export default class SeedBackupView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
activeAccount: Account | null | undefined = null; activeAccount: Account | null | undefined = null;
numAccounts = 0; numAccounts = 0;

View File

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

View File

@@ -53,10 +53,8 @@
<script lang="ts"> <script lang="ts">
import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js"; import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { World } from "@/components/World/World.js"; import { World } from "@/components/World/World.js";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
interface RendererSVGType { interface RendererSVGType {
domElement: Element; domElement: Element;
@@ -66,9 +64,16 @@ interface Dictionary<T> {
[key: string]: T; [key: string]: T;
} }
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { World, QuickNav } }) @Component({ components: { World, QuickNav } })
export default class StatisticsView extends Vue { export default class StatisticsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
world: World; world: World;
worldProperties: Dictionary<number> = {}; worldProperties: Dictionary<number> = {};

View File

@@ -1,35 +1,47 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", // Latest ECMAScript features that are widely supported by modern browsers "allowJs": true,
"module": "ESNext", // Use ES modules "resolveJsonModule": true,
"strict": true, // Enable all strict type checking options "target": "esnext",
"jsx": "preserve", // Preserves JSX to be transformed by Babel or another transpiler "module": "esnext",
"moduleResolution": "node", // Use Node.js style module resolution "strict": true,
"experimentalDecorators": true, "strictPropertyInitialization": false,
"esModuleInterop": true, // Enables compatibility with CommonJS modules for default imports "jsx": "preserve",
"allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export "moduleResolution": "node",
"forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file "experimentalDecorators": true,
"useDefineForClassFields": true, "skipLibCheck": true,
"sourceMap": true, "esModuleInterop": true,
"baseUrl": "./src", // Base directory to resolve non-relative module names "allowSyntheticDefaultImports": true,
"paths": { "forceConsistentCasingInFileNames": true,
"@/components/*": ["components/*"], "useDefineForClassFields": true,
"@/views/*": ["views/*"], "sourceMap": true,
"@/db/*": ["db/*"], "baseUrl": "./src",
"@/libs/*": ["libs/*"], "types": [
"@/constants/*": ["constants/*"], "webpack-env"
"@/store/*": ["store/*"]
},
"lib": ["ES2020", "dom", "dom.iterable"], // Include typings for ES2020 and DOM APIs
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
], ],
"exclude": [ "paths": {
"node_modules" "@/components/*": ["components/*"],
"@/views/*": ["views/*"],
"@/db/*": ["db/*"],
"@/libs/*": ["libs/*"],
"@/constants/*": ["constants/*"],
"@/store/*": ["store/*"],
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
] ]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
} }

View File

@@ -1,18 +0,0 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import * as path from "path";
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 8080
},
plugins: [ vue() ],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
buffer: path.resolve(__dirname, 'node_modules', 'buffer'),
'dexie-export-import/dist/import': 'dexie-export-import/dist/import/index.js',
},
},
});

View File

@@ -1,10 +1,35 @@
const { defineConfig } = require("@vue/cli-service");
const { gitDescribeSync } = require("git-describe");
const { exec } = require("child_process");
const TIME_SAFARI_APP_TITLE = process.env.VUE_APP_GIT_HASH = gitDescribeSync().hash;
import.meta.env.VITE_TIME_SAFARI_APP_TITLE || require("./package.json").name;
module.exports = defineConfig({ module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
devtool: "source-map",
experiments: {
topLevelAwait: true,
},
plugins: [
{
// Still don't know why this runs three times.
apply: (compiler) => {
compiler.hooks.beforeCompile.tap("BeforeCompile", () => {
// Execute combine-sw.js script
exec("node sw_combine.js", (error, stdout, stderr) => {
if (error || stderr) {
console.error("Service worker files error:", error || stderr);
} else {
console.log("Finished combining service worker files.", stdout);
}
});
});
},
},
],
},
pwa: { pwa: {
name: TIME_SAFARI_APP_TITLE,
iconPaths: { iconPaths: {
faviconSVG: "img/icons/safari-pinned-tab.svg", faviconSVG: "img/icons/safari-pinned-tab.svg",
}, },