forked from trent_larson/crowd-funder-for-time-pwa
Compare commits
25 Commits
why-migrat
...
openssl
| Author | SHA1 | Date | |
|---|---|---|---|
| 92fcffdfc5 | |||
| 5f5562f5e3 | |||
| 74ed025377 | |||
| f36ecfd8db | |||
| fc70a11bd8 | |||
| 73f890beac | |||
| 67dce9e678 | |||
| 2b66ddfb83 | |||
| 56fc2893a2 | |||
|
|
552ad5a267 | ||
|
|
910f57ec7d | ||
|
|
e813315dad | ||
| aea9626c06 | |||
|
|
7f0f1b7fc8 | ||
|
|
8684488def | ||
|
|
ee28b18b14 | ||
|
|
2d38183dce | ||
|
|
082a6eae1f | ||
|
|
d07fb47721 | ||
|
|
ccb6160bca | ||
|
|
2eaa4203aa | ||
|
|
f27a18c712 | ||
|
|
2c4a920c3c | ||
|
|
ed91cadd9d | ||
|
|
a6de282aec |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ node_modules
|
|||||||
signature.bin
|
signature.bin
|
||||||
*.pem
|
*.pem
|
||||||
verified.txt
|
verified.txt
|
||||||
|
myenv
|
||||||
|
|
||||||
*~
|
*~
|
||||||
# local env files
|
# local env files
|
||||||
|
|||||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
## [0.1.2] - 2023.11.01
|
## [0.1.3] - 2023.11
|
||||||
|
### Added
|
||||||
|
- Contact name editing
|
||||||
|
### Changed
|
||||||
|
- Don't show actions on front page if not registered.
|
||||||
|
### Removed
|
||||||
|
- Home page Notiwind test buttons
|
||||||
|
|
||||||
|
|
||||||
|
## [0.1.2] - 2023.11.01 - 7f6c93802911a030a89fe3706e18b5c17151e5bb
|
||||||
### Added
|
### Added
|
||||||
- Basics: create ID, record a give, declare a project, search, and get notifications.
|
- Basics: create ID, record a give, declare a project, search, and get notifications.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ npm run lint
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
###
|
### Web-push
|
||||||
|
|
||||||
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
|
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
|
||||||
|
|
||||||
@@ -54,8 +54,8 @@ by an existing user:
|
|||||||
On the test server, User #0 has rights to register others, so you can start
|
On the test server, User #0 has rights to register others, so you can start
|
||||||
playing one of two ways:
|
playing one of two ways:
|
||||||
|
|
||||||
- Import the keys for the test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` by importing this seed phrase:
|
- Import the keys for the test User `did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F` by importing this seed phrase:
|
||||||
`seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control`
|
`rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
|
||||||
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
|
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
|
||||||
|
|
||||||
- Alternatively, register someone else under User #0 automatically:
|
- Alternatively, register someone else under User #0 automatically:
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
Prerequisites:
|
JWT Creation & Verification
|
||||||
|
|
||||||
jq
|
To run this in a script, see ./openssl_signing_console.sh
|
||||||
|
|
||||||
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
|
Prerequisites: openssl, jq
|
||||||
|
|
||||||
|
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using
|
||||||
|
a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
|
||||||
|
|
||||||
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
|
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
|
||||||
|
|
||||||
@@ -15,20 +18,22 @@ openssl ec -in private.pem -pubout -out public.pem
|
|||||||
|
|
||||||
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
||||||
|
|
||||||
Next, create a payload object as a JSON object containing the claims you want to include in the JWT. For example schema.org :
|
Next, create a payload object as a JSON object containing the claims you want to include in the JWT.
|
||||||
|
For example schema.org :
|
||||||
|
|
||||||
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
||||||
|
|
||||||
Encode the header and payload objects as base64Url strings. You can use the jq command line utility to do this:
|
Encode the header and payload objects as base64Url strings. You can use the jq command line utility to do this:
|
||||||
|
|
||||||
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
|
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
|
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
|
|
||||||
Concatenate the encoded header, payload, and a secret to create the signing input:
|
Concatenate the encoded header, payload, and a secret to create the signing input:
|
||||||
|
|
||||||
signing_input="$header_b64.$payload_b64"
|
signing_input="$header_b64.$payload_b64"
|
||||||
|
|
||||||
Create the signature by signing the signing input with a ES256K algorithm and your secret. You can use the openssl command line utility to do this:
|
Create the signature by signing the signing input with a ES256K algorithm and your secret.
|
||||||
|
You can use the openssl command line utility to do this:
|
||||||
|
|
||||||
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
|
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
|
||||||
|
|
||||||
@@ -43,7 +48,7 @@ Authorization: Bearer $jwt
|
|||||||
|
|
||||||
To verify the JWT, you can use the openssl utility with the public key:
|
To verify the JWT, you can use the openssl utility with the public key:
|
||||||
|
|
||||||
openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature") "$signing_input"
|
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature")
|
||||||
|
|
||||||
This will verify the signature and output Verified OK if the signature is valid. If the signature is not valid, it will output an error.
|
|
||||||
|
|
||||||
|
This will verify the signature and output "Verified OK" if the signature is valid.
|
||||||
|
If the signature is not valid, it will give an error response and output "Verification failure".
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Generate a JWT, with signature verified using OpenSSL
|
||||||
|
#
|
||||||
|
# Prerequisites: openssl, jq
|
||||||
|
#
|
||||||
|
# Usage: source ./openssl_signing_console.sh
|
||||||
|
#
|
||||||
|
# For a more complete explanation, see ./openssl_signing_console.rst
|
||||||
|
#
|
||||||
|
# It's crazy that raw execution only works about 20% of the time!
|
||||||
|
# See https://stackoverflow.com/questions/77505582/why-would-openssl-verify-succeed-every-time-with-source-but-fail-80-of-the
|
||||||
|
|
||||||
|
|
||||||
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
|
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
|
||||||
openssl ec -in private.pem -pubout -out public.pem
|
openssl ec -in private.pem -pubout -out public.pem
|
||||||
|
|
||||||
@@ -7,19 +19,25 @@ header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
|||||||
|
|
||||||
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
||||||
|
|
||||||
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
|
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
|
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
|
|
||||||
signing_input="$header_b64.$payload_b64"
|
signing_input="$header_b64.$payload_b64"
|
||||||
|
|
||||||
echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem -out signature.bin
|
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
|
||||||
|
|
||||||
# Read binary signature from file and encode it to Base64 URL-Safe format
|
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature")
|
||||||
signature_b64=$(base64 -w 0 < signature.bin | tr -d '=' | tr '+' '-' | tr '/' '_')
|
|
||||||
|
# Also tested this, to no avail.
|
||||||
|
#echo -n "$signature" > sig.out
|
||||||
|
#echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature sig.out
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Read binary signature and encode it to Base64 URL-Safe format
|
||||||
|
signature_b64=$(echo -n "$signature" | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
|
|
||||||
# Construct the JWT
|
# Construct the JWT
|
||||||
jwt="$signing_input.$signature_b64"
|
jwt="$signing_input.$signature_b64"
|
||||||
|
|
||||||
openssl dgst -sha256 -verify public.pem -signature signature.bin -out verified.txt <(echo -n "$signing_input")
|
echo Resulting JWT: $jwt
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
61
package-lock.json
generated
61
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "kickstart-for-time-pwa",
|
"name": "kickstart-for-time-pwa",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "kickstart-for-time-pwa",
|
"name": "kickstart-for-time-pwa",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-facing-decorator": "^3.0.2",
|
"vue-facing-decorator": "^3.0.2",
|
||||||
|
"vue-qrcode-reader": "^5.4.1",
|
||||||
"vue-router": "^4.2.4",
|
"vue-router": "^4.2.4",
|
||||||
"web-did-resolver": "^2.0.27"
|
"web-did-resolver": "^2.0.27"
|
||||||
},
|
},
|
||||||
@@ -8676,6 +8677,16 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/dom-webcodecs": {
|
||||||
|
"version": "0.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.10.tgz",
|
||||||
|
"integrity": "sha512-qQfLMw4yhtagKQApMQKaf21KZeJu3Psysbm/wLQ3mkpyBWY3x3dHCKFcYs43WEH+s8zgTSF0DvJUPWTtyZP0Dw=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/emscripten": {
|
||||||
|
"version": "1.39.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz",
|
||||||
|
"integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw=="
|
||||||
|
},
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "8.44.4",
|
"version": "8.44.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.4.tgz",
|
||||||
@@ -11515,6 +11526,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/barcode-detector": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-yA6gR5u5j22uw2eHSlFGzhYgnnQqx6hc4amDb/r0bKWl2gcDOqVE6SzUE6O87UzJ3ZhjJjM9uG/L9+D705HsKg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/dom-webcodecs": "^0.1.9",
|
||||||
|
"zxing-wasm": "1.0.0-rc.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base-64": {
|
"node_modules/base-64": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
|
||||||
@@ -24637,6 +24657,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
|
||||||
"integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA=="
|
"integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/sdp": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
|
||||||
|
},
|
||||||
"node_modules/secp256k1": {
|
"node_modules/secp256k1": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz",
|
||||||
@@ -27264,6 +27289,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-qrcode-reader": {
|
||||||
|
"version": "5.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-5.4.1.tgz",
|
||||||
|
"integrity": "sha512-jwETIaRdumSCnXOpp0BkpZW8sySNFUfIPNOFa8oHAEmoSSdKK/ub5C1+3vMwokjU8iNERR2v/YhfBdcWDe0s5A==",
|
||||||
|
"dependencies": {
|
||||||
|
"barcode-detector": "2.1.1",
|
||||||
|
"webrtc-adapter": "8.2.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "4.2.5",
|
"version": "4.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz",
|
||||||
@@ -27839,6 +27876,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webrtc-adapter": {
|
||||||
|
"version": "8.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.3.tgz",
|
||||||
|
"integrity": "sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"sdp": "^3.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0",
|
||||||
|
"npm": ">=3.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/websocket-driver": {
|
"node_modules/websocket-driver": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
||||||
@@ -28644,6 +28693,14 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zxing-wasm": {
|
||||||
|
"version": "1.0.0-rc.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-1.0.0-rc.4.tgz",
|
||||||
|
"integrity": "sha512-SvVErHUZhzFqpqA2vpwmXeAPa6sgGdUCOkMCd5cMch6L1urZbZCZR8jb2+NI9bCfJRNkQi2ZjME9/NaiUFiSGg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/emscripten": "^1.39.9"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kickstart-for-time-pwa",
|
"name": "kickstart-for-time-pwa",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-facing-decorator": "^3.0.2",
|
"vue-facing-decorator": "^3.0.2",
|
||||||
|
"vue-qrcode-reader": "^5.4.1",
|
||||||
"vue-router": "^4.2.4",
|
"vue-router": "^4.2.4",
|
||||||
"web-did-resolver": "^2.0.27"
|
"web-did-resolver": "^2.0.27"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,13 +5,6 @@ tasks:
|
|||||||
- 40 notifications :
|
- 40 notifications :
|
||||||
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
||||||
|
|
||||||
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
|
|
||||||
|
|
||||||
- 01 fix the Discovery map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
|
|
||||||
- .1 add instructions for map location selection
|
|
||||||
|
|
||||||
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished
|
|
||||||
|
|
||||||
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
|
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
|
||||||
|
|
||||||
- Home Feed & Quick Give screen :
|
- Home Feed & Quick Give screen :
|
||||||
@@ -21,8 +14,8 @@ tasks:
|
|||||||
|
|
||||||
- 24 Move to Vite assignee:matthew
|
- 24 Move to Vite assignee:matthew
|
||||||
|
|
||||||
|
- .3 fix the Project-location-selection map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
|
||||||
- .5 switch so DiscoverView shows anywhere by default, and no number unless search is done (and maybe a better filter UI, including "mine" to consolidate with ProjectsView)
|
- .5 switch so DiscoverView shows anywhere by default, and no number unless search is done (and maybe a better filter UI, including "mine" to consolidate with ProjectsView)
|
||||||
- .2 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui
|
|
||||||
- .5 Add infinite scroll to gifts on the home page
|
- .5 Add infinite scroll to gifts on the home page
|
||||||
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
|
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
|
||||||
- .2 figure out why endorser-mobile search doesn't find recently created PlanAction
|
- .2 figure out why endorser-mobile search doesn't find recently created PlanAction
|
||||||
@@ -48,7 +41,8 @@ tasks:
|
|||||||
- .5 Display a more appealing confirmation on the map when erasing the marker
|
- .5 Display a more appealing confirmation on the map when erasing the marker
|
||||||
- .5 make a VC details page
|
- .5 make a VC details page
|
||||||
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
|
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
|
||||||
- .1 remove firstName (& lastName) from localStorage
|
- .5 include the hash of the latest commit on help page next to version
|
||||||
|
- .5 remove references to localStorage for projectId (now that it's pulling from the path)
|
||||||
|
|
||||||
- contacts v+ :
|
- contacts v+ :
|
||||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||||
@@ -65,6 +59,7 @@ tasks:
|
|||||||
|
|
||||||
- Release Minimum Viable Product :
|
- Release Minimum Viable Product :
|
||||||
- 08 thorough testing for errors & edge cases
|
- 08 thorough testing for errors & edge cases
|
||||||
|
- 01 ensure ability to recover server remotely, and add redundant access
|
||||||
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
||||||
- Add disclaimers.
|
- Add disclaimers.
|
||||||
- Switch default server to the public server.
|
- Switch default server to the public server.
|
||||||
@@ -84,6 +79,10 @@ 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 add "back" button to all screens that aren't part of the bottom tray
|
||||||
|
- .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
|
||||||
|
|
||||||
- Stats :
|
- Stats :
|
||||||
- 01 point out user's location on the world
|
- 01 point out user's location on the world
|
||||||
- 01 present a credential selected from the stats
|
- 01 present a credential selected from the stats
|
||||||
|
|||||||
173
src/App.vue
173
src/App.vue
@@ -162,17 +162,22 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
|
@click="
|
||||||
|
close(notification.id);
|
||||||
|
turnOnNotifications();
|
||||||
|
"
|
||||||
>
|
>
|
||||||
Turn on Notifications
|
Turn on Notifications
|
||||||
</button>
|
</button>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
@click="close(notification.id)"
|
@click="maybeLater(notification.id)"
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Maybe Later
|
Maybe Later
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
@click="never(notification.id)"
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
|
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Never
|
Never
|
||||||
@@ -254,4 +259,168 @@
|
|||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|
||||||
<script lang="ts"></script>
|
<script lang="ts">
|
||||||
|
import { Vue, Component } from "vue-facing-decorator";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class App extends Vue {
|
||||||
|
b64 = "";
|
||||||
|
mounted() {
|
||||||
|
axios
|
||||||
|
.get("https://timesafari-pwa.anomalistlabs.com/web-push/vapid")
|
||||||
|
.then((response) => {
|
||||||
|
this.b64 = response.data.vapidKey;
|
||||||
|
console.log(this.b64);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("API error", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private askPermission(): Promise<NotificationPermission> {
|
||||||
|
// Check if Notifications are supported
|
||||||
|
if (!("Notification" in window)) {
|
||||||
|
alert("This browser does not support notifications.");
|
||||||
|
return Promise.reject("This browser does not support notifications.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check existing permissions
|
||||||
|
if (Notification.permission === "granted") {
|
||||||
|
return Promise.resolve("granted");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request permission
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const permissionResult = Notification.requestPermission((result) => {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (permissionResult) {
|
||||||
|
permissionResult.then(resolve, reject);
|
||||||
|
}
|
||||||
|
}).then((permissionResult) => {
|
||||||
|
console.log("Permission result:", permissionResult);
|
||||||
|
|
||||||
|
if (permissionResult !== "granted") {
|
||||||
|
alert("We need notification permission to provide certain features.");
|
||||||
|
return Promise.reject("We weren't granted permission.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissionResult;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async turnOnNotifications() {
|
||||||
|
return this.askPermission()
|
||||||
|
.then((permission) => {
|
||||||
|
console.log("Permission granted:", permission);
|
||||||
|
|
||||||
|
// Call the function and handle promises
|
||||||
|
this.subscribeToPush()
|
||||||
|
.then(() => {
|
||||||
|
console.log("Subscribed successfully.");
|
||||||
|
// Assuming the subscription object is available
|
||||||
|
return navigator.serviceWorker.ready;
|
||||||
|
})
|
||||||
|
.then((registration) => {
|
||||||
|
// Fetch the existing subscription object from the registration
|
||||||
|
return registration.pushManager.getSubscription();
|
||||||
|
})
|
||||||
|
.then((subscription) => {
|
||||||
|
if (subscription) {
|
||||||
|
console.log(subscription);
|
||||||
|
return this.sendSubscriptionToServer(subscription);
|
||||||
|
} else {
|
||||||
|
throw new Error("Subscription object is not available.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log("Subscription data sent to server.");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(
|
||||||
|
"Subscription or server communication failed:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("An error occurred:", error);
|
||||||
|
// Handle error appropriately here
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to convert URL base64 to Uint8Array
|
||||||
|
private urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||||
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/-/g, "+")
|
||||||
|
.replace(/_/g, "/");
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The subscribeToPush method
|
||||||
|
private subscribeToPush(): Promise<void> {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if ("serviceWorker" in navigator && "PushManager" in window) {
|
||||||
|
const applicationServerKey = this.urlBase64ToUint8Array(this.b64);
|
||||||
|
const options: PushSubscriptionOptions = {
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: applicationServerKey,
|
||||||
|
};
|
||||||
|
console.log(options);
|
||||||
|
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then((registration) => {
|
||||||
|
return registration.pushManager.subscribe(options);
|
||||||
|
})
|
||||||
|
.then((subscription) => {
|
||||||
|
console.log("Push subscription successful:", subscription);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Push subscription failed:", error, options);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const errorMsg = "Push messaging is not supported";
|
||||||
|
console.warn(errorMsg);
|
||||||
|
reject(new Error(errorMsg));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendSubscriptionToServer(
|
||||||
|
subscription: PushSubscription,
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(subscription);
|
||||||
|
return fetch("/web-push/subscribe", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(subscription),
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to send subscription to server");
|
||||||
|
}
|
||||||
|
console.log("Subscription sent to server successfully.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
never(ID: string) {
|
||||||
|
alert(ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeLater(ID: string) {
|
||||||
|
alert(ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* See also ../libs/veramo/setup.ts
|
* See also ../libs/veramo/setup.ts
|
||||||
*/
|
*/
|
||||||
export enum AppString {
|
export enum AppString {
|
||||||
APP_NAME = "Time Safari",
|
APP_NAME = "TimeSafari",
|
||||||
|
|
||||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||||
|
|||||||
@@ -9,59 +9,39 @@ import {
|
|||||||
} from "./tables/settings";
|
} from "./tables/settings";
|
||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
|
|
||||||
// a separate DB because the seed is super-sensitive data
|
// Define types for tables that hold sensitive and non-sensitive data
|
||||||
type SensitiveTables = {
|
type SensitiveTables = { accounts: Table<Account> };
|
||||||
accounts: Table<Account>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NonsensitiveTables = {
|
type NonsensitiveTables = {
|
||||||
contacts: Table<Contact>;
|
contacts: Table<Contact>;
|
||||||
settings: Table<Settings>;
|
settings: Table<Settings>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
||||||
* In order to make the next line be acceptable, the program needs to have its linter suppress a rule:
|
|
||||||
* https://typescript-eslint.io/rules/no-unnecessary-type-constraint/
|
|
||||||
*
|
|
||||||
* and change *any* to *unknown*
|
|
||||||
*
|
|
||||||
* https://9to5answer.com/how-to-bypass-warning-unexpected-any-specify-a-different-type-typescript-eslint-no-explicit-any
|
|
||||||
*/
|
|
||||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||||
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
|
||||||
const SensitiveSchemas = Object.assign({}, AccountsSchema);
|
|
||||||
|
|
||||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||||
BaseDexie & T;
|
BaseDexie & T;
|
||||||
|
|
||||||
|
// Initialize Dexie databases for sensitive and non-sensitive data
|
||||||
|
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
||||||
|
const SensitiveSchemas = { ...AccountsSchema };
|
||||||
|
|
||||||
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
||||||
const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
|
const NonsensitiveSchemas = { ...ContactsSchema, ...SettingsSchema };
|
||||||
|
|
||||||
/**
|
// Manage the encryption key. If not present in localStorage, create and store it.
|
||||||
* Needed to enable a special webpack setting to allow *await* below:
|
|
||||||
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create password and place password in localStorage.
|
|
||||||
*
|
|
||||||
* It's good practice to keep the data encrypted at rest, so we'll do that even
|
|
||||||
* if the secret is stored right next to the app.
|
|
||||||
*/
|
|
||||||
const secret =
|
const secret =
|
||||||
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
|
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
|
||||||
|
if (!localStorage.getItem("secret")) localStorage.setItem("secret", secret);
|
||||||
|
|
||||||
if (localStorage.getItem("secret") == null) {
|
// Apply encryption to the sensitive database using the secret key
|
||||||
localStorage.setItem("secret", secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
encrypted(accountsDB, { secretKey: secret });
|
encrypted(accountsDB, { secretKey: secret });
|
||||||
accountsDB.version(1).stores(SensitiveSchemas);
|
|
||||||
|
|
||||||
|
// Define the schema for our databases
|
||||||
|
accountsDB.version(1).stores(SensitiveSchemas);
|
||||||
db.version(1).stores(NonsensitiveSchemas);
|
db.version(1).stores(NonsensitiveSchemas);
|
||||||
|
|
||||||
// initialize, a la https://dexie.org/docs/Tutorial/Design#the-populate-event
|
// Event handler to initialize the non-sensitive database with default settings
|
||||||
db.on("populate", function () {
|
db.on("populate", () => {
|
||||||
// ensure there's an initial entry for settings
|
|
||||||
db.settings.add({
|
db.settings.add({
|
||||||
id: MASTER_SETTINGS_KEY,
|
id: MASTER_SETTINGS_KEY,
|
||||||
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
|
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
|
||||||
|
|||||||
@@ -1,29 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* BoundingBox type describes the geographical bounding box coordinates.
|
||||||
|
*/
|
||||||
export type BoundingBox = {
|
export type BoundingBox = {
|
||||||
eastLong: number;
|
eastLong: number; // Eastern longitude
|
||||||
maxLat: number;
|
maxLat: number; // Maximum (Northernmost) latitude
|
||||||
minLat: number;
|
minLat: number; // Minimum (Southernmost) latitude
|
||||||
westLong: number;
|
westLong: number; // Western longitude
|
||||||
};
|
};
|
||||||
|
|
||||||
// a singleton
|
/**
|
||||||
|
* Settings type encompasses user-specific configuration details.
|
||||||
|
*/
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
id: number; // there's only one entry: MASTER_SETTINGS_KEY
|
id: number; // Only one entry using MASTER_SETTINGS_KEY
|
||||||
|
activeDid?: string; // Active Decentralized ID
|
||||||
activeDid?: string;
|
apiServer?: string; // API server URL
|
||||||
apiServer?: string;
|
firstName?: string; // User's first name
|
||||||
firstName?: string;
|
lastName?: string; // User's last name
|
||||||
|
lastViewedClaimId?: string; // Last viewed claim ID
|
||||||
|
lastNotifiedClaimId?: string; // Last notified claim ID
|
||||||
isRegistered?: boolean;
|
isRegistered?: boolean;
|
||||||
lastName?: string; // deprecated, pre v 0.1.3
|
|
||||||
lastViewedClaimId?: string;
|
// Array of named search boxes defined by bounding boxes
|
||||||
|
|
||||||
searchBoxes?: Array<{
|
searchBoxes?: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
bbox: BoundingBox;
|
bbox: BoundingBox;
|
||||||
}>;
|
}>;
|
||||||
showContactGivesInline?: boolean;
|
|
||||||
|
showContactGivesInline?: boolean; // Display contact inline or not
|
||||||
|
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
||||||
|
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
|
||||||
|
reminderOn?: boolean; // Toggle to enable or disable reminders
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema for the Settings table in the database.
|
||||||
|
*/
|
||||||
export const SettingsSchema = {
|
export const SettingsSchema = {
|
||||||
settings: "id",
|
settings: "id",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants.
|
||||||
|
*/
|
||||||
export const MASTER_SETTINGS_KEY = 1;
|
export const MASTER_SETTINGS_KEY = 1;
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/project",
|
path: "/project/:id?",
|
||||||
name: "project",
|
name: "project",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
|
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
|
||||||
@@ -168,6 +168,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
|
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/search-area",
|
||||||
|
name: "search-area",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "search-area" */ "../views/SearchAreaView.vue"
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/seed-backup",
|
path: "/seed-backup",
|
||||||
name: "seed-backup",
|
name: "seed-backup",
|
||||||
|
|||||||
@@ -655,11 +655,11 @@ export default class AccountViewView extends Vue {
|
|||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "toast",
|
type: "success",
|
||||||
title: "Download Started",
|
title: "Download Started",
|
||||||
text: "See your downloads directory for the backup.",
|
text: "See your downloads directory for the backup.",
|
||||||
},
|
},
|
||||||
5000,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
<QuickNav selected="Contacts"></QuickNav>
|
<QuickNav selected="Contacts"></QuickNav>
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1
|
||||||
|
id="ViewBreadcrumb"
|
||||||
|
class="text-lg text-center font-light relative px-7"
|
||||||
|
>
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'contacts' }"
|
:to="{ name: 'contacts' }"
|
||||||
@@ -11,11 +14,12 @@
|
|||||||
><fa icon="chevron-left" class="fa-fw"></fa
|
><fa icon="chevron-left" class="fa-fw"></fa
|
||||||
></router-link>
|
></router-link>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
||||||
|
Given with {{ contact?.name }}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
||||||
Given with {{ contact?.name }}
|
|
||||||
</h1>
|
|
||||||
<div class="flex justify-around">
|
<div class="flex justify-around">
|
||||||
<span />
|
<span />
|
||||||
<span class="justify-around">(Only 50 most recent)</span>
|
<span class="justify-around">(Only 50 most recent)</span>
|
||||||
@@ -358,7 +362,10 @@ export default class ContactsView extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
|
/*
|
||||||
|
Tooltip, generated on "title" attributes on "fa" icons
|
||||||
|
Kudos to https://www.w3schools.com/css/css_tooltip.asp
|
||||||
|
*/
|
||||||
/* Tooltip container */
|
/* Tooltip container */
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -218,7 +218,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-else>This identity has no contacts.</p>
|
<p v-else>There are no contacts.</p>
|
||||||
|
|
||||||
<div v-if="contactEdit !== null" class="dialog-overlay">
|
<div v-if="contactEdit !== null" class="dialog-overlay">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
@@ -1050,7 +1050,10 @@ export default class ContactsView extends Vue {
|
|||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
|
/*
|
||||||
|
Tooltip, generated on "title" attributes on "fa" icons
|
||||||
|
Kudos to https://www.w3schools.com/css/css_tooltip.asp
|
||||||
|
*/
|
||||||
/* Tooltip container */
|
/* Tooltip container */
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -67,46 +67,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isLocalActive">
|
<div v-if="isLocalActive">
|
||||||
<div v-if="!isChoosingSearchBox">
|
<div>
|
||||||
<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="isChoosingSearchBox = true"
|
@click="$router.push({ name: 'search-area' })"
|
||||||
>
|
>
|
||||||
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
|
||||||
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
|
|
||||||
Choose Location Below for Nearby Search
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="storeSearchBox"
|
|
||||||
>
|
|
||||||
Store This Location for Nearby Search
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="searchBox"
|
|
||||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="forgetSearchBox"
|
|
||||||
>
|
|
||||||
Delete Stored Location
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="resetLatLong"
|
|
||||||
>
|
|
||||||
Reset Marker
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="cancelSearchBoxSelect"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading Animation -->
|
<!-- Loading Animation -->
|
||||||
@@ -150,50 +118,11 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="isLocalActive && isChoosingSearchBox"
|
|
||||||
style="height: 600px; width: 800px"
|
|
||||||
>
|
|
||||||
<l-map
|
|
||||||
ref="map"
|
|
||||||
:center="[localCenterLat, localCenterLong]"
|
|
||||||
v-model:zoom="localZoom"
|
|
||||||
@click="setMapPoint"
|
|
||||||
>
|
|
||||||
<l-tile-layer
|
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
||||||
layer-type="base"
|
|
||||||
name="OpenStreetMap"
|
|
||||||
/>
|
|
||||||
<l-marker
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
:lat-lng="[localCenterLat, localCenterLong]"
|
|
||||||
@click="isNewMarkerSet = false"
|
|
||||||
/>
|
|
||||||
<l-rectangle
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
:bounds="[
|
|
||||||
[localCenterLat - localLatDiff, localCenterLong - localLongDiff],
|
|
||||||
[localCenterLat + localLatDiff, localCenterLong + localLongDiff],
|
|
||||||
]"
|
|
||||||
:weight="1"
|
|
||||||
/>
|
|
||||||
</l-map>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { LeafletMouseEvent } from "leaflet";
|
|
||||||
import "leaflet/dist/leaflet.css";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import {
|
|
||||||
LMap,
|
|
||||||
LMarker,
|
|
||||||
LRectangle,
|
|
||||||
LTileLayer,
|
|
||||||
} from "@vue-leaflet/vue-leaflet";
|
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
@@ -204,10 +133,6 @@ 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";
|
||||||
|
|
||||||
const DEFAULT_LAT_LONG_DIFF = 0.01;
|
|
||||||
const WORLD_ZOOM = 2;
|
|
||||||
const DEFAULT_ZOOM = 2;
|
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -217,13 +142,9 @@ interface Notification {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
LRectangle,
|
|
||||||
QuickNav,
|
QuickNav,
|
||||||
InfiniteScroll,
|
InfiniteScroll,
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
LMap,
|
|
||||||
LMarker,
|
|
||||||
LTileLayer,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class DiscoverView extends Vue {
|
export default class DiscoverView extends Vue {
|
||||||
@@ -238,13 +159,7 @@ export default class DiscoverView extends Vue {
|
|||||||
isChoosingSearchBox = false;
|
isChoosingSearchBox = false;
|
||||||
isLocalActive = true;
|
isLocalActive = true;
|
||||||
isRemoteActive = false;
|
isRemoteActive = false;
|
||||||
isNewMarkerSet = false;
|
|
||||||
localCenterLat = 0;
|
|
||||||
localCenterLong = 0;
|
|
||||||
localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
localCount = 0;
|
localCount = 0;
|
||||||
localZoom = DEFAULT_ZOOM;
|
|
||||||
remoteCount = 0;
|
remoteCount = 0;
|
||||||
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
@@ -258,7 +173,6 @@ export default class DiscoverView extends Vue {
|
|||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.searchBox = settings?.searchBoxes?.[0] || null;
|
this.searchBox = settings?.searchBoxes?.[0] || null;
|
||||||
this.resetLatLong();
|
|
||||||
|
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
@@ -462,128 +376,6 @@ export default class DiscoverView extends Vue {
|
|||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
setMapPoint(event: LeafletMouseEvent) {
|
|
||||||
if (this.isNewMarkerSet) {
|
|
||||||
this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat);
|
|
||||||
this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong);
|
|
||||||
} else {
|
|
||||||
// marker is not set
|
|
||||||
this.localCenterLat = event.latlng.lat;
|
|
||||||
this.localCenterLong = event.latlng.lng;
|
|
||||||
|
|
||||||
let latDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
let longDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
// Guess at a size for the bounding box.
|
|
||||||
// This doesn't seem like the right approach but it's the only way I can find to get the screen bounds.
|
|
||||||
const bounds = event.target.boxZoom?._map?.getBounds();
|
|
||||||
if (bounds) {
|
|
||||||
latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8;
|
|
||||||
longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8;
|
|
||||||
}
|
|
||||||
this.localLatDiff = latDiff;
|
|
||||||
this.localLongDiff = longDiff;
|
|
||||||
this.isNewMarkerSet = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetLatLong() {
|
|
||||||
if (this.searchBox?.bbox) {
|
|
||||||
const bbox = this.searchBox.bbox;
|
|
||||||
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
|
|
||||||
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
|
|
||||||
this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2;
|
|
||||||
this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2;
|
|
||||||
this.localZoom = WORLD_ZOOM;
|
|
||||||
this.isNewMarkerSet = true;
|
|
||||||
} else {
|
|
||||||
this.isNewMarkerSet = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async storeSearchBox() {
|
|
||||||
if (this.localCenterLong || this.localCenterLat) {
|
|
||||||
try {
|
|
||||||
const newSearchBox = {
|
|
||||||
name: "Local",
|
|
||||||
bbox: {
|
|
||||||
eastLong: this.localCenterLong + this.localLongDiff,
|
|
||||||
maxLat: this.localCenterLat + this.localLatDiff,
|
|
||||||
minLat: this.localCenterLat - this.localLatDiff,
|
|
||||||
westLong: this.localCenterLong - this.localLongDiff,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await db.open();
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
searchBoxes: [newSearchBox],
|
|
||||||
});
|
|
||||||
this.searchBox = newSearchBox;
|
|
||||||
this.isChoosingSearchBox = false;
|
|
||||||
this.searchLocal();
|
|
||||||
} catch (err) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Updating Search Settings",
|
|
||||||
text: "Try going to a different page and then coming back.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"Telling user to retry the location search setting because:",
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "warning",
|
|
||||||
title: "No Location Selected",
|
|
||||||
text: "Select a location on the map.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async forgetSearchBox() {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
searchBoxes: [],
|
|
||||||
});
|
|
||||||
this.searchBox = null;
|
|
||||||
this.localCenterLat = 0;
|
|
||||||
this.localCenterLong = 0;
|
|
||||||
this.localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
this.localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
this.localZoom = DEFAULT_ZOOM;
|
|
||||||
this.isChoosingSearchBox = false;
|
|
||||||
this.isNewMarkerSet = false;
|
|
||||||
this.searchLocal();
|
|
||||||
} catch (err) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Updating Search Settings",
|
|
||||||
text: "Try going to a different page and then coming back.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"Telling user to retry the location search setting because:",
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public cancelSearchBoxSelect() {
|
|
||||||
this.isChoosingSearchBox = false;
|
|
||||||
this.localZoom = WORLD_ZOOM;
|
|
||||||
}
|
|
||||||
|
|
||||||
public computedLocalTabClassNames() {
|
public computedLocalTabClassNames() {
|
||||||
return {
|
return {
|
||||||
"inline-block": true,
|
"inline-block": true,
|
||||||
|
|||||||
@@ -50,6 +50,11 @@
|
|||||||
<label for="includeLocation">Include Location</label>
|
<label for="includeLocation">Include Location</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
||||||
|
<div class="px-2 py-2">
|
||||||
|
For your security, we recommend you choose a location nearby but not
|
||||||
|
exactly at the place.
|
||||||
|
</div>
|
||||||
|
|
||||||
<l-map
|
<l-map
|
||||||
ref="map"
|
ref="map"
|
||||||
v-model:zoom="zoom"
|
v-model:zoom="zoom"
|
||||||
|
|||||||
@@ -137,10 +137,12 @@
|
|||||||
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4">
|
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
Given to this Project
|
Given To This Project
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<ul class="text-sm border-t border-slate-300">
|
<div v-if="givesToThis.length === 0">(None yet. Record one above.)</div>
|
||||||
|
|
||||||
|
<ul v-else class="text-sm border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
v-for="give in givesToThis"
|
v-for="give in givesToThis"
|
||||||
:key="give.id"
|
:key="give.id"
|
||||||
@@ -164,31 +166,33 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
…and from this Project
|
Contributions By This Project
|
||||||
</h3>
|
</h3>
|
||||||
|
<button
|
||||||
|
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
||||||
|
class="text-blue-500"
|
||||||
|
>
|
||||||
|
{{ fulfilledByThis.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul class="text-sm border-t border-slate-300">
|
<div
|
||||||
<li
|
v-if="fulfillersToThis.length > 0"
|
||||||
v-for="give in givesByThis"
|
class="bg-slate-100 px-4 py-3 rounded-md"
|
||||||
:key="give.id"
|
>
|
||||||
class="py-1.5 border-b border-slate-300"
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
>
|
Contributions To This Project
|
||||||
<div class="flex justify-between gap-4">
|
</h3>
|
||||||
<span
|
<ul>
|
||||||
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
<li v-for="plan in fulfillersToThis" :key="plan.handleId">
|
||||||
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
<button
|
||||||
</span>
|
@click="onClickLoadProject(plan.handleId)"
|
||||||
<span v-if="give.amount"
|
class="text-blue-500"
|
||||||
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
>
|
||||||
{{ give.amount }}
|
{{ plan.name }}
|
||||||
</span>
|
</button>
|
||||||
</div>
|
|
||||||
<div v-if="give.description" class="text-slate-500">
|
|
||||||
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
|
||||||
{{ give.description }}
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,6 +222,7 @@ import {
|
|||||||
didInfo,
|
didInfo,
|
||||||
GiverInputInfo,
|
GiverInputInfo,
|
||||||
GiveServerRecord,
|
GiveServerRecord,
|
||||||
|
PlanServerRecord,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
@@ -242,8 +247,9 @@ export default class ProjectViewView extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
description = "";
|
description = "";
|
||||||
expanded = false;
|
expanded = false;
|
||||||
|
fulfilledByThis: PlanServerRecord | null = null;
|
||||||
|
fulfillersToThis: Array<PlanServerRecord> = [];
|
||||||
givesToThis: Array<GiveServerRecord> = [];
|
givesToThis: Array<GiveServerRecord> = [];
|
||||||
givesByThis: Array<GiveServerRecord> = [];
|
|
||||||
latitude = 0;
|
latitude = 0;
|
||||||
longitude = 0;
|
longitude = 0;
|
||||||
name = "";
|
name = "";
|
||||||
@@ -266,7 +272,12 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.allMyDids = accountsArr.map((acc) => acc.did);
|
this.allMyDids = accountsArr.map((acc) => acc.did);
|
||||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
this.LoadProject(identity);
|
|
||||||
|
const pathParam = window.location.pathname.substring("/project/".length);
|
||||||
|
if (pathParam) {
|
||||||
|
this.projectId = decodeURIComponent(pathParam);
|
||||||
|
}
|
||||||
|
this.LoadProject(this.projectId, identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
public async getIdentity(activeDid: string) {
|
||||||
@@ -320,11 +331,11 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.expanded = false;
|
this.expanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async LoadProject(identity: IIdentifier) {
|
async LoadProject(projectId: string, identity: IIdentifier) {
|
||||||
|
this.projectId = projectId;
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
this.apiServer +
|
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId);
|
||||||
"/api/claim/byHandle/" +
|
|
||||||
encodeURIComponent(this.projectId);
|
|
||||||
const headers: RawAxiosRequestHeaders = {
|
const headers: RawAxiosRequestHeaders = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
@@ -389,7 +400,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
const givesInUrl =
|
const givesInUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/givesForPlans?planIds=" +
|
"/api/v2/report/givesForPlans?planIds=" +
|
||||||
encodeURIComponent(JSON.stringify([this.projectId]));
|
encodeURIComponent(JSON.stringify([projectId]));
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(givesInUrl, { headers });
|
const resp = await this.axios.get(givesInUrl, { headers });
|
||||||
if (resp.status === 200 && resp.data.data) {
|
if (resp.status === 200 && resp.data.data) {
|
||||||
@@ -422,21 +433,21 @@ export default class ProjectViewView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const givesOutUrl =
|
const fulfilledByUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/givesProvidedBy?providerId=" +
|
"/api/v2/report/planFulfilledByPlan?planHandleId=" +
|
||||||
encodeURIComponent(this.projectId);
|
encodeURIComponent(projectId);
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(givesOutUrl, { headers });
|
const resp = await this.axios.get(fulfilledByUrl, { headers });
|
||||||
if (resp.status === 200 && resp.data.data) {
|
if (resp.status === 200) {
|
||||||
this.givesByThis = resp.data.data;
|
this.fulfilledByThis = resp.data.data;
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Failed to retrieve gives by this project.",
|
text: "Failed to retrieve plans fulfilled by this project.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -448,15 +459,64 @@ export default class ProjectViewView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Something went wrong retrieving gives by project.",
|
text: "Something went wrong retrieving plans fulfilled by this project.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
console.error(
|
console.error(
|
||||||
"Error retrieving gives by this project:",
|
"Error retrieving plans fulfilled by this project:",
|
||||||
serverError.message,
|
serverError.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fulfillersToUrl =
|
||||||
|
this.apiServer +
|
||||||
|
"/api/v2/report/planFulfillersToPlan?planHandleId=" +
|
||||||
|
encodeURIComponent(projectId);
|
||||||
|
try {
|
||||||
|
const resp = await this.axios.get(fulfillersToUrl, { headers });
|
||||||
|
if (resp.status === 200) {
|
||||||
|
this.fulfillersToThis = resp.data.data;
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to retrieve plan fulfillers to this project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const serverError = error as AxiosError;
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Something went wrong retrieving plan fulfillers to this project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Error retrieving plan fulfillers to this project:",
|
||||||
|
serverError.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle clicking on a project entry found in the list
|
||||||
|
* @param id of the project
|
||||||
|
**/
|
||||||
|
async onClickLoadProject(projectId: string) {
|
||||||
|
localStorage.setItem("projectId", projectId);
|
||||||
|
const route = {
|
||||||
|
path: "/project/" + encodeURIComponent(projectId),
|
||||||
|
};
|
||||||
|
this.$router.push(route);
|
||||||
|
this.LoadProject(projectId, await this.getIdentity(this.activeDid));
|
||||||
}
|
}
|
||||||
|
|
||||||
getOpenStreetMapUrl() {
|
getOpenStreetMapUrl() {
|
||||||
|
|||||||
281
src/views/SearchAreaView.vue
Normal file
281
src/views/SearchAreaView.vue
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
<template>
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<!-- Back -->
|
||||||
|
<div class="text-lg text-center font-light relative px-7">
|
||||||
|
<h1
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
@click="$router.back()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Heading -->
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
|
Area for Nearby Search
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-2 py-4">
|
||||||
|
This location is only stored on your device. It is used to show you more
|
||||||
|
appropriate projects but is not stored on any servers.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
|
||||||
|
Click to Choose a Location for Nearby Search
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="storeSearchBox"
|
||||||
|
>
|
||||||
|
Store This Location for Nearby Search
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="searchBox"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="forgetSearchBox"
|
||||||
|
>
|
||||||
|
Delete Stored Location
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="searchBox"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="resetLatLong"
|
||||||
|
>
|
||||||
|
Reset Marker
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="isNewMarkerSet = false"
|
||||||
|
>
|
||||||
|
Erase Marker
|
||||||
|
</button>
|
||||||
|
<div v-if="isNewMarkerSet">
|
||||||
|
Click on the pin to erase it. Click anywhere else to set a different
|
||||||
|
different corner.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="height: 600px; width: 800px">
|
||||||
|
<l-map
|
||||||
|
ref="map"
|
||||||
|
:center="[localCenterLat, localCenterLong]"
|
||||||
|
v-model:zoom="localZoom"
|
||||||
|
@click="setMapPoint"
|
||||||
|
>
|
||||||
|
<l-tile-layer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
layer-type="base"
|
||||||
|
name="OpenStreetMap"
|
||||||
|
/>
|
||||||
|
<l-marker
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
:lat-lng="[localCenterLat, localCenterLong]"
|
||||||
|
@click="isNewMarkerSet = false"
|
||||||
|
/>
|
||||||
|
<l-rectangle
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
:bounds="[
|
||||||
|
[localCenterLat - localLatDiff, localCenterLong - localLongDiff],
|
||||||
|
[localCenterLat + localLatDiff, localCenterLong + localLongDiff],
|
||||||
|
]"
|
||||||
|
:weight="1"
|
||||||
|
/>
|
||||||
|
</l-map>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { LeafletMouseEvent } from "leaflet";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import {
|
||||||
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LRectangle,
|
||||||
|
LTileLayer,
|
||||||
|
} from "@vue-leaflet/vue-leaflet";
|
||||||
|
|
||||||
|
import { db } from "@/db/index";
|
||||||
|
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
|
const DEFAULT_LAT_LONG_DIFF = 0.01;
|
||||||
|
const WORLD_ZOOM = 2;
|
||||||
|
const DEFAULT_ZOOM = 2;
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
LRectangle,
|
||||||
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LTileLayer,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class DiscoverView extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
isChoosingSearchBox = false;
|
||||||
|
isNewMarkerSet = false;
|
||||||
|
|
||||||
|
// "local" vars are for the currently selected map box
|
||||||
|
localCenterLat = 0;
|
||||||
|
localCenterLong = 0;
|
||||||
|
localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
localZoom = DEFAULT_ZOOM;
|
||||||
|
|
||||||
|
// searchBox reflects what is stored in the database
|
||||||
|
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
this.searchBox = settings?.searchBoxes?.[0] || null;
|
||||||
|
this.resetLatLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
setMapPoint(event: LeafletMouseEvent) {
|
||||||
|
if (this.isNewMarkerSet) {
|
||||||
|
this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat);
|
||||||
|
this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong);
|
||||||
|
} else {
|
||||||
|
// marker is not set
|
||||||
|
this.localCenterLat = event.latlng.lat;
|
||||||
|
this.localCenterLong = event.latlng.lng;
|
||||||
|
|
||||||
|
let latDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
let longDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
// Guess at a size for the bounding box.
|
||||||
|
// This doesn't seem like the right approach but it's the only way I can find to get the screen bounds.
|
||||||
|
const bounds = event.target.boxZoom?._map?.getBounds();
|
||||||
|
if (bounds) {
|
||||||
|
latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8;
|
||||||
|
longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8;
|
||||||
|
}
|
||||||
|
this.localLatDiff = latDiff;
|
||||||
|
this.localLongDiff = longDiff;
|
||||||
|
this.isNewMarkerSet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetLatLong() {
|
||||||
|
if (this.searchBox?.bbox) {
|
||||||
|
const bbox = this.searchBox.bbox;
|
||||||
|
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
|
||||||
|
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
|
||||||
|
this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2;
|
||||||
|
this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2;
|
||||||
|
this.localZoom = WORLD_ZOOM;
|
||||||
|
this.isNewMarkerSet = true;
|
||||||
|
} else {
|
||||||
|
this.isNewMarkerSet = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async storeSearchBox() {
|
||||||
|
if (this.localCenterLong || this.localCenterLat) {
|
||||||
|
try {
|
||||||
|
const newSearchBox = {
|
||||||
|
name: "Local",
|
||||||
|
bbox: {
|
||||||
|
eastLong: this.localCenterLong + this.localLongDiff,
|
||||||
|
maxLat: this.localCenterLat + this.localLatDiff,
|
||||||
|
minLat: this.localCenterLat - this.localLatDiff,
|
||||||
|
westLong: this.localCenterLong - this.localLongDiff,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
searchBoxes: [newSearchBox],
|
||||||
|
});
|
||||||
|
this.searchBox = newSearchBox;
|
||||||
|
this.isChoosingSearchBox = false;
|
||||||
|
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Saved",
|
||||||
|
text: "That has been saved in your preferences.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Search Settings",
|
||||||
|
text: "Try going to a different page and then coming back.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to retry the location search setting because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "No Location Selected",
|
||||||
|
text: "Select a location on the map.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async forgetSearchBox() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
searchBoxes: [],
|
||||||
|
});
|
||||||
|
this.searchBox = null;
|
||||||
|
this.localCenterLat = 0;
|
||||||
|
this.localCenterLong = 0;
|
||||||
|
this.localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
this.localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
this.localZoom = DEFAULT_ZOOM;
|
||||||
|
this.isChoosingSearchBox = false;
|
||||||
|
this.isNewMarkerSet = false;
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Search Settings",
|
||||||
|
text: "Try going to a different page and then coming back.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to retry the location search setting because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelSearchBoxSelect() {
|
||||||
|
this.isChoosingSearchBox = false;
|
||||||
|
this.localZoom = WORLD_ZOOM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -16,14 +16,14 @@
|
|||||||
{
|
{
|
||||||
group: 'alert',
|
group: 'alert',
|
||||||
type: 'toast',
|
type: 'toast',
|
||||||
text: 'I\'m a toast. Don\'t mind me.',
|
text: 'I\'m a toast. Without a timeout, I\'m stuck.',
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
class="font-bold uppercase bg-slate-400 text-white px-3 py-2 rounded-md mr-2"
|
class="font-bold uppercase bg-slate-900 text-white px-3 py-2 rounded-md mr-2"
|
||||||
>
|
>
|
||||||
Toast (self-dismiss)
|
Toast
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
33
sw_scripts/additional-scripts.js
Normal file
33
sw_scripts/additional-scripts.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const notifications = require("./safari-notifications.js");
|
||||||
|
|
||||||
|
|
||||||
|
self.addEventListener("push", function (event) {
|
||||||
|
let payload;
|
||||||
|
if (event.data) {
|
||||||
|
payload = JSON.parse(event.data.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = payload ? payload.title : "Custom Title";
|
||||||
|
const options = {
|
||||||
|
body: payload ? payload.body : "Custom body text",
|
||||||
|
icon: payload ? payload.icon : "icon.png",
|
||||||
|
badge: payload ? payload.badge : "badge.png",
|
||||||
|
};
|
||||||
|
|
||||||
|
event.waitUntil(self.registration.showNotification(title, options));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
self.addEventListener("message", function (event) {
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
const result = notifications.getNotificationCount()
|
||||||
|
|
||||||
|
switch (data.command) {
|
||||||
|
case "account":
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log("Unknown command:", data.command);
|
||||||
|
}
|
||||||
|
});
|
||||||
5157
sw_scripts/safari-notifications.js
Normal file
5157
sw_scripts/safari-notifications.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,5 +11,8 @@ module.exports = defineConfig({
|
|||||||
iconPaths: {
|
iconPaths: {
|
||||||
faviconSVG: "img/icons/safari-pinned-tab.svg",
|
faviconSVG: "img/icons/safari-pinned-tab.svg",
|
||||||
},
|
},
|
||||||
|
workboxOptions: {
|
||||||
|
importScripts: ["additional-scripts.js"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ from the SERVICE.
|
|||||||
The SERVICE will provide context and obtain explicit permission before prompting
|
The SERVICE will provide context and obtain explicit permission before prompting
|
||||||
for notification permission:
|
for notification permission:
|
||||||
|
|
||||||
In order to provide this context and explict permission a two-step opt-in process
|
In order to provide this context and explicit permission, a two-step opt-in process
|
||||||
where the user is first presented with a pre-permission dialog box that explains
|
first presents the user with a pre-permission dialog box that explains
|
||||||
what the notifications are for and why they are useful. This may help reduce the
|
what the notifications are for and why they are useful. This may help reduce the
|
||||||
possibility of users clicking "don't allow".
|
possibility of users clicking "don't allow".
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ The `sw.js` file contains the logic for what a service worker should do.
|
|||||||
It executes in a separate thread of execution from the web page but provides a
|
It executes in a separate thread of execution from the web page but provides a
|
||||||
means of communicating between itself and the web page via messages.
|
means of communicating between itself and the web page via messages.
|
||||||
|
|
||||||
Note that there is a scope can specify what network requests it may
|
Note that there is a scope that can specify what network requests it may
|
||||||
intercept.
|
intercept.
|
||||||
|
|
||||||
The Vue project already has its own service worker but it is possible to
|
The Vue project already has its own service worker but it is possible to
|
||||||
|
|||||||
Reference in New Issue
Block a user