forked from trent_larson/crowd-funder-for-time-pwa
Complete SharedPhotoView.vue Enhanced Triple Migration Pattern (6 minutes)
Database Migration: Replace databaseUtil + PlatformServiceFactory with PlatformServiceMixin SQL Abstraction: Use $first<Temp>(), $dbQuery(), $dbExec(), $accountSettings(), $updateSettings() Notification Migration: Add 2 constants, migrate 3 $notify calls to helper methods Documentation: Add comprehensive file and method-level documentation Time: 6 minutes | Complexity: Medium | Issues: Linting fixed Testing: Manual required | Validation: Full Enhanced Triple Migration Pattern Component Features: - External image sharing from device apps via deep linking - Image upload to server with JWT authentication - Convert images to gifts (GiveAction) or profile pictures - Temporary storage management and cleanup - Integration with PhotoDialog for profile image cropping - Comprehensive error handling for upload scenarios Technical Updates: - Type-safe database operations with proper result mapping - Centralized notification constants for consistency - Enhanced error handling with detailed logging - Clean separation of concerns between data access and UI
This commit is contained in:
@@ -197,6 +197,17 @@ export const NOTIFY_CAMERA_SHARE_METHOD = {
|
|||||||
noText: "we will share another way",
|
noText: "we will share another way",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SharedPhotoView.vue constants
|
||||||
|
export const NOTIFY_SHARED_PHOTO_LOAD_ERROR = {
|
||||||
|
title: "Error",
|
||||||
|
message: "Got an error loading this data.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NOTIFY_SHARED_PHOTO_SAVE_ERROR = {
|
||||||
|
title: "Error",
|
||||||
|
message: "There was a problem saving the picture.",
|
||||||
|
};
|
||||||
|
|
||||||
// OfferDialog.vue constants
|
// OfferDialog.vue constants
|
||||||
export const NOTIFY_OFFER_SETTINGS_ERROR = {
|
export const NOTIFY_OFFER_SETTINGS_ERROR = {
|
||||||
title: "Error",
|
title: "Error",
|
||||||
|
|||||||
@@ -1,3 +1,40 @@
|
|||||||
|
<!--
|
||||||
|
SharedPhotoView.vue - External Image Sharing Handler
|
||||||
|
|
||||||
|
Handles images shared to TimeSafari from external applications via deep linking.
|
||||||
|
Provides options to use shared images for gifts or profile pictures, with
|
||||||
|
integrated image upload and processing capabilities.
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Process externally shared images from device
|
||||||
|
- Upload images to image server with authentication
|
||||||
|
- Convert images to gifts (GiveAction) or profile pictures
|
||||||
|
- Temporary storage management for shared image data
|
||||||
|
- Comprehensive error handling for upload failures
|
||||||
|
- Integration with PhotoDialog for profile image cropping
|
||||||
|
|
||||||
|
Image Flow:
|
||||||
|
1. External app shares image → TimeSafari deep link
|
||||||
|
2. Image stored temporarily in database as base64
|
||||||
|
3. User chooses: Record Gift, Save as Profile, or Cancel
|
||||||
|
4. Image uploaded to server with JWT authentication
|
||||||
|
5. Temporary storage cleaned up
|
||||||
|
|
||||||
|
Navigation Paths:
|
||||||
|
- External Share → SharedPhotoView
|
||||||
|
- Record Gift → GiftedDetailsView (with image URL)
|
||||||
|
- Save Profile → PhotoDialog → AccountView
|
||||||
|
- Cancel → HomeView
|
||||||
|
|
||||||
|
Migration Status: ✅ Complete Enhanced Triple Migration Pattern
|
||||||
|
- Phase 1: Database Migration (PlatformServiceMixin) ✅
|
||||||
|
- Phase 2: SQL Abstraction ($dbQuery, $dbExec, $accountSettings, $updateSettings) ✅
|
||||||
|
- Phase 3: Notification Migration (3 constants, helper methods) ✅
|
||||||
|
- Phase 4: Template Streamlining (Simple template) ✅
|
||||||
|
|
||||||
|
Author: Matthew Raymer
|
||||||
|
Last Updated: 2025-07-07
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<QuickNav />
|
<QuickNav />
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
@@ -79,43 +116,77 @@ import {
|
|||||||
IMAGE_TYPE_PROFILE,
|
IMAGE_TYPE_PROFILE,
|
||||||
NotificationIface,
|
NotificationIface,
|
||||||
} from "../constants/app";
|
} from "../constants/app";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
import { accessToken } from "../libs/crypto";
|
import { accessToken } from "../libs/crypto";
|
||||||
import { base64ToBlob, SHARED_PHOTO_BASE64_KEY } from "../libs/util";
|
import { base64ToBlob, SHARED_PHOTO_BASE64_KEY } from "../libs/util";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
||||||
import { Temp } from "@/db/tables/temp";
|
import { Temp } from "@/db/tables/temp";
|
||||||
@Component({ components: { PhotoDialog, QuickNav } })
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
|
import {
|
||||||
|
NOTIFY_SHARED_PHOTO_LOAD_ERROR,
|
||||||
|
NOTIFY_SHARED_PHOTO_SAVE_ERROR,
|
||||||
|
} from "@/constants/notifications";
|
||||||
|
@Component({
|
||||||
|
components: { PhotoDialog, QuickNav },
|
||||||
|
mixins: [PlatformServiceMixin],
|
||||||
|
})
|
||||||
export default class SharedPhotoView extends Vue {
|
export default class SharedPhotoView extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
$router!: Router;
|
$router!: Router;
|
||||||
$route!: RouteLocationNormalizedLoaded;
|
$route!: RouteLocationNormalizedLoaded;
|
||||||
|
|
||||||
|
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active user's DID for authentication and image ownership
|
||||||
|
*/
|
||||||
activeDid: string | undefined = undefined;
|
activeDid: string | undefined = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blob data of the shared image for processing and upload
|
||||||
|
*/
|
||||||
imageBlob: Blob | undefined = undefined;
|
imageBlob: Blob | undefined = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Original filename of the shared image from external app
|
||||||
|
*/
|
||||||
imageFileName: string | undefined = undefined;
|
imageFileName: string | undefined = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload status indicator for UI feedback
|
||||||
|
*/
|
||||||
uploading = false;
|
uploading = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browser URL API for creating object URLs from blobs
|
||||||
|
*/
|
||||||
URL = window.URL || window.webkitURL;
|
URL = window.URL || window.webkitURL;
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
/**
|
||||||
|
* Vue lifecycle hook - Initialize shared image processing
|
||||||
|
*
|
||||||
|
* Loads the shared image data from temporary storage, retrieves user settings,
|
||||||
|
* and prepares the component for image processing. Cleans up temporary storage
|
||||||
|
* after successful image loading.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
*/
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.notify = createNotifyHelpers(this.$notify);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
const settings = await this.$accountSettings();
|
||||||
this.activeDid = settings.activeDid;
|
this.activeDid = settings.activeDid;
|
||||||
|
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const temp = await this.$first<Temp>("SELECT * FROM temp WHERE id = ?", [
|
||||||
const tempQuery = await platformService.dbQuery(
|
SHARED_PHOTO_BASE64_KEY,
|
||||||
"SELECT * FROM temp WHERE id = ?",
|
]);
|
||||||
[SHARED_PHOTO_BASE64_KEY],
|
|
||||||
);
|
|
||||||
const temp = databaseUtil.mapQueryResultToValues(tempQuery)?.[0] as Temp;
|
|
||||||
const imageB64 = temp?.blobB64 as string;
|
const imageB64 = temp?.blobB64 as string;
|
||||||
if (temp) {
|
if (temp) {
|
||||||
this.imageBlob = base64ToBlob(imageB64);
|
this.imageBlob = base64ToBlob(imageB64);
|
||||||
|
|
||||||
// clear the temp image
|
// clear the temp image
|
||||||
await platformService.dbExec("DELETE FROM temp WHERE id = ?", [
|
await this.$dbExec("DELETE FROM temp WHERE id = ?", [
|
||||||
SHARED_PHOTO_BASE64_KEY,
|
SHARED_PHOTO_BASE64_KEY,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -125,18 +196,21 @@ export default class SharedPhotoView extends Vue {
|
|||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
logger.error("Got an error loading an identifier:", err);
|
logger.error("Got an error loading an identifier:", err);
|
||||||
this.$notify(
|
this.notify.error(
|
||||||
{
|
NOTIFY_SHARED_PHOTO_LOAD_ERROR.message,
|
||||||
group: "alert",
|
TIMEOUTS.STANDARD,
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "Got an error loading this data.",
|
|
||||||
},
|
|
||||||
3000,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process shared image as a gift record
|
||||||
|
*
|
||||||
|
* Uploads the shared image to the server as a GiveAction and navigates
|
||||||
|
* to the gift details view with the uploaded image URL for gift recording.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
*/
|
||||||
async recordGift() {
|
async recordGift() {
|
||||||
await this.sendToImageServer("GiveAction").then((url) => {
|
await this.sendToImageServer("GiveAction").then((url) => {
|
||||||
if (url) {
|
if (url) {
|
||||||
@@ -155,10 +229,17 @@ export default class SharedPhotoView extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process shared image as profile picture
|
||||||
|
*
|
||||||
|
* Opens the PhotoDialog component for image cropping and profile picture
|
||||||
|
* processing. Updates user settings with the new profile image URL and
|
||||||
|
* navigates to the account view upon completion.
|
||||||
|
*/
|
||||||
recordProfile() {
|
recordProfile() {
|
||||||
(this.$refs.photoDialog as PhotoDialog).open(
|
(this.$refs.photoDialog as PhotoDialog).open(
|
||||||
async (imgUrl) => {
|
async (imgUrl) => {
|
||||||
databaseUtil.updateDefaultSettings({ profileImageUrl: imgUrl });
|
await this.$updateSettings({ profileImageUrl: imgUrl });
|
||||||
this.$router.push({ name: "account" });
|
this.$router.push({ name: "account" });
|
||||||
},
|
},
|
||||||
IMAGE_TYPE_PROFILE,
|
IMAGE_TYPE_PROFILE,
|
||||||
@@ -168,12 +249,31 @@ export default class SharedPhotoView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel image processing and return to home
|
||||||
|
*
|
||||||
|
* Clears the shared image data and navigates back to the home view,
|
||||||
|
* effectively canceling the image sharing operation.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
*/
|
||||||
async cancel() {
|
async cancel() {
|
||||||
this.imageBlob = undefined;
|
this.imageBlob = undefined;
|
||||||
this.imageFileName = undefined;
|
this.imageFileName = undefined;
|
||||||
this.$router.push({ name: "home" });
|
this.$router.push({ name: "home" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload image to the image server with comprehensive error handling
|
||||||
|
*
|
||||||
|
* Sends the shared image to the configured image server with JWT authentication.
|
||||||
|
* Provides detailed error handling for various failure scenarios including
|
||||||
|
* authentication, file size, format, and server errors.
|
||||||
|
*
|
||||||
|
* @param imageType - The type of image claim (e.g., "GiveAction")
|
||||||
|
* @returns Promise<string | undefined> - The uploaded image URL or undefined on failure
|
||||||
|
* @async
|
||||||
|
*/
|
||||||
async sendToImageServer(imageType: string) {
|
async sendToImageServer(imageType: string) {
|
||||||
this.uploading = true;
|
this.uploading = true;
|
||||||
|
|
||||||
@@ -212,17 +312,11 @@ export default class SharedPhotoView extends Vue {
|
|||||||
result = response.data.url as string;
|
result = response.data.url as string;
|
||||||
} else {
|
} else {
|
||||||
logger.error("Problem uploading the image", response.data);
|
logger.error("Problem uploading the image", response.data);
|
||||||
this.$notify(
|
const errorMessage =
|
||||||
{
|
NOTIFY_SHARED_PHOTO_SAVE_ERROR.message +
|
||||||
group: "alert",
|
" " +
|
||||||
type: "danger",
|
(response?.data?.message || "");
|
||||||
title: "Error",
|
this.notify.error(errorMessage, TIMEOUTS.LONG);
|
||||||
text:
|
|
||||||
"There was a problem saving the picture. " +
|
|
||||||
(response?.data?.message || ""),
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
@@ -278,15 +372,7 @@ export default class SharedPhotoView extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$notify(
|
this.notify.error(errorMessage, TIMEOUTS.LONG);
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: errorMessage,
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
Reference in New Issue
Block a user