From aa7d82c531bfb092f35e24c17a7ff58b4fd6649b Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 10 May 2024 13:17:20 -0600 Subject: [PATCH 1/3] add a share_target for people to add a photo --- src/components/GiftedPhotoDialog.vue | 2 +- src/constants/app.ts | 2 + src/db/index.ts | 26 +++-- src/db/tables/temp.ts | 13 +++ src/router/index.ts | 5 + src/views/AccountViewView.vue | 18 +-- src/views/GiftedDetails.vue | 19 +++- src/views/HelpView.vue | 45 ++++++-- src/views/NewEditProjectView.vue | 2 +- src/views/ProjectViewView.vue | 24 ++-- src/views/SeedBackupView.vue | 5 +- src/views/SharedPhotoView.vue | 164 +++++++++++++++++++++++++++ src/views/TestView.vue | 55 ++++++++- sw_scripts/additional-scripts.js | 21 +++- sw_scripts/safari-notifications.js | 14 +++ vite.config.mjs | 21 ++++ 16 files changed, 377 insertions(+), 59 deletions(-) create mode 100644 src/db/tables/temp.ts create mode 100644 src/views/SharedPhotoView.vue diff --git a/src/components/GiftedPhotoDialog.vue b/src/components/GiftedPhotoDialog.vue index 20600bf..93c3582 100644 --- a/src/components/GiftedPhotoDialog.vue +++ b/src/components/GiftedPhotoDialog.vue @@ -330,7 +330,7 @@ export default class GiftedPhotoDialog extends Vue { this.uploading = false; return; } - formData.append("image", this.blob, "snapshot.png"); // png is set in snapshot() + formData.append("image", this.blob, "snapshot.png"); formData.append("claimType", this.claimType); try { const response = await axios.post( diff --git a/src/constants/app.ts b/src/constants/app.ts index 4f51f6b..161644e 100644 --- a/src/constants/app.ts +++ b/src/constants/app.ts @@ -30,6 +30,8 @@ export const DEFAULT_IMAGE_API_SERVER = export const DEFAULT_PUSH_SERVER = window.location.protocol + "//" + window.location.host; +export const IMAGE_TYPE_PROFILE = "profile"; + /** * The possible values for "group" and "type" are in App.vue. * From the notiwind package diff --git a/src/db/index.ts b/src/db/index.ts index 290a001..6bb59f2 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -8,6 +8,7 @@ import { Settings, SettingsSchema, } from "./tables/settings"; +import { Temp, TempSchema } from "./tables/temp"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; // Define types for tables that hold sensitive and non-sensitive data @@ -16,6 +17,7 @@ type NonsensitiveTables = { contacts: Table; logs: Table; settings: Table; + temp: Table; }; // Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings @@ -25,14 +27,7 @@ export type NonsensitiveDexie = // Initialize Dexie databases for sensitive and non-sensitive data export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie; -const SensitiveSchemas = { ...AccountsSchema }; - export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie; -const NonsensitiveSchemas = { - ...ContactSchema, - ...LogSchema, - ...SettingsSchema, -}; // Manage the encryption key. If not present in localStorage, create and store it. const secret = @@ -42,11 +37,18 @@ if (!localStorage.getItem("secret")) localStorage.setItem("secret", secret); // Apply encryption to the sensitive database using the secret key encrypted(accountsDB, { secretKey: secret }); -// Define the schema for our databases -accountsDB.version(1).stores(SensitiveSchemas); -// v1 was contacts & settings -// v2 added logs -db.version(2).stores(NonsensitiveSchemas); +// Define the schemas for our databases +// Only the tables with index modifications need listing. https://dexie.org/docs/Tutorial/Design#database-versioning +accountsDB.version(1).stores(AccountsSchema); +// v1 also had contacts & settings +// v2 added Log +db.version(2).stores({ + ...ContactSchema, + ...LogSchema, + ...SettingsSchema, +}); +// v3 added Temp +db.version(3).stores(TempSchema); // Event handler to initialize the non-sensitive database with default settings db.on("populate", () => { diff --git a/src/db/tables/temp.ts b/src/db/tables/temp.ts new file mode 100644 index 0000000..02b592b --- /dev/null +++ b/src/db/tables/temp.ts @@ -0,0 +1,13 @@ +// for ephemeral uses, eg. passing a blob from the service worker to the main thread + +export type Temp = { + id: string; + blob?: Blob; +}; + +/** + * Schema for the Temp table in the database. + */ +export const TempSchema = { + temp: "id", +}; diff --git a/src/router/index.ts b/src/router/index.ts index 6ddb916..1189599 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -169,6 +169,11 @@ const routes: Array = [ name: "seed-backup", component: () => import("../views/SeedBackupView.vue"), }, + { + path: "/shared-photo", + name: "shared-photo", + component: () => import("@/views/SharedPhotoView.vue"), + }, { path: "/start", name: "start", diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index e3b1b17..93d47eb 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -4,16 +4,6 @@
- -
-

