diff --git a/package-lock.json b/package-lock.json index ca08fc893..ee6df906d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,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" @@ -2369,6 +2370,14 @@ "node": ">=6.9.0" } }, + "node_modules/@bassist/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@bassist/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-aoFTl0jUjm8/tDZodP41wnEkvB+C5O9NFCuYN/ztL6jSUSsuBkXq90/1ifBm1XhV/zySHgLYlU1+tgo3XtQ+nA==", + "dependencies": { + "@withtypes/mime": "^0.1.2" + } + }, "node_modules/@bitauth/libauth": { "version": "1.19.1", "license": "MIT", @@ -9696,6 +9705,25 @@ } } }, + "node_modules/@withtypes/mime": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@withtypes/mime/-/mime-0.1.2.tgz", + "integrity": "sha512-PB9BfZGzwblUONJY0LiOwsHCA6uV3DIPj/w9ReekdHxPOl0VdUFgI5s4avKycuuq9Gf5Nz2ZPA2O36GAUzlMPA==", + "dependencies": { + "mime": "^3.0.0" + } + }, + "node_modules/@withtypes/mime/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.7.13", "license": "MIT", @@ -11439,6 +11467,11 @@ "license": "SEE LICENSE IN LICENSE.md", "optional": true }, + "node_modules/cropperjs": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.1.tgz", + "integrity": "sha512-F4wsi+XkDHCOMrHMYjrTEE4QBOrsHHN5/2VsVAaRq8P7E5z7xQpT75S+f/9WikmBEailas3+yo+6zPIomW+NOA==" + }, "node_modules/cross-fetch": { "version": "4.0.0", "license": "MIT", @@ -21419,6 +21452,18 @@ "vue": "^3.0.0" } }, + "node_modules/vue-picture-cropper": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz", + "integrity": "sha512-NF7+Dgso6d0GB16E5d/BbrcTIHm1VWz8dS3IjLhoBl+ZeC+yDA46CyJphQuO32SisaPmrKHN8VbiE2LgAfhnkQ==", + "dependencies": { + "@bassist/utils": "^0.4.0", + "cropperjs": "^1.6.1" + }, + "peerDependencies": { + "vue": ">=3.2.13" + } + }, "node_modules/vue-qrcode-reader": { "version": "5.5.3", "license": "MIT", diff --git a/package.json b/package.json index e0af68f28..b5b5a6ae5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev": "vite", "serve": "vite preview", "build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build", - "lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src", + "lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src", "lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src", "prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js" }, @@ -62,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" diff --git a/src/components/EntityIcon.vue b/src/components/EntityIcon.vue index 373da04d4..b14be8452 100644 --- a/src/components/EntityIcon.vue +++ b/src/components/EntityIcon.vue @@ -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 = { - 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 `avatar`; + } else { + const identifier = this.contact?.did || this.entityId; + const options: StyleOptions = { + seed: (identifier as string) || "", + size: this.iconSize, + }; + const avatar = createAvatar(avataaars, options); + const svgString = avatar.toString(); + return svgString; + } } } diff --git a/src/components/GiftedPhotoDialog.vue b/src/components/GiftedPhotoDialog.vue index 8fb17bf1d..80b5b0517 100644 --- a/src/components/GiftedPhotoDialog.vue +++ b/src/components/GiftedPhotoDialog.vue @@ -20,28 +20,54 @@
- +
-
+
+ + +
+
+
+ +
+
+
+
+
-
- -
-

- {{ givenName }} - - - -

+
+

+ {{ givenName }} + + + +

+
+
+ + + + + + + + +
+
+
+ ... and those without your image see this: +
+
+ +
+
+
+
+ +
+
ID
@@ -537,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 { @@ -555,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; @@ -566,7 +626,7 @@ interface IAccount { const inputFileNameRef = ref(); @Component({ - components: { QuickNav, TopMessage }, + components: {EntityIcon, GiftedPhotoDialog, QuickNav, TopMessage }, }) export default class AccountViewView extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; @@ -586,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; @@ -657,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; @@ -1260,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, + ); + } + } + } } diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index f6237166d..e47e40efe 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -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; diff --git a/src/views/ContactGiftingView.vue b/src/views/ContactGiftingView.vue index d522729d1..446abcc24 100644 --- a/src/views/ContactGiftingView.vue +++ b/src/views/ContactGiftingView.vue @@ -47,7 +47,7 @@

diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 72cbc9585..be3a0c794 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -44,7 +44,8 @@ :dotsOptions="{ type: 'square' }" class="flex justify-center" /> - Click QR to copy your contact URL to your clipboard. + Click that QR to copy your contact URL to your clipboard. +
Not scanning? Show it in pieces.

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, ); } diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index e5c699beb..6f1119d45 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -94,17 +94,17 @@

+ @click="showLargeIdenticon = contact" + /> {{ contact.name || AppString.NO_CONTACT_NAME }}

@@ -348,7 +348,7 @@ export default class ContactsView extends Vue { showGiveNumbers = false; showGiveTotals = true; showGiveConfirmed = true; - showLargeIdenticon = ""; + showLargeIdenticon?: Contact; AppString = AppString; libsUtil = libsUtil; @@ -672,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); } diff --git a/src/views/DiscoverView.vue b/src/views/DiscoverView.vue index 9126cc5a5..5256ca505 100644 --- a/src/views/DiscoverView.vue +++ b/src/views/DiscoverView.vue @@ -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, diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 66511dbd8..dc1479a4f 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -138,7 +138,7 @@ @click="openDialog(contact)" > @@ -268,7 +268,6 @@