forked from trent_larson/crowd-funder-for-time-pwa
Compare commits
69 Commits
feat/vitej
...
profile-pi
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d6e5266b4 | |||
| 581a374b05 | |||
| 1009574721 | |||
| 50cae65214 | |||
| 48a46cf6f1 | |||
| 60b2bf35fb | |||
| cb5a7135ac | |||
| a7a9e35766 | |||
| f029835e15 | |||
| 017a172df3 | |||
| 7837122a95 | |||
| 0093255246 | |||
| 30bd53fb6f | |||
| ca22930012 | |||
| c7c5bda014 | |||
| 19aa572c95 | |||
| 03fae5dd95 | |||
| 80818a8861 | |||
| d29a8d9637 | |||
| f0b0231515 | |||
| b73d2a3b58 | |||
| 22cba5babf | |||
| 708ac51f23 | |||
| a91ffc88b9 | |||
| d727c2841b | |||
| 226a97732d | |||
| c94dd7743b | |||
| 64e38cb8ff | |||
| e61ac31710 | |||
| 3fbf68b117 | |||
| d4390483d9 | |||
| 8dea2091af | |||
| e3696e3ac5 | |||
|
|
027825b155 | ||
| 911203c190 | |||
| 2da0394003 | |||
| 4a65d095db | |||
| 8ea5779312 | |||
| 144ab76716 | |||
|
|
8da2c8cc30 | ||
|
|
570b31e2d6 | ||
|
|
07f542ca16 | ||
|
|
62e0fc51c2 | ||
|
|
94b600e527 | ||
|
|
5388e6052c | ||
|
|
21fe5a0279 | ||
|
|
ffba89a7b5 | ||
|
|
31954d2690 | ||
| 340d0a5219 | |||
| 2d2785d6a0 | |||
| 41d6e5fc73 | |||
| 7412d67c33 | |||
| 83db5302ad | |||
| 75f9f20ea3 | |||
| e43c45ebea | |||
| 708032311a | |||
| 5dead960ae | |||
| 12d81b79c7 | |||
| f3dc81e6eb | |||
| ef5f81932d | |||
| 214a264179 | |||
| 9b183a4b6c | |||
| f365cc9e3c | |||
| 9059f7a9a7 | |||
| e6cd86618e | |||
| c3fd27b140 | |||
| cf2e800dec | |||
| b60383cfe9 | |||
| c7d93db6f2 |
@@ -1,7 +1,3 @@
|
||||
|
||||
# I tried setting values here and using `vue-cli-service build --mode development`
|
||||
# but it didn't create some things in "dist":
|
||||
# - the "css" directory with the CSS extracted from Vue files
|
||||
# - the sw_scripts-combined* files
|
||||
#
|
||||
# ¯\_(ツ)_/¯
|
||||
# I tried and failed to set things here with vue-cli-service but
|
||||
# things may be more reliable with vite so let's try again.
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -10,6 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Nothing
|
||||
|
||||
|
||||
## [0.3.7] - 2024.04-10 - cf18f1543a700d62a5f9e764905a4aafe1fb229b
|
||||
### Added
|
||||
- Filter on home page feed
|
||||
- Ability to set time of daily notification
|
||||
- Jump to app on click of notification
|
||||
### Changed
|
||||
- Built with vite
|
||||
- Descriptions on home page to include projects
|
||||
### Changed in DB or environment
|
||||
- Nothing
|
||||
|
||||
|
||||
## [0.3.6] - 2024.03.24 - 3a07e31d6313ab95711265562d9023c42916e141
|
||||
### Added
|
||||
- Button to mirror photo during video
|
||||
|
||||
@@ -45,9 +45,9 @@ npm run lint
|
||||
|
||||
* Test
|
||||
```
|
||||
# (See .env.development for more details.)
|
||||
# (Let's replace this with a .env.development or .env.staging file.)
|
||||
# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
|
||||
VITE_TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app npm run build
|
||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app npm run build
|
||||
```
|
||||
|
||||
* Production
|
||||
@@ -99,7 +99,7 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
|
||||
- Use a mobile user as well as a desktop user.
|
||||
- Backup seed & data & get a CSV dump from Endorser Mobile.
|
||||
- Check that the version is updated.
|
||||
- Clear the browser data & add identity & import Time Safari contacts and then CSV contacts.
|
||||
- Clear the browser data & add identity & import Time Safari contacts and then CSV contacts.
|
||||
- Make sure that it's using the test API (under Identity in 'Advanced').
|
||||
- Clear the browser data again. (See "Reset" below.)
|
||||
- Go to the account page before visiting the home page to see that there is no ID.
|
||||
|
||||
2258
package-lock.json
generated
2258
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TimeSafari",
|
||||
"version": "0.3.7-beta",
|
||||
"version": "0.3.8-beta",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -8,7 +8,6 @@
|
||||
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build",
|
||||
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
||||
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
||||
"postbuild": "cp ./sw_scripts-combined.js dist/",
|
||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -63,6 +62,7 @@
|
||||
"vue": "^3.4.21",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-facing-decorator": "^3.0.4",
|
||||
"vue-picture-cropper": "^0.7.0",
|
||||
"vue-qrcode-reader": "^5.5.3",
|
||||
"vue-router": "^4.3.0",
|
||||
"web-did-resolver": "^2.0.27"
|
||||
@@ -87,6 +87,7 @@
|
||||
"prettier": "^3.2.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^5.2.0"
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-pwa": "^0.19.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,181 +1,4 @@
|
||||
|
||||
tasks :
|
||||
|
||||
- fix the notification link to the app
|
||||
- 01 change scanning flow - allow them to stay on the QR/scanning screen after scanning someone
|
||||
|
||||
- 24 contextual tutorials https://docs.google.com/document/d/11C_K3RM0rgo0onih20KFhcIzukZyq_CRWqaWX5om_kM/edit#heading=h.iwiwcydou5hw
|
||||
|
||||
- feeds - add "remote" filter, if they choose 'visible' then warn that they won't see any others, cache list & don't reload front page on change
|
||||
|
||||
- .1 add shortcut from project (etc?) to the public project page in a browser
|
||||
- .1 add KindSpring link to ideas
|
||||
- .1 on feed, don't show "to someone anonymous" if it's to a project
|
||||
- 16 save data backups in Google
|
||||
- 16 generate and use passkeys for identities
|
||||
- .5 show "give" buttons (eg. from anonymous) even if they can't give, greyed out, and give them a warning and instructions
|
||||
- .2 when adding a claim on home screen, push that claim to the top of the list
|
||||
|
||||
- .2 fix give dialog from "more contacts" off home page to allow giving to this user
|
||||
- .5 stop from seeing an error on the first page when browser doesn't support service workers (which I've seen on iPhone; visible in Firefox private window)
|
||||
- .2 don't show a warning on a totally new project when the authorized agent is set
|
||||
- .2 anchor hash into BTC
|
||||
- .2 list the "show more" contacts alphabetically
|
||||
- .5 add back the explicit wait for browser subscription timing problems?
|
||||
|
||||
- .5 make Time Safari a share_target for images
|
||||
|
||||
- 08 add image on profile
|
||||
|
||||
- 01 ask to detect location & record it in settings
|
||||
- 01 if personal location is set, show potential local affiliations
|
||||
- 02 refactor the buttons for chosing a search location so that the actions are clear assignee-group:ui
|
||||
|
||||
- 24 compelling UI for credential presentations
|
||||
- discover who in my network has activity on a project
|
||||
|
||||
- 24 compelling UI for statistics (eg. World?)
|
||||
|
||||
- 01 in the feed, group by project or contact or topic or time/$ (via BC); new projects, offers, search area, etc assignee-group:ui
|
||||
- 01 separate not-on-platform vs totally anonymous; terminology "unidentified"?
|
||||
- .2 add links between projects assignee-group:ui
|
||||
- 24 make the contact browsing on the front page something that invites more action
|
||||
|
||||
- .5 change server plan & project endpoints to use jwtId as identifier rather than rowid
|
||||
- 16 edit offers & gives, or revoke allowing re-creation
|
||||
- .1 When available in the server, give message for 'nonAmountGiven' on offers on ProjectsView page.
|
||||
- .1 Add help instructions for "Encryption key has changed" error. (It is a problem if localStorage is cleared, but the contacts & settings remain and they have to restore their seeds.)
|
||||
- .1 show better error when user with no ID goes to the "My Project" page
|
||||
- 01 in front page prompt for ideas for gratitude :
|
||||
- randomize (not show in order)
|
||||
- checkboxes - show non-person-oriented messages, show only contacts, show only projects
|
||||
|
||||
- .5 add a notice on the front page if their notifications are off
|
||||
- 08 allow user to add a time when they want their daily notification
|
||||
|
||||
- .5 prompt for the name directly when they visit the QR scan page
|
||||
- 01 mark a project as inactive
|
||||
- 01 add share button for sending a message to confirmers when we can't see the claim (like the "visible" links)
|
||||
- .5 add TimeSafari as a shareable app https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target
|
||||
- .5 choose a project's alternative agent ("authorized representative") via a contact chooser (not just copy-paste a DID)
|
||||
- .5 find out why clicking quickly back-and-forth onto the "my project" page often shows error "You need an identifier to load your projects." (easier to reproduce on desktop?)
|
||||
- .5 bug - it didn't show the "fulfills offer" on the claim detail page for a give that had one - https://test.timesafari.app/claim/01HMFWRPA3PD6Q9EYFKX3MC41J
|
||||
- 01 replace all "confirm" prompts with nicer modal
|
||||
- .1 hide project-create button on project page if not registered
|
||||
- .1 hide offer & give buttons on project list page if not registered
|
||||
- .1 add cursor-pointer on the icons for giving on the project page, and on the list of projects on the discover page
|
||||
- .2 record when InfiniteScroll hits the end of the list and don't trigger any more loads (feed, project list, give & offer lists)
|
||||
|
||||
- bug (that is hard to reproduce) - got blank screen and errors on iPhone with no bottom tabs
|
||||
- bug - turning notifications on from the help screen did not stay, though account screen toggle did stay (From Jason on iPhone.)
|
||||
- refactor - supply the projectId to the OfferDialog just like we do with the GiftedDialog offerId (in the "open" method, maybe as well as an attribute)
|
||||
- the confirm button on each give on the ProjectViewView page doesn't have all the context of the ClaimView page, so it can show sometimes inappropriately; consider consolidation
|
||||
- make the "give" on contact screen work like other give (allowing donation vs current blank)
|
||||
- .2 on ClaimView, the "ask someone" should refer to "visible" IDs, or to confirmations only if confirmations are visible
|
||||
- message "send them to this page" on ClaimView should be a link (for installed app)
|
||||
- When we update a version, desktop browser users have seen nothing happen after clicking on the contact page QR and on the account page "Help"; errors show in the console. Reload fixed it. If this happens on mobile, ask the user to reload.
|
||||
|
||||
- 01 show my VCs - most interesting, or via search
|
||||
- 04 allow user to download & prove chains of VCs, mine + ones I can see about me from others
|
||||
|
||||
- revenue to support server operation
|
||||
|
||||
- .1 copy button for seed
|
||||
- .5 If notifications are not enabled, add message to front page with link/button to enable
|
||||
- make server endpoint for full English description of limits
|
||||
- create a help-desk document & add screenshots
|
||||
|
||||
- .1 update "offer" units to have same functionality as "give" units
|
||||
- .5 add a link to any 'give' records that fulfill an offer on ClaimView
|
||||
- 01 on home page, prompt for install check in addition to "supports notifications" check (since they won't get notified if Chrome is closed)
|
||||
- 01 on Mac (& Windows?) desktop, add a help blurb so that they can find it again (since it doesn't show in Application list)
|
||||
- bug (that is hard to reproduce) - got error adding on Firefox user #0 as contact for themselves
|
||||
- bug (that is hard to reproduce) - in Chrome, install app then delete app and try from Chrome browser and see log errors "Uncaught TypeError: self.appendDailyLog is not a function"
|
||||
- bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent
|
||||
- 04 remove 'rowid' references (that are sqlite-specific); may involve server
|
||||
- 04 look at other examples for better onboarding UI, eg friend.tech
|
||||
- .5 Add inactive flag / end date, start date to project
|
||||
- .3 check that Android shows "back" buttons on screens without bottom tray
|
||||
- .1 Make give description text box into something that expands as they type?
|
||||
- .2 Show a warning if both giver and recipient are the same (but still allow?)
|
||||
- .5 Shrink the buttons on project pages so they don't expand to the width of the screen assignee-group:ui
|
||||
- .5 Display a more appealing confirmation on the map when erasing the marker
|
||||
- .5 remove references to localStorage for projectId (now that it's pulling from the path)
|
||||
- switch some checks for activeDid to check for isRegistered
|
||||
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
||||
- .5 fix cert generation on server (since it didn't happen automatically for Nov 30)
|
||||
- warn if they're using the web (android only?)
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps
|
||||
https://web.dev/articles/get-installed-related-apps
|
||||
- .5 fix the "onboarding help" list of instructions so that it always formats right (currently doesn't show numbers aligned on Google Pixel 6a, iPhone 11 Pro, iPhone 12 mini)
|
||||
- .5 make the "onboarding help" it so that it doesn't cover the QR icon on the contacts page
|
||||
- .5 fix masked icon (because some of the top-right of the binoculars is cut off)
|
||||
|
||||
- contacts v+ :
|
||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||
- .2 show error to user when adding a duplicate contact
|
||||
- 01 parse input more robustly (with CSV lib and not commas)
|
||||
|
||||
- stats v1 :
|
||||
- 01 show numeric stats
|
||||
- 04 show different graphic for projects vs people (gnome?) on world
|
||||
- 01 link to world for specific stats
|
||||
- .5 don't load another instance of a bush if it already exists
|
||||
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
|
||||
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
|
||||
|
||||
- .5 show seed phrase in a QR code for transfer to another device
|
||||
- .5 on DiscoverView, switch to a filter UI (eg. just from friend)
|
||||
- .5 don't show "Offer" on project screen if they aren't registered
|
||||
- 01 especially for iOS, check for new version & update, eg. https://stackoverflow.com/questions/52221805/any-way-yet-to-auto-update-or-just-clear-the-cache-on-a-pwa-on-ios
|
||||
|
||||
- 24 allow a person record with interests, including location; purpose? contact methods? enhance other connections the same? (suggestion from Philippines) assignee-group:ui
|
||||
- 24 brief introduction slides https://docs.google.com/document/d/11C_K3RM0rgo0onih20KFhcIzukZyq_CRWqaWX5om_kM/edit#heading=h.iwiwcydou5hw
|
||||
- 12 feedback https://docs.google.com/document/d/11C_K3RM0rgo0onih20KFhcIzukZyq_CRWqaWX5om_kM/edit#heading=h.iwiwcydou5hw
|
||||
|
||||
- 32 accept images for projects
|
||||
- 32 accept images for contacts
|
||||
- import project interactions from GitHub/GitLab and manage signing
|
||||
|
||||
- show total time offered to & fulfilled to a project
|
||||
- show total time offered by & fulfilled by a contact
|
||||
|
||||
- linking between projects or plans :
|
||||
- show total time given to & from a project
|
||||
- terminology:
|
||||
- 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 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
|
||||
- 04 show gives spreading to other places
|
||||
- badge for most gives/receives/confirms per day/week/month
|
||||
- badge for amount given/offered to your project
|
||||
- set a goal of given/offers
|
||||
|
||||
- automated tests, eg. pup-test or cypress
|
||||
|
||||
- Notifications (wake on the phone, push notifications)
|
||||
- 02 change push server so that the web-push/subscribe call sets up a thread for the 10-seconds-later push notification, but returns immediately to the callee
|
||||
- pull instead of push, maybe via scheduled runs
|
||||
- have a notification pop-up on Mac screen
|
||||
|
||||
- 16 Connect with phone contacts - this may be a whole different app, because we want a quick link A) to the same phone contact and B) from the phone contact app
|
||||
|
||||
- Support KERI AIDs
|
||||
- Support Peer DIDs
|
||||
- Support messaging through DIDComm
|
||||
- Write to or read from a different ledger (eg. private ACDC, EAS & attest.sh)
|
||||
|
||||
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
|
||||
|
||||
- allow some gives even if they aren't registered - maybe someday as a gift to the world, but we really want this to be built via personal connections -- and that allows spam
|
||||
- .1 When Chrome shows compatibility https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare
|
||||
then change the canShare check in this app to check the real canShare() method.
|
||||
|
||||
log :
|
||||
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
|
||||
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27
|
||||
- This list has moved - see https://sharing.clickup.com/9014278710/l/h/6-901402735154-1/685f1be3f9b150d
|
||||
|
||||
@@ -5,20 +5,29 @@
|
||||
import { createAvatar, StyleOptions } from "@dicebear/core";
|
||||
import { avataaars } from "@dicebear/collection";
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
|
||||
@Component
|
||||
export default class EntityIcon extends Vue {
|
||||
@Prop entityId = "";
|
||||
@Prop contact: Contact;
|
||||
@Prop entityId = ""; // overridden by contact.did or profileImageUrl
|
||||
@Prop iconSize = 0;
|
||||
@Prop profileImageUrl = ""; // overridden by contact.profileImageUrl
|
||||
|
||||
generateIcon() {
|
||||
const options: StyleOptions<object> = {
|
||||
seed: this.entityId || "",
|
||||
size: this.iconSize,
|
||||
};
|
||||
const avatar = createAvatar(avataaars, options);
|
||||
const svgString = avatar.toString();
|
||||
return svgString;
|
||||
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
|
||||
if (imageUrl) {
|
||||
return `<img src="${imageUrl}" alt="avatar" width="${this.iconSize}" height="${this.iconSize}" />`;
|
||||
} else {
|
||||
const identifier = this.contact?.did || this.entityId;
|
||||
const options: StyleOptions<object> = {
|
||||
seed: (identifier as string) || "",
|
||||
size: this.iconSize,
|
||||
};
|
||||
const avatar = createAvatar(avataaars, options);
|
||||
const svgString = avatar.toString();
|
||||
return svgString;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -20,28 +20,54 @@
|
||||
</div>
|
||||
|
||||
<div v-if="uploading" class="flex justify-center">
|
||||
<fa icon="spinner" class="fa-spin fa-3x text-center block" />
|
||||
<fa
|
||||
icon="spinner"
|
||||
class="fa-spin fa-3x text-center block px-12 py-12"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="blob">
|
||||
<div
|
||||
class="flex justify-center gap-2 absolute bottom-[1rem] left-[1rem] right-[1rem] bg-black/50 px-4 py-2"
|
||||
>
|
||||
<div v-if="crop">
|
||||
<VuePictureCropper
|
||||
:boxStyle="{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: '#f8f8f8',
|
||||
margin: 'auto',
|
||||
}"
|
||||
:img="URL.createObjectURL(blob)"
|
||||
:options="{
|
||||
viewMode: 1,
|
||||
dragMode: 'crop',
|
||||
aspectRatio: 9 / 9,
|
||||
}"
|
||||
/>
|
||||
<!-- This gives a round cropper.
|
||||
:presetMode="{
|
||||
mode: 'round',
|
||||
}"
|
||||
-->
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="flex justify-center">
|
||||
<img :src="URL.createObjectURL(blob)" class="mt-2 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-[1rem] left-[1rem] px-2 py-1">
|
||||
<button
|
||||
@click="uploadImage"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white font-bold py-2 px-4 rounded-md"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
|
||||
>
|
||||
<span>Upload</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="absolute bottom-[1rem] right-[1rem] px-2 py-1">
|
||||
<button
|
||||
@click="retryImage"
|
||||
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white font-bold py-2 px-4 rounded-md"
|
||||
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
|
||||
>
|
||||
<span>Retry</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<img :src="URL.createObjectURL(blob)" class="mt-2 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else ref="cameraContainer">
|
||||
<!--
|
||||
@@ -92,6 +118,7 @@
|
||||
import axios from "axios";
|
||||
import Camera from "simple-vue-camera";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
|
||||
import { getIdentity } from "@/libs/util";
|
||||
@@ -99,13 +126,15 @@ import { db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
|
||||
@Component({ components: { Camera } })
|
||||
@Component({ components: { Camera, VuePictureCropper } })
|
||||
export default class GiftedPhotoDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
activeDeviceNumber = 0;
|
||||
activeDid = "";
|
||||
blob: Blob | null = null;
|
||||
claimType = "GiveAction";
|
||||
crop = false;
|
||||
mirror = false;
|
||||
numDevices = 0;
|
||||
setImage: (arg: string) => void = () => {};
|
||||
@@ -134,8 +163,10 @@ export default class GiftedPhotoDialog extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
open(setImageFn: (arg: string) => void) {
|
||||
open(setImageFn: (arg: string) => void, crop?: boolean, claimType?: string) {
|
||||
this.visible = true;
|
||||
this.crop = !!crop;
|
||||
this.claimType = claimType || "GiveAction";
|
||||
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
|
||||
if (bottomNav) {
|
||||
bottomNav.style.display = "none";
|
||||
@@ -274,6 +305,11 @@ export default class GiftedPhotoDialog extends Vue {
|
||||
|
||||
async uploadImage() {
|
||||
this.uploading = true;
|
||||
|
||||
if (this.crop) {
|
||||
this.blob = await cropper?.getBlob();
|
||||
}
|
||||
|
||||
const identifier = await getIdentity(this.activeDid);
|
||||
const token = await accessToken(identifier);
|
||||
const headers = {
|
||||
@@ -295,7 +331,7 @@ export default class GiftedPhotoDialog extends Vue {
|
||||
return;
|
||||
}
|
||||
formData.append("image", this.blob, "snapshot.png"); // png is set in snapshot()
|
||||
formData.append("claimType", "GiveAction");
|
||||
formData.append("claimType", this.claimType);
|
||||
try {
|
||||
const response = await axios.post(
|
||||
DEFAULT_IMAGE_API_SERVER + "/image",
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface Contact {
|
||||
did: string;
|
||||
name?: string;
|
||||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
||||
profileImageUrl?: string;
|
||||
publicKeyBase64?: string;
|
||||
seesMe?: boolean;
|
||||
registered?: boolean;
|
||||
|
||||
@@ -20,11 +20,12 @@ export type Settings = {
|
||||
filterFeedByNearby?: boolean; // filter by nearby
|
||||
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden
|
||||
|
||||
firstName?: string; // User's first name
|
||||
firstName?: string; // user's full name
|
||||
isRegistered?: boolean;
|
||||
lastName?: string; // deprecated - put all names in firstName
|
||||
lastNotifiedClaimId?: string; // Last notified claim ID
|
||||
lastViewedClaimId?: string; // Last viewed claim ID
|
||||
lastNotifiedClaimId?: string;
|
||||
lastViewedClaimId?: string;
|
||||
profileImageUrl?: string;
|
||||
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
|
||||
reminderOn?: boolean; // Toggle to enable or disable reminders
|
||||
|
||||
|
||||
@@ -63,12 +63,14 @@
|
||||
|
||||
<!-- Identity Details -->
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||
<h2 v-if="givenName" class="text-xl font-semibold mb-2">
|
||||
{{ givenName }}
|
||||
<router-link :to="{ name: 'new-edit-account' }">
|
||||
<fa icon="pen" class="text-xs text-blue-500 mb-1"></fa>
|
||||
</router-link>
|
||||
</h2>
|
||||
<div v-if="givenName">
|
||||
<h2 class="text-xl font-semibold mb-2">
|
||||
{{ givenName }}
|
||||
<router-link :to="{ name: 'new-edit-account' }">
|
||||
<fa icon="pen" class="text-xs text-blue-500 mb-1"></fa>
|
||||
</router-link>
|
||||
</h2>
|
||||
</div>
|
||||
<span v-else>
|
||||
<router-link
|
||||
:to="{ name: 'new-edit-account' }"
|
||||
@@ -77,6 +79,61 @@
|
||||
Set Your Name
|
||||
</router-link>
|
||||
</span>
|
||||
<div class="flex justify-center mt-4">
|
||||
<span v-if="profileImageUrl" class="flex justify-between">
|
||||
<EntityIcon
|
||||
:icon-size="96"
|
||||
:profileImageUrl="profileImageUrl"
|
||||
class="inline-block align-text-bottom border border-slate-300 rounded"
|
||||
@click="showLargeIdenticonUrl = profileImageUrl"
|
||||
/>
|
||||
<fa
|
||||
icon="trash-can"
|
||||
@click="confirmDeleteImage"
|
||||
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
|
||||
/>
|
||||
</span>
|
||||
<span v-else>
|
||||
<fa
|
||||
icon="camera"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
|
||||
@click="openPhotoDialog"
|
||||
/>
|
||||
</span>
|
||||
<GiftedPhotoDialog ref="photoDialog" />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="flex justify-center">
|
||||
... and those without your image see this:
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<EntityIcon
|
||||
:entityId="activeDid"
|
||||
:iconSize="64"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
@click="showLargeIdenticonId = activeDid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showLargeIdenticonId || showLargeIdenticonUrl"
|
||||
class="fixed z-[100] top-0 inset-x-0 w-full"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||
>
|
||||
<EntityIcon
|
||||
:entityId="showLargeIdenticonId"
|
||||
:iconSize="512"
|
||||
:profileImageUrl="showLargeIdenticonUrl"
|
||||
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||
@click="
|
||||
showLargeIdenticonId = undefined;
|
||||
showLargeIdenticonUrl = undefined;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">ID</div>
|
||||
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||
@@ -113,7 +170,7 @@
|
||||
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
||||
<!-- label -->
|
||||
<div class="mb-2 font-bold">Settings</div>
|
||||
<div class="mb-2 font-bold">Notifications</div>
|
||||
<div
|
||||
v-if="!notificationMaybeChanged"
|
||||
class="flex items-center justify-between cursor-pointer"
|
||||
@@ -145,7 +202,11 @@
|
||||
<router-link class="pl-4 text-sm text-blue-500" to="/help-notifications">
|
||||
Troubleshoot your notification setup.
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
||||
<!-- label -->
|
||||
<div class="mb-2 font-bold">Location</div>
|
||||
<router-link
|
||||
:to="{ name: 'search-area' }"
|
||||
v-if="activeDid"
|
||||
@@ -154,19 +215,6 @@
|
||||
Set Search Area…
|
||||
<!-- If already set, change button label to "Change Search Area" -->
|
||||
</router-link>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold mt-6 mb-2">
|
||||
Topics of Interest
|
||||
</div>
|
||||
<textarea
|
||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||
rows="3"
|
||||
value="longing, rusted, seventeen, daybreak, furnace, nine, benign, homecoming, one, freight car"
|
||||
>
|
||||
</textarea>
|
||||
<div class="text-slate-500 text-sm mt-2 mb-2">
|
||||
Separate topics with a comma.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -546,6 +594,7 @@ import { ref } from "vue";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import TopMessage from "@/components/TopMessage.vue";
|
||||
import {
|
||||
@@ -564,6 +613,8 @@ import {
|
||||
ImageRateLimits,
|
||||
} from "@/libs/endorserServer";
|
||||
import { Buffer } from "buffer/";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import {Contact} from "@/db/tables/contacts";
|
||||
|
||||
interface IAccount {
|
||||
did: string;
|
||||
@@ -575,7 +626,7 @@ interface IAccount {
|
||||
const inputFileNameRef = ref<Blob>();
|
||||
|
||||
@Component({
|
||||
components: { QuickNav, TopMessage },
|
||||
components: {EntityIcon, GiftedPhotoDialog, QuickNav, TopMessage },
|
||||
})
|
||||
export default class AccountViewView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
@@ -595,10 +646,14 @@ export default class AccountViewView extends Vue {
|
||||
isRegistered = false;
|
||||
isSubscribed = false;
|
||||
notificationMaybeChanged = false;
|
||||
profileImageUrl?: string;
|
||||
publicHex = "";
|
||||
publicBase64 = "";
|
||||
showLargeIdenticonId?: string;
|
||||
showLargeIdenticonUrl?: string;
|
||||
webPushServer = "";
|
||||
webPushServerInput = "";
|
||||
|
||||
limitsMessage = "";
|
||||
loadingLimits = false;
|
||||
showContactGives = false;
|
||||
@@ -666,6 +721,7 @@ export default class AccountViewView extends Vue {
|
||||
(settings?.firstName || "") +
|
||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
this.profileImageUrl = settings?.profileImageUrl as string;
|
||||
this.showContactGives = !!settings?.showContactGivesInline;
|
||||
this.showShortcutBvc = !!settings?.showShortcutBvc;
|
||||
this.warnIfProdServer = !!settings?.warnIfProdServer;
|
||||
@@ -1269,5 +1325,95 @@ export default class AccountViewView extends Vue {
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
openPhotoDialog() {
|
||||
(this.$refs.photoDialog as GiftedPhotoDialog).open(
|
||||
async (imgUrl) => {
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
profileImageUrl: imgUrl,
|
||||
});
|
||||
this.profileImageUrl = imgUrl;
|
||||
//console.log("Got image URL:", imgUrl);
|
||||
},
|
||||
true,
|
||||
"profile",
|
||||
);
|
||||
}
|
||||
|
||||
confirmDeleteImage() {
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Are you sure you want to delete your profile picture?",
|
||||
text: "",
|
||||
onYes: this.deleteImage,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
async deleteImage() {
|
||||
if (!this.profileImageUrl) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
if (!identity) {
|
||||
throw Error("No identity found.");
|
||||
}
|
||||
const token = await accessToken(identity);
|
||||
const response = await this.axios.delete(
|
||||
DEFAULT_IMAGE_API_SERVER +
|
||||
"/image/" +
|
||||
encodeURIComponent(this.profileImageUrl),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (response.status === 204) {
|
||||
// don't bother with a notification
|
||||
// (either they'll simply continue or they're canceling and going back)
|
||||
} else {
|
||||
console.error("Non-success deleting image:", response);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was a problem deleting the image.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
// keep the imageUrl in localStorage so the user can try again if they want
|
||||
return;
|
||||
}
|
||||
|
||||
this.profileImageUrl = undefined;
|
||||
} catch (error) {
|
||||
console.error("Error deleting image:", error);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((error as any).response.status === 404) {
|
||||
console.log("The image was already deleted:", error);
|
||||
|
||||
this.profileImageUrl = undefined;
|
||||
|
||||
// it already doesn't exist so we won't say anything to the user
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error deleting the image.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -415,12 +415,11 @@ import { accessToken } from "@/libs/crypto";
|
||||
import * as serverUtil from "@/libs/endorserServer";
|
||||
import * as libsUtil from "@/libs/util";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
import { GiverInputInfo } from "@/libs/endorserServer";
|
||||
|
||||
@Component({
|
||||
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav },
|
||||
components: { GiftedDialog, OfferDialog, QuickNav },
|
||||
})
|
||||
export default class ClaimView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<h2 class="text-base flex gap-4 items-center">
|
||||
<span class="grow font-semibold">
|
||||
<EntityIcon
|
||||
:entityId="contact.did"
|
||||
:contact="contact"
|
||||
:iconSize="32"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
/>
|
||||
@@ -99,12 +99,10 @@ export default class ContactGiftingView extends Vue {
|
||||
allContacts: Array<Contact> = [];
|
||||
apiServer = "";
|
||||
accounts: typeof AccountsSchema;
|
||||
numAccounts = 0;
|
||||
projectId = localStorage.getItem("projectId") || "";
|
||||
|
||||
async beforeCreate() {
|
||||
accountsDB.open();
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
}
|
||||
|
||||
async created() {
|
||||
@@ -113,7 +111,13 @@ export default class ContactGiftingView extends Vue {
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.allContacts = await db.contacts.orderBy("name").toArray();
|
||||
|
||||
// .orderBy("name") wouldn't retrieve any entries with a blank name
|
||||
// .toCollection.sortBy("name") didn't sort in an order I understood
|
||||
const baseContacts = await db.contacts.toArray();
|
||||
this.allContacts = baseContacts.sort((a, b) =>
|
||||
(a.name || "").localeCompare(b.name || ""),
|
||||
);
|
||||
|
||||
localStorage.removeItem("projectId");
|
||||
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
:dotsOptions="{ type: 'square' }"
|
||||
class="flex justify-center"
|
||||
/>
|
||||
<span> Click QR to copy your contact URL to your clipboard. </span>
|
||||
<span> Click that QR to copy your contact URL to your clipboard. </span>
|
||||
<div>Not scanning? Show it in pieces.</div>
|
||||
</div>
|
||||
<div class="text-center" v-else>
|
||||
You have no identitifiers yet, so
|
||||
@@ -81,7 +82,7 @@ import { useClipboard } from "@vueuse/core";
|
||||
import { NotificationIface } from "@/constants/app";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { deriveAddress, nextDerivationPath, SimpleSigner } from "@/libs/crypto";
|
||||
import {deriveAddress, getContactPayloadFromJwtUrl, nextDerivationPath, SimpleSigner} from "@/libs/crypto";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
import {
|
||||
@@ -153,6 +154,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
|
||||
publicEncKey,
|
||||
nextPublicEncKeyHash: nextPublicEncKeyHashBase64,
|
||||
profileImageUrl: settings?.profileImageUrl,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -177,9 +179,24 @@ export default class ContactQRScanShow extends Vue {
|
||||
// Unfortunately, there are not typescript definitions for the qrcode-stream component yet.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onScanDetect(content: any) {
|
||||
if (content[0]?.rawValue) {
|
||||
localStorage.setItem("contactEndorserUrl", content[0].rawValue);
|
||||
this.$router.push({ name: "contacts" });
|
||||
const url = content[0]?.rawValue;
|
||||
if (url) {
|
||||
try {
|
||||
const fullData = getContactPayloadFromJwtUrl(url);
|
||||
console.log("fullData", fullData);
|
||||
localStorage.setItem("contactEndorserUrl", url);
|
||||
this.$router.push({ name: "contacts" });
|
||||
} catch (e) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Invalid Contact QR Code",
|
||||
text: "The QR code isn't in the right format.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
@@ -188,7 +205,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
title: "Invalid Contact QR Code",
|
||||
text: "No QR code detected with contact information.",
|
||||
},
|
||||
-1,
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -203,7 +220,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
title: "Invalid Scan",
|
||||
text: "The scan was invalid.",
|
||||
},
|
||||
-1,
|
||||
5000,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -94,17 +94,17 @@
|
||||
<div class="grow overflow-hidden">
|
||||
<h2 class="text-base font-semibold">
|
||||
<EntityIcon
|
||||
:entityId="contact.did"
|
||||
:contact="contact"
|
||||
:iconSize="24"
|
||||
class="inline-block align-text-bottom border border-slate-300 rounded"
|
||||
@click="showLargeIdenticon = contact.did"
|
||||
></EntityIcon>
|
||||
@click="showLargeIdenticon = contact"
|
||||
/>
|
||||
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
||||
<button
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md"
|
||||
@click="
|
||||
contactEdit = contact;
|
||||
contactNewName = contact.name;
|
||||
contactNewName = contact.name || '';
|
||||
"
|
||||
title="Edit"
|
||||
>
|
||||
@@ -246,10 +246,10 @@
|
||||
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||
>
|
||||
<EntityIcon
|
||||
:entityId="showLargeIdenticon"
|
||||
:contact="showLargeIdenticon"
|
||||
:iconSize="512"
|
||||
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||
@click="showLargeIdenticon = ''"
|
||||
@click="showLargeIdenticon = undefined"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -348,7 +348,7 @@ export default class ContactsView extends Vue {
|
||||
showGiveNumbers = false;
|
||||
showGiveTotals = true;
|
||||
showGiveConfirmed = true;
|
||||
showLargeIdenticon = "";
|
||||
showLargeIdenticon?: Contact;
|
||||
|
||||
AppString = AppString;
|
||||
libsUtil = libsUtil;
|
||||
@@ -364,7 +364,13 @@ export default class ContactsView extends Vue {
|
||||
if (this.showGiveNumbers) {
|
||||
this.loadGives();
|
||||
}
|
||||
this.contacts = await db.contacts.orderBy("name").toArray();
|
||||
|
||||
// .orderBy("name") wouldn't retrieve any entries with a blank name
|
||||
// .toCollection.sortBy("name") didn't sort in an order I understood
|
||||
const baseContacts = await db.contacts.toArray();
|
||||
this.contacts = baseContacts.sort((a, b) =>
|
||||
(a.name || "").localeCompare(b.name || ""),
|
||||
);
|
||||
|
||||
if (this.contactEndorserUrl) {
|
||||
await this.addContactFromScan(this.contactEndorserUrl);
|
||||
@@ -560,7 +566,13 @@ export default class ContactsView extends Vue {
|
||||
-1,
|
||||
);
|
||||
}
|
||||
this.contacts = await db.contacts.orderBy("name").toArray();
|
||||
|
||||
// .orderBy("name") wouldn't retrieve any entries with a blank name
|
||||
// .toCollection.sortBy("name") didn't sort in an order I understood
|
||||
const baseContacts = await db.contacts.toArray();
|
||||
this.contacts = baseContacts.sort((a, b) =>
|
||||
(a.name || "").localeCompare(b.name || ""),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -660,6 +672,7 @@ export default class ContactsView extends Vue {
|
||||
did: payload.iss,
|
||||
name: payload.own.name,
|
||||
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
|
||||
profileImageUrl: payload.own.profileImageUrl,
|
||||
publicKeyBase64: payload.own.publicEncKey,
|
||||
} as Contact);
|
||||
}
|
||||
@@ -864,7 +877,9 @@ export default class ContactsView extends Vue {
|
||||
let userMessage = "There was an error. See logs for more info.";
|
||||
const serverError = error as AxiosError;
|
||||
if (serverError) {
|
||||
if (serverError.message) {
|
||||
if (serverError.response?.data?.error?.message) {
|
||||
userMessage = serverError.response.data.error.message;
|
||||
} else if (serverError.message) {
|
||||
userMessage = serverError.message; // Info for the user
|
||||
} else {
|
||||
userMessage = JSON.stringify(serverError.toJSON());
|
||||
|
||||
@@ -131,7 +131,6 @@ import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import ProjectIcon from "@/components/ProjectIcon.vue";
|
||||
import TopMessage from "@/components/TopMessage.vue";
|
||||
import { NotificationIface } from "@/constants/app";
|
||||
@@ -143,7 +142,6 @@ import { didInfo, PlanData } from "@/libs/endorserServer";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
EntityIcon,
|
||||
InfiniteScroll,
|
||||
ProjectIcon,
|
||||
QuickNav,
|
||||
|
||||
@@ -285,6 +285,11 @@
|
||||
and you may have to close all your tabs. In addition, it may be running as an
|
||||
installed app, so look for any Time Safari app that may be running outside a browser.
|
||||
</li>
|
||||
<li>
|
||||
There may be a problem with your identity. Go to the Identity
|
||||
<fa icon="circle-user" class="fa-fw" /> page, then "Advanced", and "Switch Identifier"
|
||||
and you may see helpful info there. If it shows a problem, try adding your identifier again.
|
||||
</li>
|
||||
<li>
|
||||
It can help to reregister the service worker:
|
||||
<ul>
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
@click="openDialog(contact)"
|
||||
>
|
||||
<EntityIcon
|
||||
:entityId="contact.did"
|
||||
:contact="contact"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
/>
|
||||
@@ -268,7 +268,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import * as R from "ramda";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
@@ -306,8 +305,8 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
|
||||
displayName: string;
|
||||
known: boolean;
|
||||
};
|
||||
image: string;
|
||||
recipientProjectName: string | undefined;
|
||||
image?: string;
|
||||
recipientProjectName?: string;
|
||||
receiver: {
|
||||
displayName: string;
|
||||
known: boolean;
|
||||
@@ -488,70 +487,63 @@ export default class HomeView extends Vue {
|
||||
endOfResults = false;
|
||||
// include the descriptions of the giver and receiver
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
const newFeedData: Array<Promise<GiveRecordWithContactInfo>> =
|
||||
results.data.map(async (record: GiveSummaryRecord) => {
|
||||
// similar code is in endorser-mobile utility.ts
|
||||
// claim.claim happen for some claims wrapped in a Verifiable Credential
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const claim = (record.fullClaim as any).claim || record.fullClaim;
|
||||
// agent.did is for legacy data, before March 2023
|
||||
const giverDid =
|
||||
claim.agent?.identifier || (claim.agent as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
// recipient.did is for legacy data, before March 2023
|
||||
const recipientDid =
|
||||
claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const plan = await getPlanFromCache(
|
||||
record.fulfillsPlanHandleId,
|
||||
identity,
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
);
|
||||
for (const record: GiveSummaryRecord of results.data) {
|
||||
// similar code is in endorser-mobile utility.ts
|
||||
// claim.claim happen for some claims wrapped in a Verifiable Credential
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const claim = (record.fullClaim as any).claim || record.fullClaim;
|
||||
// agent.did is for legacy data, before March 2023
|
||||
const giverDid =
|
||||
claim.agent?.identifier || (claim.agent as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
// recipient.did is for legacy data, before March 2023
|
||||
const recipientDid =
|
||||
claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const plan = await getPlanFromCache(
|
||||
record.fulfillsPlanHandleId,
|
||||
identity,
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
);
|
||||
|
||||
// check if the record should be filtered out
|
||||
let anyMatch = false;
|
||||
if (
|
||||
this.isFeedFilteredByVisible &&
|
||||
containsNonHiddenDid(record)
|
||||
) {
|
||||
// has a visible DID so it's a keeper
|
||||
anyMatch = true;
|
||||
}
|
||||
if (!anyMatch && this.isFeedFilteredByNearby) {
|
||||
// check if the associated project has a location inside user's search box
|
||||
if (record.fulfillsPlanHandleId) {
|
||||
if (plan?.locLat && plan?.locLon) {
|
||||
if (this.latLongInAnySearchBox(plan.locLat, plan.locLon)) {
|
||||
anyMatch = true;
|
||||
}
|
||||
// check if the record should be filtered out
|
||||
let anyMatch = false;
|
||||
if (this.isFeedFilteredByVisible && containsNonHiddenDid(record)) {
|
||||
// has a visible DID so it's a keeper
|
||||
anyMatch = true;
|
||||
}
|
||||
if (!anyMatch && this.isFeedFilteredByNearby) {
|
||||
// check if the associated project has a location inside user's search box
|
||||
if (record.fulfillsPlanHandleId) {
|
||||
if (plan?.locLat && plan?.locLon) {
|
||||
if (this.latLongInAnySearchBox(plan.locLat, plan.locLon)) {
|
||||
anyMatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.isAnyFeedFilterOn && !anyMatch) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (this.isAnyFeedFilterOn && !anyMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
giver: didInfoForContact(
|
||||
giverDid,
|
||||
this.activeDid,
|
||||
contactForDid(giverDid, this.allContacts),
|
||||
this.allMyDids,
|
||||
),
|
||||
image: claim.image,
|
||||
recipientProjectName: plan?.name,
|
||||
receiver: didInfoForContact(
|
||||
recipientDid,
|
||||
this.activeDid,
|
||||
contactForDid(recipientDid, this.allContacts),
|
||||
this.allMyDids,
|
||||
),
|
||||
};
|
||||
});
|
||||
const allNewFeedData: GiveRecordWithContactInfo[] =
|
||||
await Promise.all(newFeedData);
|
||||
const filteredFeedData = allNewFeedData.filter(R.isNotNil);
|
||||
this.feedData = this.feedData.concat(filteredFeedData);
|
||||
const newRecord: GiveRecordWithContactInfo = {
|
||||
...record,
|
||||
giver: didInfoForContact(
|
||||
giverDid,
|
||||
this.activeDid,
|
||||
contactForDid(giverDid, this.allContacts),
|
||||
this.allMyDids,
|
||||
),
|
||||
image: claim.image,
|
||||
recipientProjectName: plan?.name as string,
|
||||
receiver: didInfoForContact(
|
||||
recipientDid,
|
||||
this.activeDid,
|
||||
contactForDid(recipientDid, this.allContacts),
|
||||
this.allMyDids,
|
||||
),
|
||||
};
|
||||
this.feedData.push(newRecord);
|
||||
}
|
||||
this.feedPreviousOldestId =
|
||||
results.data[results.data.length - 1].jwtId;
|
||||
// The following update is only done on the first load.
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<!-- SHOW ME instead while processing saving changes -->
|
||||
<button
|
||||
type="button"
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
|
||||
@click="onClickCancel()"
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
@click="openGiftDialog(contact)"
|
||||
>
|
||||
<EntityIcon
|
||||
:entityId="contact.did"
|
||||
:contact="contact"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
/>
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
:entityId="offer.recipientDid"
|
||||
:iconSize="48"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md"
|
||||
></EntityIcon>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
</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.
|
||||
This location is only stored on your device. It is sometimes sent from
|
||||
your device to run searches but it is not stored on our servers.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -112,6 +112,25 @@ self.addEventListener("message", (event) => {
|
||||
logConsoleAndDb("Service worker posted a message.");
|
||||
});
|
||||
|
||||
self.addEventListener("notificationclick", (event) => {
|
||||
logConsoleAndDb("Notification got clicked.", event);
|
||||
event.notification.close();
|
||||
// from https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/notificationclick_event
|
||||
// ... though I don't see any benefit over just "clients.openWindow"
|
||||
event.waitUntil(
|
||||
clients
|
||||
.matchAll({
|
||||
type: "window",
|
||||
})
|
||||
.then((clientList) => {
|
||||
for (const client of clientList) {
|
||||
if (client.url === "/" && "focus" in client) return client.focus();
|
||||
}
|
||||
if (clients.openWindow) return clients.openWindow("/");
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
logConsoleAndDb("Service worker got fetch event.", event);
|
||||
});
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import * as path from "path";
|
||||
import * as path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 8080
|
||||
},
|
||||
plugins: [ vue() ],
|
||||
plugins: [
|
||||
vue(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
strategies: 'injectManifest',
|
||||
srcDir: '.',
|
||||
filename: 'sw_scripts-combined.js',
|
||||
manifest: {
|
||||
name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
buffer: path.resolve(__dirname, 'node_modules', 'buffer'),
|
||||
'dexie-export-import/dist/import': 'dexie-export-import/dist/import/index.js',
|
||||
},
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
const TIME_SAFARI_APP_TITLE =
|
||||
import.meta.env.VITE_TIME_SAFARI_APP_TITLE || require("./package.json").name;
|
||||
|
||||
module.exports = defineConfig({
|
||||
pwa: {
|
||||
name: TIME_SAFARI_APP_TITLE,
|
||||
iconPaths: {
|
||||
faviconSVG: "img/icons/safari-pinned-tab.svg",
|
||||
},
|
||||
workboxPluginMode: "InjectManifest",
|
||||
workboxOptions: {
|
||||
// this script will be checked for linting (sw_scripts/* files generate about 1000 linting errors)
|
||||
swSrc: "./sw_scripts-combined.js",
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user