Compare commits
9 Commits
home-view-
...
openssl
| Author | SHA1 | Date | |
|---|---|---|---|
| 92fcffdfc5 | |||
| 5f5562f5e3 | |||
| 74ed025377 | |||
| f36ecfd8db | |||
| fc70a11bd8 | |||
| 73f890beac | |||
| 67dce9e678 | |||
| 2b66ddfb83 | |||
| 56fc2893a2 |
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]
|
||||
|
||||
|
||||
## [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
|
||||
- Basics: create ID, record a give, declare a project, search, and get notifications.
|
||||
|
||||
@@ -28,7 +28,7 @@ npm run lint
|
||||
|
||||
## Tests
|
||||
|
||||
###
|
||||
### Web-push
|
||||
|
||||
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
|
||||
playing one of two ways:
|
||||
|
||||
- Import the keys for the test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` 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`
|
||||
- Import the keys for the test User `did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F` by importing this seed phrase:
|
||||
`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).)
|
||||
|
||||
- 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:
|
||||
|
||||
@@ -15,20 +18,22 @@ openssl ec -in private.pem -pubout -out public.pem
|
||||
|
||||
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"}'
|
||||
|
||||
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')
|
||||
payload_b64=$(echo -n "$payload" | 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' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||
|
||||
Concatenate the encoded header, payload, and a secret to create the signing input:
|
||||
|
||||
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)
|
||||
|
||||
@@ -43,7 +48,7 @@ Authorization: Bearer $jwt
|
||||
|
||||
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"
|
||||
|
||||
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.
|
||||
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 give an error response and output "Verification failure".
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
#!/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 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"}'
|
||||
|
||||
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
|
||||
payload_b64=$(echo -n "$payload" | 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' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||
|
||||
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
|
||||
signature_b64=$(base64 -w 0 < signature.bin | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature")
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
82
package-lock.json
generated
82
package-lock.json
generated
@@ -12,7 +12,6 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||
"@lionello/secp256k1-js": "^1.1.0",
|
||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||
"@tweenjs/tween.js": "^21.0.0",
|
||||
"@veramo/core": "^5.4.1",
|
||||
@@ -31,13 +30,11 @@
|
||||
"dexie": "^3.2.4",
|
||||
"dexie-export-import": "^4.0.7",
|
||||
"did-jwt": "^7.2.7",
|
||||
"elliptic": "^6.5.4",
|
||||
"ethereum-cryptography": "^2.1.2",
|
||||
"ethereumjs-util": "^7.1.5",
|
||||
"ethr-did-resolver": "^8.1.2",
|
||||
"jdenticon": "^3.2.0",
|
||||
"js-generate-password": "^0.1.9",
|
||||
"jssha": "^3.3.1",
|
||||
"localstorage-slim": "^2.5.0",
|
||||
"luxon": "^3.4.3",
|
||||
"merkletreejs": "^0.3.10",
|
||||
@@ -75,13 +72,13 @@
|
||||
"@vue/cli-service": "~5.0.8",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"postcss": "^8.4.29",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.0.3",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "~5.2.2"
|
||||
}
|
||||
@@ -2824,9 +2821,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz",
|
||||
"integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
|
||||
"integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
@@ -2874,9 +2871,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz",
|
||||
"integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz",
|
||||
"integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
@@ -5501,12 +5498,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.13",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
|
||||
"version": "0.11.11",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
|
||||
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@humanwhocodes/object-schema": "^2.0.1",
|
||||
"@humanwhocodes/object-schema": "^1.2.1",
|
||||
"debug": "^4.1.1",
|
||||
"minimatch": "^3.0.5"
|
||||
},
|
||||
@@ -5528,9 +5525,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/object-schema": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
|
||||
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
|
||||
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jest/create-cache-key-function": {
|
||||
@@ -6061,22 +6058,6 @@
|
||||
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@lionello/secp256k1-js": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lionello/secp256k1-js/-/secp256k1-js-1.1.0.tgz",
|
||||
"integrity": "sha512-frvrdwwgWm0gq43rYcGwwZee+k5sX9HfWnB80740h1dvRT0FO0Z6Wt5C5I7bS0dXKy1pt6IomkR1VcXirP+g4Q==",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.11.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lionello/secp256k1-js/node_modules/bn.js": {
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.3.0.tgz",
|
||||
@@ -9225,12 +9206,6 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@unimodules/core": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@unimodules/core/-/core-7.1.2.tgz",
|
||||
@@ -14245,19 +14220,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.53.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz",
|
||||
"integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz",
|
||||
"integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/eslintrc": "^2.1.3",
|
||||
"@eslint/js": "8.53.0",
|
||||
"@humanwhocodes/config-array": "^0.11.13",
|
||||
"@eslint/eslintrc": "^2.1.2",
|
||||
"@eslint/js": "8.51.0",
|
||||
"@humanwhocodes/config-array": "^0.11.11",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"@ungap/structured-clone": "^1.2.0",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
@@ -18885,14 +18859,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jssha": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz",
|
||||
"integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/keccak": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz",
|
||||
@@ -23171,9 +23137,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
|
||||
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
|
||||
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||
"@lionello/secp256k1-js": "^1.1.0",
|
||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||
"@tweenjs/tween.js": "^21.0.0",
|
||||
"@veramo/core": "^5.4.1",
|
||||
@@ -31,13 +30,11 @@
|
||||
"dexie": "^3.2.4",
|
||||
"dexie-export-import": "^4.0.7",
|
||||
"did-jwt": "^7.2.7",
|
||||
"elliptic": "^6.5.4",
|
||||
"ethereum-cryptography": "^2.1.2",
|
||||
"ethereumjs-util": "^7.1.5",
|
||||
"ethr-did-resolver": "^8.1.2",
|
||||
"jdenticon": "^3.2.0",
|
||||
"js-generate-password": "^0.1.9",
|
||||
"jssha": "^3.3.1",
|
||||
"localstorage-slim": "^2.5.0",
|
||||
"luxon": "^3.4.3",
|
||||
"merkletreejs": "^0.3.10",
|
||||
@@ -75,13 +72,13 @@
|
||||
"@vue/cli-service": "~5.0.8",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"postcss": "^8.4.29",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.0.3",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "~5.2.2"
|
||||
}
|
||||
|
||||
@@ -5,13 +5,6 @@ tasks:
|
||||
- 40 notifications :
|
||||
- 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)
|
||||
|
||||
- Home Feed & Quick Give screen :
|
||||
@@ -21,8 +14,8 @@ tasks:
|
||||
|
||||
- 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)
|
||||
- .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 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
|
||||
@@ -48,7 +41,8 @@ tasks:
|
||||
- .5 Display a more appealing confirmation on the map when erasing the marker
|
||||
- .5 make a VC details page
|
||||
- .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+ :
|
||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||
@@ -65,6 +59,7 @@ tasks:
|
||||
|
||||
- Release Minimum Viable Product :
|
||||
- 08 thorough testing for errors & edge cases
|
||||
- 01 ensure ability to recover server remotely, and add redundant access
|
||||
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
||||
- Add disclaimers.
|
||||
- Switch default server to the public server.
|
||||
@@ -84,6 +79,10 @@ tasks:
|
||||
- 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)
|
||||
|
||||
- .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 :
|
||||
- 01 point out user's location on the world
|
||||
- 01 present a credential selected from the stats
|
||||
|
||||
178
src/App.vue
178
src/App.vue
@@ -261,30 +261,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
import axios, { AxiosError } from "axios";
|
||||
|
||||
interface ServiceWorkerMessage {
|
||||
type: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface ServiceWorkerResponse {
|
||||
// Define the properties and their types
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// Example interface for error
|
||||
interface ErrorResponse {
|
||||
message: string;
|
||||
// Other properties as needed
|
||||
}
|
||||
|
||||
interface VapidResponse {
|
||||
data: {
|
||||
vapidKey: string;
|
||||
};
|
||||
}
|
||||
import axios from "axios";
|
||||
|
||||
@Component
|
||||
export default class App extends Vue {
|
||||
@@ -292,87 +269,45 @@ export default class App extends Vue {
|
||||
mounted() {
|
||||
axios
|
||||
.get("https://timesafari-pwa.anomalistlabs.com/web-push/vapid")
|
||||
.then((response: VapidResponse) => {
|
||||
.then((response) => {
|
||||
this.b64 = response.data.vapidKey;
|
||||
console.log(this.b64);
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
console.log("New service worker is now controlling the page");
|
||||
});
|
||||
})
|
||||
.catch((error: AxiosError) => {
|
||||
.catch((error) => {
|
||||
console.error("API error", error);
|
||||
});
|
||||
}
|
||||
|
||||
private sendMessageToServiceWorker(
|
||||
message: ServiceWorkerMessage,
|
||||
): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
const messageChannel = new MessageChannel();
|
||||
|
||||
messageChannel.port1.onmessage = (event: MessageEvent) => {
|
||||
if (event.data.error) {
|
||||
reject(event.data.error as ErrorResponse);
|
||||
} else {
|
||||
resolve(event.data as ServiceWorkerResponse);
|
||||
}
|
||||
};
|
||||
|
||||
navigator.serviceWorker.controller.postMessage(message, [
|
||||
messageChannel.port2,
|
||||
]);
|
||||
} else {
|
||||
reject("Service worker controller not available");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private askPermission(): Promise<NotificationPermission> {
|
||||
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
|
||||
return Promise.reject("Service worker not available.");
|
||||
}
|
||||
|
||||
const secret = localStorage.getItem("secret");
|
||||
if (!secret) {
|
||||
return Promise.reject("No secret found.");
|
||||
}
|
||||
|
||||
return this.sendSecretToServiceWorker(secret)
|
||||
.then(() => this.checkNotificationSupport())
|
||||
.then(() => this.requestNotificationPermission())
|
||||
.catch((error) => Promise.reject(error));
|
||||
}
|
||||
|
||||
private sendSecretToServiceWorker(secret: string): Promise<void> {
|
||||
const message: ServiceWorkerMessage = {
|
||||
type: "SEND_LOCAL_DATA",
|
||||
data: secret,
|
||||
};
|
||||
|
||||
return this.sendMessageToServiceWorker(message).then((response) => {
|
||||
console.log("Response from service worker:", response);
|
||||
});
|
||||
}
|
||||
|
||||
private checkNotificationSupport(): Promise<void> {
|
||||
// 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.");
|
||||
}
|
||||
if (Notification.permission === "granted") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private requestNotificationPermission(): Promise<NotificationPermission> {
|
||||
return Notification.requestPermission().then((permission) => {
|
||||
if (permission !== "granted") {
|
||||
alert("We need notification permission to provide certain features.");
|
||||
throw new Error("We weren't granted permission.");
|
||||
// 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);
|
||||
}
|
||||
return permission;
|
||||
}).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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -431,49 +366,34 @@ export default class App extends Vue {
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
// The subscribeToPush method
|
||||
private subscribeToPush(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!("serviceWorker" in navigator && "PushManager" in window)) {
|
||||
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);
|
||||
return reject(new Error(errorMsg));
|
||||
reject(new Error(errorMsg));
|
||||
}
|
||||
|
||||
if (Notification.permission !== "granted") {
|
||||
const errorMsg = "Notification permission not granted";
|
||||
console.warn(errorMsg);
|
||||
return reject(new Error(errorMsg));
|
||||
}
|
||||
|
||||
const applicationServerKey = this.urlBase64ToUint8Array(this.b64);
|
||||
const options: PushSubscriptionOptions = {
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: applicationServerKey,
|
||||
};
|
||||
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
return registration.pushManager.subscribe(options);
|
||||
})
|
||||
.then((subscription) => {
|
||||
console.log("Push subscription successful:", subscription);
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"Subscription or server communication failed:",
|
||||
error,
|
||||
options,
|
||||
);
|
||||
|
||||
// Inform the user about the issue
|
||||
alert(
|
||||
"We encountered an issue setting up push notifications. " +
|
||||
"If you wish to revoke notification permissions, please do so in your browser settings.",
|
||||
);
|
||||
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -134,8 +134,8 @@ export function didInfo(
|
||||
return contact
|
||||
? contact.name || "Contact With No Name"
|
||||
: isHiddenDid(did)
|
||||
? "Someone Not In Network"
|
||||
: "Someone Not In Contacts";
|
||||
? "Someone Not In Network"
|
||||
: "Someone Not In Contacts";
|
||||
}
|
||||
|
||||
export interface ResultWithType {
|
||||
@@ -231,10 +231,10 @@ export async function createAndSubmitGive(
|
||||
error === null
|
||||
? "Null error"
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: typeof error === "object" && error !== null && "message" in error
|
||||
? (error as { message: string }).message
|
||||
: "Unknown error";
|
||||
? error.message
|
||||
: typeof error === "object" && error !== null && "message" in error
|
||||
? (error as { message: string }).message
|
||||
: "Unknown error";
|
||||
|
||||
return {
|
||||
type: "error",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { register } from "register-service-worker";
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
register("/additional-scripts.js", {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready() {
|
||||
console.log(
|
||||
"App is being served from cache by a service worker.\n" +
|
||||
|
||||
@@ -148,7 +148,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/project",
|
||||
path: "/project/:id?",
|
||||
name: "project",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
|
||||
@@ -168,6 +168,14 @@ const routes: Array<RouteRecordRaw> = [
|
||||
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/search-area",
|
||||
name: "search-area",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "search-area" */ "../views/SearchAreaView.vue"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/seed-backup",
|
||||
name: "seed-backup",
|
||||
|
||||
@@ -655,11 +655,11 @@ export default class AccountViewView extends Vue {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
type: "success",
|
||||
title: "Download Started",
|
||||
text: "See your downloads directory for the backup.",
|
||||
},
|
||||
5000,
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
<QuickNav selected="Contacts"></QuickNav>
|
||||
<section id="Content" class="p-6 pb-24">
|
||||
<!-- Breadcrumb -->
|
||||
<div id="ViewBreadcrumb" class="mb-8">
|
||||
<h1 class="text-lg text-center font-light relative px-7">
|
||||
<div class="mb-8">
|
||||
<h1
|
||||
id="ViewBreadcrumb"
|
||||
class="text-lg text-center font-light relative px-7"
|
||||
>
|
||||
<!-- Back -->
|
||||
<router-link
|
||||
:to="{ name: 'contacts' }"
|
||||
@@ -11,11 +14,12 @@
|
||||
><fa icon="chevron-left" class="fa-fw"></fa
|
||||
></router-link>
|
||||
</h1>
|
||||
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
||||
Given with {{ contact?.name }}
|
||||
</h1>
|
||||
</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">
|
||||
<span />
|
||||
<span class="justify-around">(Only 50 most recent)</span>
|
||||
@@ -358,7 +362,10 @@ export default class ContactsView extends Vue {
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
position: relative;
|
||||
|
||||
@@ -67,8 +67,8 @@
|
||||
showGiveTotals
|
||||
? "Total"
|
||||
: showGiveConfirmed
|
||||
? "Confirmed"
|
||||
: "Unconfirmed"
|
||||
? "Confirmed"
|
||||
: "Unconfirmed"
|
||||
}}
|
||||
</button>
|
||||
<br />
|
||||
@@ -218,7 +218,7 @@
|
||||
</div>
|
||||
</li>
|
||||
</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 class="dialog">
|
||||
@@ -1050,7 +1050,10 @@ export default class ContactsView extends Vue {
|
||||
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 {
|
||||
position: relative;
|
||||
|
||||
@@ -67,46 +67,14 @@
|
||||
</div>
|
||||
|
||||
<div v-if="isLocalActive">
|
||||
<div v-if="!isChoosingSearchBox">
|
||||
<div>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<!-- Loading Animation -->
|
||||
@@ -150,50 +118,11 @@
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
</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 { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
@@ -204,10 +133,6 @@ import QuickNav from "@/components/QuickNav.vue";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
|
||||
const DEFAULT_LAT_LONG_DIFF = 0.01;
|
||||
const WORLD_ZOOM = 2;
|
||||
const DEFAULT_ZOOM = 2;
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
@@ -217,13 +142,9 @@ interface Notification {
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
LRectangle,
|
||||
QuickNav,
|
||||
InfiniteScroll,
|
||||
EntityIcon,
|
||||
LMap,
|
||||
LMarker,
|
||||
LTileLayer,
|
||||
},
|
||||
})
|
||||
export default class DiscoverView extends Vue {
|
||||
@@ -238,13 +159,7 @@ export default class DiscoverView extends Vue {
|
||||
isChoosingSearchBox = false;
|
||||
isLocalActive = true;
|
||||
isRemoteActive = false;
|
||||
isNewMarkerSet = false;
|
||||
localCenterLat = 0;
|
||||
localCenterLong = 0;
|
||||
localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
localCount = 0;
|
||||
localZoom = DEFAULT_ZOOM;
|
||||
remoteCount = 0;
|
||||
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
||||
isLoading = false;
|
||||
@@ -258,7 +173,6 @@ export default class DiscoverView extends Vue {
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.searchBox = settings?.searchBoxes?.[0] || null;
|
||||
this.resetLatLong();
|
||||
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
|
||||
@@ -462,128 +376,6 @@ export default class DiscoverView extends Vue {
|
||||
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() {
|
||||
return {
|
||||
"inline-block": true,
|
||||
|
||||
@@ -8,38 +8,22 @@
|
||||
|
||||
<!-- show the actions for recognizing a give -->
|
||||
<div class="mb-8">
|
||||
<div
|
||||
v-if="!activeDid"
|
||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||
>
|
||||
<p class="text-lg mb-3">
|
||||
You need an <b>identifier</b> before you can record others' giving.
|
||||
</p>
|
||||
<router-link
|
||||
:to="{ name: 'start' }"
|
||||
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||
>
|
||||
Create Your Identifier</router-link
|
||||
<div v-if="!activeDid">
|
||||
To record others' giving,
|
||||
<router-link :to="{ name: 'start' }" class="text-blue-500">
|
||||
create your identifier.</router-link
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="!isRegistered"
|
||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||
>
|
||||
Someone must register your account before you can record others' giving.
|
||||
To do this:
|
||||
<router-link
|
||||
:to="{ name: 'contact-qr' }"
|
||||
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||
<div v-else-if="!isRegistered">
|
||||
To record others' giving, someone must register your account, so show
|
||||
them
|
||||
<router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
|
||||
your identity info</router-link
|
||||
>
|
||||
1. Show Them Your Identity Info</router-link
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'account' }"
|
||||
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||
>
|
||||
2. Check Your Limits</router-link
|
||||
and then
|
||||
<router-link :to="{ name: 'account' }" class="text-blue-500">
|
||||
check your limits.</router-link
|
||||
>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@
|
||||
<label for="includeLocation">Include Location</label>
|
||||
</div>
|
||||
<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
|
||||
ref="map"
|
||||
v-model:zoom="zoom"
|
||||
|
||||
@@ -137,10 +137,12 @@
|
||||
<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">
|
||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||
Given to this Project
|
||||
Given To This Project
|
||||
</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
|
||||
v-for="give in givesToThis"
|
||||
:key="give.id"
|
||||
@@ -164,31 +166,33 @@
|
||||
</ul>
|
||||
</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">
|
||||
…and from this Project
|
||||
Contributions By This Project
|
||||
</h3>
|
||||
<button
|
||||
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
||||
class="text-blue-500"
|
||||
>
|
||||
{{ fulfilledByThis.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="text-sm border-t border-slate-300">
|
||||
<li
|
||||
v-for="give in givesByThis"
|
||||
:key="give.id"
|
||||
class="py-1.5 border-b border-slate-300"
|
||||
>
|
||||
<div class="flex justify-between gap-4">
|
||||
<span
|
||||
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
||||
</span>
|
||||
<span v-if="give.amount"
|
||||
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||
{{ give.amount }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="give.description" class="text-slate-500">
|
||||
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
||||
{{ give.description }}
|
||||
</div>
|
||||
<div
|
||||
v-if="fulfillersToThis.length > 0"
|
||||
class="bg-slate-100 px-4 py-3 rounded-md"
|
||||
>
|
||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||
Contributions To This Project
|
||||
</h3>
|
||||
<ul>
|
||||
<li v-for="plan in fulfillersToThis" :key="plan.handleId">
|
||||
<button
|
||||
@click="onClickLoadProject(plan.handleId)"
|
||||
class="text-blue-500"
|
||||
>
|
||||
{{ plan.name }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -218,6 +222,7 @@ import {
|
||||
didInfo,
|
||||
GiverInputInfo,
|
||||
GiveServerRecord,
|
||||
PlanServerRecord,
|
||||
} from "@/libs/endorserServer";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
@@ -242,8 +247,9 @@ export default class ProjectViewView extends Vue {
|
||||
apiServer = "";
|
||||
description = "";
|
||||
expanded = false;
|
||||
fulfilledByThis: PlanServerRecord | null = null;
|
||||
fulfillersToThis: Array<PlanServerRecord> = [];
|
||||
givesToThis: Array<GiveServerRecord> = [];
|
||||
givesByThis: Array<GiveServerRecord> = [];
|
||||
latitude = 0;
|
||||
longitude = 0;
|
||||
name = "";
|
||||
@@ -266,7 +272,12 @@ export default class ProjectViewView extends Vue {
|
||||
this.allMyDids = accountsArr.map((acc) => acc.did);
|
||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
||||
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) {
|
||||
@@ -320,11 +331,11 @@ export default class ProjectViewView extends Vue {
|
||||
this.expanded = false;
|
||||
}
|
||||
|
||||
async LoadProject(identity: IIdentifier) {
|
||||
async LoadProject(projectId: string, identity: IIdentifier) {
|
||||
this.projectId = projectId;
|
||||
|
||||
const url =
|
||||
this.apiServer +
|
||||
"/api/claim/byHandle/" +
|
||||
encodeURIComponent(this.projectId);
|
||||
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId);
|
||||
const headers: RawAxiosRequestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
@@ -389,7 +400,7 @@ export default class ProjectViewView extends Vue {
|
||||
const givesInUrl =
|
||||
this.apiServer +
|
||||
"/api/v2/report/givesForPlans?planIds=" +
|
||||
encodeURIComponent(JSON.stringify([this.projectId]));
|
||||
encodeURIComponent(JSON.stringify([projectId]));
|
||||
try {
|
||||
const resp = await this.axios.get(givesInUrl, { headers });
|
||||
if (resp.status === 200 && resp.data.data) {
|
||||
@@ -422,21 +433,21 @@ export default class ProjectViewView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
const givesOutUrl =
|
||||
const fulfilledByUrl =
|
||||
this.apiServer +
|
||||
"/api/v2/report/givesProvidedBy?providerId=" +
|
||||
encodeURIComponent(this.projectId);
|
||||
"/api/v2/report/planFulfilledByPlan?planHandleId=" +
|
||||
encodeURIComponent(projectId);
|
||||
try {
|
||||
const resp = await this.axios.get(givesOutUrl, { headers });
|
||||
if (resp.status === 200 && resp.data.data) {
|
||||
this.givesByThis = resp.data.data;
|
||||
const resp = await this.axios.get(fulfilledByUrl, { headers });
|
||||
if (resp.status === 200) {
|
||||
this.fulfilledByThis = resp.data.data;
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to retrieve gives by this project.",
|
||||
text: "Failed to retrieve plans fulfilled by this project.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -448,15 +459,64 @@ export default class ProjectViewView extends Vue {
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Something went wrong retrieving gives by project.",
|
||||
text: "Something went wrong retrieving plans fulfilled by this project.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
console.error(
|
||||
"Error retrieving gives by this project:",
|
||||
"Error retrieving plans fulfilled by this project:",
|
||||
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() {
|
||||
|
||||
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',
|
||||
type: 'toast',
|
||||
text: 'I\'m a toast. Don\'t mind me.',
|
||||
text: 'I\'m a toast. Without a timeout, I\'m stuck.',
|
||||
},
|
||||
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
|
||||
|
||||
@@ -1,63 +1,33 @@
|
||||
/* eslint-env serviceworker */
|
||||
/* global workbox */
|
||||
importScripts(
|
||||
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
|
||||
);
|
||||
const notifications = require("./safari-notifications.js");
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
importScripts(
|
||||
"safari-notifications.js",
|
||||
"nacl.js",
|
||||
"noble-curves.js",
|
||||
"noble-hashes.js",
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("push", function (event) {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
try {
|
||||
let payload;
|
||||
if (event.data) {
|
||||
payload = JSON.parse(event.data.text());
|
||||
}
|
||||
const message = await self.getNotificationCount();
|
||||
const title = payload ? payload.title : "Custom Title";
|
||||
const options = {
|
||||
body: message,
|
||||
icon: payload ? payload.icon : "icon.png",
|
||||
badge: payload ? payload.badge : "badge.png",
|
||||
};
|
||||
await self.registration.showNotification(title, options);
|
||||
} catch (error) {
|
||||
console.error("Error in processing the push event:", error);
|
||||
}
|
||||
})(),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data && event.data.type === "SEND_LOCAL_DATA") {
|
||||
self.secret = event.data.data;
|
||||
event.ports[0].postMessage({ success: true });
|
||||
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("activate", (event) => {
|
||||
event.waitUntil(clients.claim());
|
||||
console.log("Service worker activated", event);
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
console.log(event.request);
|
||||
});
|
||||
self.addEventListener("message", function (event) {
|
||||
const data = event.data;
|
||||
|
||||
self.addEventListener("error", (event) => {
|
||||
console.error("Error in Service Worker:", event.message);
|
||||
console.error("File:", event.filename);
|
||||
console.error("Line:", event.lineno);
|
||||
console.error("Column:", event.colno);
|
||||
console.error("Error Object:", event.error);
|
||||
});
|
||||
const result = notifications.getNotificationCount()
|
||||
|
||||
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
|
||||
switch (data.command) {
|
||||
case "account":
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log("Unknown command:", data.command);
|
||||
}
|
||||
});
|
||||
|
||||
1051
sw_scripts/nacl.js
1051
sw_scripts/nacl.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -11,9 +11,8 @@ module.exports = defineConfig({
|
||||
iconPaths: {
|
||||
faviconSVG: "img/icons/safari-pinned-tab.svg",
|
||||
},
|
||||
workboxPluginMode: "InjectManifest",
|
||||
workboxOptions: {
|
||||
swSrc: "./sw_scripts/additional-scripts.js",
|
||||
importScripts: ["additional-scripts.js"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -29,8 +29,8 @@ from the SERVICE.
|
||||
The SERVICE will provide context and obtain explicit permission before prompting
|
||||
for notification permission:
|
||||
|
||||
In order to provide this context and explict permission a two-step opt-in process
|
||||
where the user is first presented with a pre-permission dialog box that explains
|
||||
In order to provide this context and explicit permission, a two-step opt-in process
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
The Vue project already has its own service worker but it is possible to
|
||||
@@ -389,4 +389,4 @@ Simply tap on the Mute Notifications toggle switch in `AccountViewView` to immed
|
||||
While notifications are turned on, the user can tap on the App Notifications toggle switch in `AccountViewView` to trigger the Turn Off Notifications Dialog. User is given the following choices:
|
||||
|
||||
- "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on).
|
||||
- "Leave it On" to make no changes and dismiss the dialog.
|
||||
- "Leave it On" to make no changes and dismiss the dialog.
|
||||
|
||||
Reference in New Issue
Block a user