- -

-
-

Your Identity @@ -631,6 +621,7 @@ import { AppString, DEFAULT_IMAGE_API_SERVER, DEFAULT_PUSH_SERVER, + IMAGE_TYPE_PROFILE, NotificationIface, } from "@/constants/app"; import { db, accountsDB } from "@/db/index"; @@ -1129,8 +1120,7 @@ export default class AccountViewView extends Vue { console.error("Export Error:", error); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async uploadFile(event: any) { + async uploadFile(event: Event) { inputFileNameRef.value = event.target.files[0]; } @@ -1163,7 +1153,7 @@ export default class AccountViewView extends Vue { async submitFile() { if (inputFileNameRef.value != null) { await db.delete(); - await Dexie.import(inputFileNameRef.value, { + await Dexie.import(inputFileNameRef.value as Blob, { progressCallback: this.progressCallback, }); } @@ -1387,7 +1377,7 @@ export default class AccountViewView extends Vue { //console.log("Got image URL:", imgUrl); }, true, - "profile", + IMAGE_TYPE_PROFILE, ); } diff --git a/src/views/GiftedDetails.vue b/src/views/GiftedDetails.vue index 0f9f085..a37c2e3 100644 --- a/src/views/GiftedDetails.vue +++ b/src/views/GiftedDetails.vue @@ -180,7 +180,24 @@ export default class GiftedDetails extends Vue { } this.unitCode = this.$route.query.unitCode as string; - this.imageUrl = localStorage.getItem("imageUrl") || ""; + this.imageUrl = + (this.$route.query.imageUrl as string) || + localStorage.getItem("imageUrl") || + ""; + + // this is an endpoint for sharing project info to highlight something given + // https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target + if (this.$route.query.shareTitle) { + this.description = this.$route.query.shareTitle as string; + } + if (this.$route.query.shareText) { + this.description = + (this.description ? this.description + " " : "") + + (this.$route.query.shareText as string); + } + if (this.$route.query.shareUrl) { + this.imageUrl = this.$route.query.shareUrl as string; + } try { await db.open(); diff --git a/src/views/HelpView.vue b/src/views/HelpView.vue index 2217011..d8ab1c3 100644 --- a/src/views/HelpView.vue +++ b/src/views/HelpView.vue @@ -100,8 +100,9 @@

How do I backup all my data?

- There are two sets of data to backup: the identifier secrets and the - other data that isn't quite a secret such as settings, contacts, etc. + There are three sets of data to backup: the identifier secrets; + the non-public textual data that isn't quite a secret such as settings and contacts; + the non-public image for yourself; and the data that you have sent to the public.

@@ -122,7 +123,7 @@

- How do I backup my other (non-identifier-secret) data? + How do I backup my non-secret, non-public text data?

  • @@ -134,6 +135,27 @@ won't lose it.
+ +

+ How do I backup my non-secret, non-public image? +

+
    +
  • + Go to Your Identity page, + tap on your image, and save it. +
  • +
+ +

+ How do I backup my public data? +

+
    +
  • + This requires use of the API, so investigate the endpoints + here + (particularly the "claim" endpoints). +
  • +

How do I restore my data?

@@ -178,8 +200,7 @@

How do I erase my data?

- Before doing this, note the two kinds of data to backup: identity data, - and other data for contacts and settings (see instructions above). + Before doing this, you may want to back up your data with the instructions above.

  • @@ -198,11 +219,11 @@
    • Chrome: - Clear at chrome://settings/content/all and + Clear at "chrome://settings/content/all" and also clear under dev tools Application
    • - Firefox: go here, Manage Data, + Firefox: Navigate to "about:preferences", Manage Data, find timesafari.app and select, hit Remove Selected, then Save Changes
    • @@ -232,7 +253,7 @@

      There is a even more functionality in a mobile app (and more documentation) at - + EndorserSearch.com

      @@ -303,7 +324,7 @@ find "timesafari.app", and click "Unregister".
    • - Search + Search for instructions for other browsers.
    Then reload Time Safari. @@ -344,7 +365,7 @@ by disabling notifications on the Account page.
    For all other claim data, - + the Endorser Service has this Privacy Policy.

    @@ -352,7 +373,7 @@

    Where can I read more?

    This is part of the - + Lives of Giving initiative. @@ -362,7 +383,7 @@

    {{ package.version }} ({{ commitHash }})

    - For any other questions, including removing your data: + For any other questions, including removing all your data from the public ledger:

    Contact us at diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue index 6293290..d1abe2c 100644 --- a/src/views/NewEditProjectView.vue +++ b/src/views/NewEditProjectView.vue @@ -227,7 +227,7 @@ export default class NewEditProjectView extends Vue { return headers; } - async created() { + async mounted() { await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); this.activeDid = (settings?.activeDid as string) || ""; diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index 962522c..707bf51 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -505,7 +505,7 @@ export default class ProjectViewView extends Vue { title: "Error", text: "There was a problem getting that project. See logs for more info.", }, - -1, + 5000, ); } } catch (error: unknown) { @@ -519,7 +519,7 @@ export default class ProjectViewView extends Vue { title: "Error", text: "That project does not exist.", }, - -1, + 5000, ); } else { this.$notify( @@ -529,7 +529,7 @@ export default class ProjectViewView extends Vue { title: "Error", text: "Something went wrong retrieving that project. See logs for more info.", }, - -1, + 5000, ); } } @@ -561,7 +561,7 @@ export default class ProjectViewView extends Vue { title: "Error", text: "Failed to retrieve plans fulfilled by this project.", }, - -1, + 5000, ); } } catch (error: unknown) { @@ -573,7 +573,7 @@ export default class ProjectViewView extends Vue { title: "Error", text: "Something went wrong retrieving plans fulfilled by this project.", }, - -1, + 5000, ); console.error( "Error retrieving plans fulfilled by this project:", @@ -616,7 +616,7 @@ export default class ProjectViewView extends Vue { title: "Error", text: "Failed to retrieve more gives to this project.", }, - -1, + 5000, ); } } catch (error: unknown) { @@ -628,7 +628,7 @@ export default class ProjectViewView extends Vue { title: "Error", text: "Something went wrong retrieving more gives to this project.", }, - -1, + 5000, ); console.error( "Something went wrong retrieving more gives to this project:", @@ -671,7 +671,7 @@ export default class ProjectViewView extends Vue { title: "Error", text: "Failed to retrieve more offers to this project.", }, - -1, + 5000, ); } } catch (error: unknown) { @@ -683,7 +683,7 @@ export default class ProjectViewView extends Vue { title: "Error", text: "Something went wrong retrieving more offers to this project.", }, - -1, + 5000, ); console.error( "Something went wrong retrieving more offers to this project:", @@ -727,7 +727,7 @@ export default class ProjectViewView extends Vue { title: "Error", text: "Failed to retrieve more plans that fullfill this project.", }, - -1, + 5000, ); } } catch (error: unknown) { @@ -739,7 +739,7 @@ export default class ProjectViewView extends Vue { title: "Error", text: "Something went wrong retrieving more plans that fulfull this project.", }, - -1, + 5000, ); console.error( "Something went wrong retrieving more plans that fulfill this project:", @@ -928,7 +928,7 @@ export default class ProjectViewView extends Vue { title: "Error", text: message, }, - -1, + 5000, ); } } diff --git a/src/views/SeedBackupView.vue b/src/views/SeedBackupView.vue index b2bd711..0176b81 100644 --- a/src/views/SeedBackupView.vue +++ b/src/views/SeedBackupView.vue @@ -70,12 +70,9 @@ import * as R from "ramda"; import QuickNav from "@/components/QuickNav.vue"; import { NotificationIface } from "@/constants/app"; import { accountsDB, db } from "@/db/index"; +import { Account } from "@/db/tables/accounts"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; -interface Account { - mnemonic: string; -} - @Component({ components: { QuickNav } }) export default class SeedBackupView extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; diff --git a/src/views/SharedPhotoView.vue b/src/views/SharedPhotoView.vue new file mode 100644 index 0000000..48510ab --- /dev/null +++ b/src/views/SharedPhotoView.vue @@ -0,0 +1,164 @@ + + + diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 5e896d5..3e31dc0 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -153,13 +153,66 @@ Notif OFF + +

    +

    Share Image

    + Populates the "shared-photo" view as if they used "share_target". + + + Go to Shared Page + +
