diff --git a/src/components/GiftedPhotoDialog.vue b/src/components/GiftedPhotoDialog.vue index 20600bf89..93c358213 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 4f51f6bbc..161644e1f 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 290a0012c..6bb59f235 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 000000000..02b592b56 --- /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 6ddb916cb..118959935 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 e3b1b1761..93d47eb30 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 0f9f085b2..a37c2e341 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 2217011f1..d8ab1c342 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 6293290ea..d1abe2cec 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 962522c7a..707bf511e 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 b2bd7113e..0176b81a7 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 000000000..48510ab9f --- /dev/null +++ b/src/views/SharedPhotoView.vue @@ -0,0 +1,164 @@ + + + diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 5e896d5b8..3e31dc076 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 07e5d2d20..196e9909b 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 7803e6f00..0103f5beb 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 4e59d462e..3964ac325 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'], + }, + ], + }, + }, }, }), ],