profile-pic #114

Merged
trentlarson merged 4 commits from profile-pic into master 7 months ago
  1. 45
      package-lock.json
  2. 3
      package.json
  3. 25
      src/components/EntityIcon.vue
  4. 60
      src/components/GiftedPhotoDialog.vue
  5. 1
      src/db/tables/contacts.ts
  6. 7
      src/db/tables/settings.ts
  7. 169
      src/views/AccountViewView.vue
  8. 3
      src/views/ClaimView.vue
  9. 2
      src/views/ContactGiftingView.vue
  10. 31
      src/views/ContactQRScanShowView.vue
  11. 15
      src/views/ContactsView.vue
  12. 2
      src/views/DiscoverView.vue
  13. 122
      src/views/HomeView.vue
  14. 2
      src/views/ProjectViewView.vue
  15. 2
      src/views/ProjectsView.vue

45
package-lock.json

@ -59,6 +59,7 @@
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.4", "vue-facing-decorator": "^3.0.4",
"vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3", "vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.3.0", "vue-router": "^4.3.0",
"web-did-resolver": "^2.0.27" "web-did-resolver": "^2.0.27"
@ -2369,6 +2370,14 @@
"node": ">=6.9.0" "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": { "node_modules/@bitauth/libauth": {
"version": "1.19.1", "version": "1.19.1",
"license": "MIT", "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": { "node_modules/@xmldom/xmldom": {
"version": "0.7.13", "version": "0.7.13",
"license": "MIT", "license": "MIT",
@ -11439,6 +11467,11 @@
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"optional": true "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": { "node_modules/cross-fetch": {
"version": "4.0.0", "version": "4.0.0",
"license": "MIT", "license": "MIT",
@ -21419,6 +21452,18 @@
"vue": "^3.0.0" "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": { "node_modules/vue-qrcode-reader": {
"version": "5.5.3", "version": "5.5.3",
"license": "MIT", "license": "MIT",

3
package.json

@ -6,7 +6,7 @@
"dev": "vite", "dev": "vite",
"serve": "vite preview", "serve": "vite preview",
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build", "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", "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" "prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js"
}, },
@ -62,6 +62,7 @@
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.4", "vue-facing-decorator": "^3.0.4",
"vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3", "vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.3.0", "vue-router": "^4.3.0",
"web-did-resolver": "^2.0.27" "web-did-resolver": "^2.0.27"

25
src/components/EntityIcon.vue

@ -5,20 +5,29 @@
import { createAvatar, StyleOptions } from "@dicebear/core"; import { createAvatar, StyleOptions } from "@dicebear/core";
import { avataaars } from "@dicebear/collection"; import { avataaars } from "@dicebear/collection";
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { Contact } from "@/db/tables/contacts";
@Component @Component
export default class EntityIcon extends Vue { export default class EntityIcon extends Vue {
@Prop entityId = ""; @Prop contact: Contact;
@Prop entityId = ""; // overridden by contact.did or profileImageUrl
@Prop iconSize = 0; @Prop iconSize = 0;
@Prop profileImageUrl = ""; // overridden by contact.profileImageUrl
generateIcon() { generateIcon() {
const options: StyleOptions<object> = { const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
seed: this.entityId || "", if (imageUrl) {
size: this.iconSize, return `<img src="${imageUrl}" alt="avatar" width="${this.iconSize}" height="${this.iconSize}" />`;
}; } else {
const avatar = createAvatar(avataaars, options); const identifier = this.contact?.did || this.entityId;
const svgString = avatar.toString(); const options: StyleOptions<object> = {
return svgString; seed: (identifier as string) || "",
size: this.iconSize,
};
const avatar = createAvatar(avataaars, options);
const svgString = avatar.toString();
return svgString;
}
} }
} }
</script> </script>

60
src/components/GiftedPhotoDialog.vue

@ -20,28 +20,54 @@
</div> </div>
<div v-if="uploading" class="flex justify-center"> <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>
<div v-else-if="blob"> <div v-else-if="blob">
<div <div v-if="crop">
class="flex justify-center gap-2 absolute bottom-[1rem] left-[1rem] right-[1rem] bg-black/50 px-4 py-2" <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 <button
@click="uploadImage" @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> <span>Upload</span>
</button> </button>
</div>
<div class="absolute bottom-[1rem] right-[1rem] px-2 py-1">
<button <button
@click="retryImage" @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> <span>Retry</span>
</button> </button>
</div> </div>
<div class="flex justify-center">
<img :src="URL.createObjectURL(blob)" class="mt-2 rounded" />
</div>
</div> </div>
<div v-else ref="cameraContainer"> <div v-else ref="cameraContainer">
<!-- <!--
@ -92,6 +118,7 @@
import axios from "axios"; import axios from "axios";
import Camera from "simple-vue-camera"; import Camera from "simple-vue-camera";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { getIdentity } from "@/libs/util"; import { getIdentity } from "@/libs/util";
@ -99,13 +126,15 @@ import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
@Component({ components: { Camera } }) @Component({ components: { Camera, VuePictureCropper } })
export default class GiftedPhotoDialog extends Vue { export default class GiftedPhotoDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeDeviceNumber = 0; activeDeviceNumber = 0;
activeDid = ""; activeDid = "";
blob: Blob | null = null; blob: Blob | null = null;
claimType = "GiveAction";
crop = false;
mirror = false; mirror = false;
numDevices = 0; numDevices = 0;
setImage: (arg: string) => void = () => {}; 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.visible = true;
this.crop = !!crop;
this.claimType = claimType || "GiveAction";
const bottomNav = document.querySelector("#QuickNav") as HTMLElement; const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
if (bottomNav) { if (bottomNav) {
bottomNav.style.display = "none"; bottomNav.style.display = "none";
@ -274,6 +305,11 @@ export default class GiftedPhotoDialog extends Vue {
async uploadImage() { async uploadImage() {
this.uploading = true; this.uploading = true;
if (this.crop) {
this.blob = await cropper?.getBlob();
}
const identifier = await getIdentity(this.activeDid); const identifier = await getIdentity(this.activeDid);
const token = await accessToken(identifier); const token = await accessToken(identifier);
const headers = { const headers = {
@ -295,7 +331,7 @@ export default class GiftedPhotoDialog extends Vue {
return; return;
} }
formData.append("image", this.blob, "snapshot.png"); // png is set in snapshot() formData.append("image", this.blob, "snapshot.png"); // png is set in snapshot()
formData.append("claimType", "GiveAction"); formData.append("claimType", this.claimType);
try { try {
const response = await axios.post( const response = await axios.post(
DEFAULT_IMAGE_API_SERVER + "/image", DEFAULT_IMAGE_API_SERVER + "/image",

1
src/db/tables/contacts.ts

@ -2,6 +2,7 @@ export interface Contact {
did: string; did: string;
name?: string; name?: string;
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
profileImageUrl?: string;
publicKeyBase64?: string; publicKeyBase64?: string;
seesMe?: boolean; seesMe?: boolean;
registered?: boolean; registered?: boolean;

7
src/db/tables/settings.ts

@ -20,11 +20,12 @@ export type Settings = {
filterFeedByNearby?: boolean; // filter by nearby filterFeedByNearby?: boolean; // filter by nearby
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden
firstName?: string; // User's first name firstName?: string; // user's full name
isRegistered?: boolean; isRegistered?: boolean;
lastName?: string; // deprecated - put all names in firstName lastName?: string; // deprecated - put all names in firstName
lastNotifiedClaimId?: string; // Last notified claim ID lastNotifiedClaimId?: string;
lastViewedClaimId?: string; // Last viewed claim ID lastViewedClaimId?: string;
profileImageUrl?: string;
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
reminderOn?: boolean; // Toggle to enable or disable reminders reminderOn?: boolean; // Toggle to enable or disable reminders

169
src/views/AccountViewView.vue

@ -63,12 +63,14 @@
<!-- Identity Details --> <!-- Identity Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 v-if="givenName" class="text-xl font-semibold mb-2"> <div v-if="givenName">
{{ givenName }} <h2 class="text-xl font-semibold mb-2">
<router-link :to="{ name: 'new-edit-account' }"> {{ givenName }}
<fa icon="pen" class="text-xs text-blue-500 mb-1"></fa> <router-link :to="{ name: 'new-edit-account' }">
</router-link> <fa icon="pen" class="text-xs text-blue-500 mb-1"></fa>
</h2> </router-link>
</h2>
</div>
<span v-else> <span v-else>
<router-link <router-link
:to="{ name: 'new-edit-account' }" :to="{ name: 'new-edit-account' }"
@ -77,6 +79,61 @@
Set Your Name Set Your Name
</router-link> </router-link>
</span> </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-slate-500 text-sm font-bold">ID</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1"> <div class="text-sm text-slate-500 flex justify-start items-center mb-1">
@ -537,6 +594,7 @@ import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { import {
@ -555,6 +613,8 @@ import {
ImageRateLimits, ImageRateLimits,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Buffer } from "buffer/"; import { Buffer } from "buffer/";
import EntityIcon from "@/components/EntityIcon.vue";
import {Contact} from "@/db/tables/contacts";
interface IAccount { interface IAccount {
did: string; did: string;
@ -566,7 +626,7 @@ interface IAccount {
const inputFileNameRef = ref<Blob>(); const inputFileNameRef = ref<Blob>();
@Component({ @Component({
components: { QuickNav, TopMessage }, components: {EntityIcon, GiftedPhotoDialog, QuickNav, TopMessage },
}) })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
@ -586,10 +646,14 @@ export default class AccountViewView extends Vue {
isRegistered = false; isRegistered = false;
isSubscribed = false; isSubscribed = false;
notificationMaybeChanged = false; notificationMaybeChanged = false;
profileImageUrl?: string;
publicHex = ""; publicHex = "";
publicBase64 = ""; publicBase64 = "";
showLargeIdenticonId?: string;
showLargeIdenticonUrl?: string;
webPushServer = ""; webPushServer = "";
webPushServerInput = ""; webPushServerInput = "";
limitsMessage = ""; limitsMessage = "";
loadingLimits = false; loadingLimits = false;
showContactGives = false; showContactGives = false;
@ -657,6 +721,7 @@ export default class AccountViewView extends Vue {
(settings?.firstName || "") + (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3 (settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.profileImageUrl = settings?.profileImageUrl as string;
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
this.showShortcutBvc = !!settings?.showShortcutBvc; this.showShortcutBvc = !!settings?.showShortcutBvc;
this.warnIfProdServer = !!settings?.warnIfProdServer; this.warnIfProdServer = !!settings?.warnIfProdServer;
@ -1260,5 +1325,95 @@ export default class AccountViewView extends Vue {
-1, -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> </script>

3
src/views/ClaimView.vue

@ -415,12 +415,11 @@ import { accessToken } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { GiverInputInfo } from "@/libs/endorserServer"; import { GiverInputInfo } from "@/libs/endorserServer";
@Component({ @Component({
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav }, components: { GiftedDialog, OfferDialog, QuickNav },
}) })
export default class ClaimView extends Vue { export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;

2
src/views/ContactGiftingView.vue

@ -47,7 +47,7 @@
<h2 class="text-base flex gap-4 items-center"> <h2 class="text-base flex gap-4 items-center">
<span class="grow font-semibold"> <span class="grow font-semibold">
<EntityIcon <EntityIcon
:entityId="contact.did" :contact="contact"
:iconSize="32" :iconSize="32"
class="inline-block align-middle border border-slate-300 rounded-md mr-1" class="inline-block align-middle border border-slate-300 rounded-md mr-1"
/> />

31
src/views/ContactQRScanShowView.vue

@ -44,7 +44,8 @@
:dotsOptions="{ type: 'square' }" :dotsOptions="{ type: 'square' }"
class="flex justify-center" class="flex justify-center"
/> />
<span> Click QR to copy your contact URL to your clipboard. </span> <span> Click that QR to copy your contact URL to your clipboard. </span>
<div>Not scanning? Show it in pieces.</div>
</div> </div>
<div class="text-center" v-else> <div class="text-center" v-else>
You have no identitifiers yet, so You have no identitifiers yet, so
@ -81,7 +82,7 @@ import { useClipboard } from "@vueuse/core";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, nextDerivationPath, SimpleSigner } from "@/libs/crypto"; import {deriveAddress, getContactPayloadFromJwtUrl, nextDerivationPath, SimpleSigner} from "@/libs/crypto";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { import {
@ -153,6 +154,7 @@ export default class ContactQRScanShow extends Vue {
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3 (settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
publicEncKey, publicEncKey,
nextPublicEncKeyHash: nextPublicEncKeyHashBase64, 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. // Unfortunately, there are not typescript definitions for the qrcode-stream component yet.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanDetect(content: any) { onScanDetect(content: any) {
if (content[0]?.rawValue) { const url = content[0]?.rawValue;
localStorage.setItem("contactEndorserUrl", content[0].rawValue); if (url) {
this.$router.push({ name: "contacts" }); 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 { } else {
this.$notify( this.$notify(
{ {
@ -188,7 +205,7 @@ export default class ContactQRScanShow extends Vue {
title: "Invalid Contact QR Code", title: "Invalid Contact QR Code",
text: "No QR code detected with contact information.", text: "No QR code detected with contact information.",
}, },
-1, 5000,
); );
} }
} }
@ -203,7 +220,7 @@ export default class ContactQRScanShow extends Vue {
title: "Invalid Scan", title: "Invalid Scan",
text: "The scan was invalid.", text: "The scan was invalid.",
}, },
-1, 5000,
); );
} }

15
src/views/ContactsView.vue

@ -94,17 +94,17 @@
<div class="grow overflow-hidden"> <div class="grow overflow-hidden">
<h2 class="text-base font-semibold"> <h2 class="text-base font-semibold">
<EntityIcon <EntityIcon
:entityId="contact.did" :contact="contact"
:iconSize="24" :iconSize="24"
class="inline-block align-text-bottom border border-slate-300 rounded" class="inline-block align-text-bottom border border-slate-300 rounded"
@click="showLargeIdenticon = contact.did" @click="showLargeIdenticon = contact"
></EntityIcon> />
{{ contact.name || AppString.NO_CONTACT_NAME }} {{ contact.name || AppString.NO_CONTACT_NAME }}
<button <button
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md" class="text-sm uppercase bg-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=" @click="
contactEdit = contact; contactEdit = contact;
contactNewName = contact.name; contactNewName = contact.name || '';
" "
title="Edit" title="Edit"
> >
@ -246,10 +246,10 @@
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50" class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
> >
<EntityIcon <EntityIcon
:entityId="showLargeIdenticon" :contact="showLargeIdenticon"
:iconSize="512" :iconSize="512"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg" 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>
</div> </div>
@ -348,7 +348,7 @@ export default class ContactsView extends Vue {
showGiveNumbers = false; showGiveNumbers = false;
showGiveTotals = true; showGiveTotals = true;
showGiveConfirmed = true; showGiveConfirmed = true;
showLargeIdenticon = ""; showLargeIdenticon?: Contact;
AppString = AppString; AppString = AppString;
libsUtil = libsUtil; libsUtil = libsUtil;
@ -672,6 +672,7 @@ export default class ContactsView extends Vue {
did: payload.iss, did: payload.iss,
name: payload.own.name, name: payload.own.name,
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash, nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
profileImageUrl: payload.own.profileImageUrl,
publicKeyBase64: payload.own.publicEncKey, publicKeyBase64: payload.own.publicEncKey,
} as Contact); } as Contact);
} }

2
src/views/DiscoverView.vue

@ -131,7 +131,6 @@ import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import ProjectIcon from "@/components/ProjectIcon.vue"; import ProjectIcon from "@/components/ProjectIcon.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
@ -143,7 +142,6 @@ import { didInfo, PlanData } from "@/libs/endorserServer";
@Component({ @Component({
components: { components: {
EntityIcon,
InfiniteScroll, InfiniteScroll,
ProjectIcon, ProjectIcon,
QuickNav, QuickNav,

122
src/views/HomeView.vue

@ -138,7 +138,7 @@
@click="openDialog(contact)" @click="openDialog(contact)"
> >
<EntityIcon <EntityIcon
:entityId="contact.did" :contact="contact"
:iconSize="64" :iconSize="64"
class="mx-auto border border-slate-300 rounded-md mb-1" class="mx-auto border border-slate-300 rounded-md mb-1"
/> />
@ -268,7 +268,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import * as R from "ramda";
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
@ -306,8 +305,8 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
displayName: string; displayName: string;
known: boolean; known: boolean;
}; };
image: string; image?: string;
recipientProjectName: string | undefined; recipientProjectName?: string;
receiver: { receiver: {
displayName: string; displayName: string;
known: boolean; known: boolean;
@ -488,70 +487,63 @@ export default class HomeView extends Vue {
endOfResults = false; endOfResults = false;
// include the descriptions of the giver and receiver // include the descriptions of the giver and receiver
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
const newFeedData: Array<Promise<GiveRecordWithContactInfo>> = for (const record: GiveSummaryRecord of results.data) {
results.data.map(async (record: GiveSummaryRecord) => { // similar code is in endorser-mobile utility.ts
// similar code is in endorser-mobile utility.ts // claim.claim happen for some claims wrapped in a Verifiable Credential
// claim.claim happen for some claims wrapped in a Verifiable Credential // eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any const claim = (record.fullClaim as any).claim || record.fullClaim;
const claim = (record.fullClaim as any).claim || record.fullClaim; // agent.did is for legacy data, before March 2023
// agent.did is for legacy data, before March 2023 const giverDid =
const giverDid = claim.agent?.identifier || (claim.agent as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
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
// recipient.did is for legacy data, before March 2023 const recipientDid =
const recipientDid = claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any const plan = await getPlanFromCache(
const plan = await getPlanFromCache( record.fulfillsPlanHandleId,
record.fulfillsPlanHandleId, identity,
identity, this.axios,
this.axios, this.apiServer,
this.apiServer, );
);
// check if the record should be filtered out
// check if the record should be filtered out let anyMatch = false;
let anyMatch = false; if (this.isFeedFilteredByVisible && containsNonHiddenDid(record)) {
if ( // has a visible DID so it's a keeper
this.isFeedFilteredByVisible && anyMatch = true;
containsNonHiddenDid(record) }
) { if (!anyMatch && this.isFeedFilteredByNearby) {
// has a visible DID so it's a keeper // check if the associated project has a location inside user's search box
anyMatch = true; if (record.fulfillsPlanHandleId) {
} if (plan?.locLat && plan?.locLon) {
if (!anyMatch && this.isFeedFilteredByNearby) { if (this.latLongInAnySearchBox(plan.locLat, plan.locLon)) {
// check if the associated project has a location inside user's search box anyMatch = true;
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, const newRecord: GiveRecordWithContactInfo = {
giver: didInfoForContact( ...record,
giverDid, giver: didInfoForContact(
this.activeDid, giverDid,
contactForDid(giverDid, this.allContacts), this.activeDid,
this.allMyDids, contactForDid(giverDid, this.allContacts),
), this.allMyDids,
image: claim.image, ),
recipientProjectName: plan?.name, image: claim.image,
receiver: didInfoForContact( recipientProjectName: plan?.name as string,
recipientDid, receiver: didInfoForContact(
this.activeDid, recipientDid,
contactForDid(recipientDid, this.allContacts), this.activeDid,
this.allMyDids, contactForDid(recipientDid, this.allContacts),
), this.allMyDids,
}; ),
}); };
const allNewFeedData: GiveRecordWithContactInfo[] = this.feedData.push(newRecord);
await Promise.all(newFeedData); }
const filteredFeedData = allNewFeedData.filter(R.isNotNil);
this.feedData = this.feedData.concat(filteredFeedData);
this.feedPreviousOldestId = this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId; results.data[results.data.length - 1].jwtId;
// The following update is only done on the first load. // The following update is only done on the first load.

2
src/views/ProjectViewView.vue

@ -162,7 +162,7 @@
@click="openGiftDialog(contact)" @click="openGiftDialog(contact)"
> >
<EntityIcon <EntityIcon
:entityId="contact.did" :contact="contact"
:iconSize="64" :iconSize="64"
class="mx-auto border border-slate-300 rounded-md mb-1" class="mx-auto border border-slate-300 rounded-md mb-1"
/> />

2
src/views/ProjectsView.vue

@ -98,7 +98,7 @@
:entityId="offer.recipientDid" :entityId="offer.recipientDid"
:iconSize="48" :iconSize="48"
class="inline-block align-middle border border-slate-300 rounded-md" class="inline-block align-middle border border-slate-300 rounded-md"
></EntityIcon> />
</div> </div>
<div> <div>

Loading…
Cancel
Save