diff --git a/sw_scripts/additional-scripts.js b/sw_scripts/additional-scripts.js index 07e5d2d..196e990 100644 --- a/sw_scripts/additional-scripts.js +++ b/sw_scripts/additional-scripts.js @@ -81,7 +81,7 @@ self.addEventListener("push", function (event) { } else { title = payload.title || "Update"; } - // getNotificationCount is injected from safari-notifications.js at build time by the vue.config.js configureWebpack apply plugin + // getNotificationCount is injected from safari-notifications.js at build time by the sw_combine.js script // eslint-disable-next-line no-undef message = await getNotificationCount(); } @@ -131,8 +131,27 @@ self.addEventListener("notificationclick", (event) => { ); }); +// This is invoked when the user chooses this as a share_target, mapped to share-target in the manifest. self.addEventListener("fetch", (event) => { logConsoleAndDb("Service worker got fetch event.", event); + + // Regular requests not related to Web Share Target. + if (event.request.method !== "POST") { + event.respondWith(fetch(event.request)); + return; + } + + // Requests related to Web Share share-target Target. + event.respondWith( + (async () => { + const formData = await event.request.formData(); + const photo = formData.get("photo"); + // savePhoto is injected from safari-notifications.js at build time by the sw_combine.js script + // eslint-disable-next-line no-undef + await savePhoto(photo); + return Response.redirect("/shared-photo", 303); + })(), + ); }); self.addEventListener("error", (event) => { diff --git a/sw_scripts/safari-notifications.js b/sw_scripts/safari-notifications.js index 7803e6f..0103f5b 100644 --- a/sw_scripts/safari-notifications.js +++ b/sw_scripts/safari-notifications.js @@ -566,6 +566,20 @@ async function getNotificationCount() { return result; } +// Store the image blob and go immediate to a page to upload it. +// @param photo - image Blob to store for later retrieval after redirect +async function savePhoto(photo) { + try { + const db = await openIndexedDB("TimeSafari"); + const transaction = db.transaction("temp", "readwrite"); + const store = transaction.objectStore("temp"); + await updateRecord(store, { id: "shared-photo", blob: photo }); + transaction.oncomplete = () => db.close(); + } catch (error) { + console.error("safari-notifications logMessage IndexedDB error", error); + } +} + self.appendDailyLog = appendDailyLog; self.getNotificationCount = getNotificationCount; self.decodeBase64 = decodeBase64; diff --git a/vite.config.mjs b/vite.config.mjs index 4e59d46..3964ac3 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -17,6 +17,27 @@ export default defineConfig({ filename: 'sw_scripts-combined.js', manifest: { name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name, + short_name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name, + // 192x192 and 512x512 are important for Chrome to show that it's installable + "icons":[ + {"src":"./img/icons/android-chrome-192x192.png","sizes":"192x192","type":"image/png"}, + {"src":"./img/icons/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}, + {"src":"./img/icons/android-chrome-maskable-192x192.png","sizes":"192x192","type":"image/png","purpose":"maskable"}, + {"src":"./img/icons/android-chrome-maskable-512x512.png","sizes":"512x512","type":"image/png","purpose":"maskable"} + ], + share_target: { + action: '/share-target', + method: 'POST', + enctype: 'multipart/form-data', + params: { + files: [ + { + name: 'photo', + accept: ['image/jpg', 'image/jpeg', 'image/png'], + }, + ], + }, + }, }, }), ], -- 2.30.2 From 6bcc0023cd2a0bb1fd46ad259b162726891a4eab Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 11 May 2024 07:09:48 -0600 Subject: [PATCH 2/3] style the sharing screen (plus other fixes) --- src/components/GiftedDialog.vue | 2 +- src/components/QuickNav.vue | 10 +++--- src/views/GiftedDetails.vue | 60 ++++++++++++++++++++++----------- src/views/SharedPhotoView.vue | 54 ++++++++++++++++++++--------- 4 files changed, 86 insertions(+), 40 deletions(-) diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index 2ad08b0..9dd8f01 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -15,7 +15,7 @@ class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20" @click="changeUnitCode()" > - {{ libsUtil.UNIT_SHORT[unitCode] }} + {{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
- + @@ -28,7 +28,7 @@ :to="{ name: 'discover' }" class="block text-center py-3 px-1" > - + @@ -44,7 +44,7 @@ :to="{ name: 'projects' }" class="block text-center py-3 px-1" > - + @@ -60,7 +60,7 @@ :to="{ name: 'contacts' }" class="block text-center py-3 px-1" > - + @@ -76,7 +76,7 @@ :to="{ name: 'account' }" class="block text-center py-3 px-1" > - + diff --git a/src/views/GiftedDetails.vue b/src/views/GiftedDetails.vue index a37c2e3..9d53398 100644 --- a/src/views/GiftedDetails.vue +++ b/src/views/GiftedDetails.vue @@ -5,10 +5,13 @@
-
+

@@ -31,7 +34,7 @@ class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20" @click="changeUnitCode()" > - {{ libsUtil.UNIT_SHORT[unitCode] }} + {{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
-
+
- Choose the purpose of this image: -
- -
- -
- +
Choose how to use this image
+
+ + + +
- Shared Image +
+ Shared Image +
-
+

No image found.

@@ -93,7 +112,12 @@ export default class SharedPhotoView extends Vue { if (url) { this.$router.push({ name: "gifted-details", - query: { imageUrl: url }, + query: { + destinationNameAfter: "home", + hideBackButton: true, + imageUrl: url, + recipientDid: this.activeDid, + }, }); } }); -- 2.30.2 From b81c096fe4e2c31555b9c4e16e3df13c4bb0b619 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 11 May 2024 12:30:10 -0600 Subject: [PATCH 3/3] add file-chooser to the profile image selection --- ...{GiftedPhotoDialog.vue => PhotoDialog.vue} | 66 +++++++++----- src/views/AccountViewView.vue | 85 +++++++++++++------ src/views/GiftedDetails.vue | 8 +- 3 files changed, 108 insertions(+), 51 deletions(-) rename src/components/{GiftedPhotoDialog.vue => PhotoDialog.vue} (89%) diff --git a/src/components/GiftedPhotoDialog.vue b/src/components/PhotoDialog.vue similarity index 89% rename from src/components/GiftedPhotoDialog.vue rename to src/components/PhotoDialog.vue index 93c3582..dcec9c9 100644 --- a/src/components/GiftedPhotoDialog.vue +++ b/src/components/PhotoDialog.vue @@ -34,7 +34,7 @@ backgroundColor: '#f8f8f8', margin: 'auto', }" - :img="URL.createObjectURL(blob)" + :img="createBlobURL(blob)" :options="{ viewMode: 1, dragMode: 'crop', @@ -49,7 +49,7 @@
- +
@@ -60,7 +60,10 @@ Upload
-
+
-
-
- ... and those without your image see this (if you let them see your - activity): +
+
+ People without your image see this: +
+ (if you've let them see your activity)
Import - +
- +
{ + (this.$refs.photoDialog as PhotoDialog).open((imgUrl) => { this.imageUrl = imgUrl; }); } -- 2.30.2