From 4911d8933b8f200b775e35dca552813b73885569 Mon Sep 17 00:00:00 2001 From: Scott Benton Date: Tue, 31 Oct 2023 21:36:14 -0400 Subject: [PATCH] feat(portraits): Character portraits now get deleted when changed or when the character gets deleted --- src/api-calls/character/deleteCharacter.ts | 25 ++++++++++-- .../character/removeCharacterPortrait.ts | 38 +++++++++++++++++++ .../character/updateCharacterPortrait.ts | 17 ++++++++- .../PortraitUploaderDialog.tsx | 7 ++-- src/lib/storage.lib.ts | 22 ++++++++++- src/stores/character/character.slice.ts | 3 ++ .../currentCharacter.slice.ts | 3 ++ 7 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 src/api-calls/character/removeCharacterPortrait.ts diff --git a/src/api-calls/character/deleteCharacter.ts b/src/api-calls/character/deleteCharacter.ts index 2ae7a8c1..81924980 100644 --- a/src/api-calls/character/deleteCharacter.ts +++ b/src/api-calls/character/deleteCharacter.ts @@ -8,17 +8,27 @@ import { } from "../assets/_getRef"; import { deleteNotes } from "api-calls/notes/deleteNotes"; import { getCharacterSettingsDoc } from "api-calls/custom-move-oracle-settings/_getRef"; -import { getCharacterDoc } from "./_getRef"; +import { + constructCharacterPortraitFolderPath, + getCharacterDoc, +} from "./_getRef"; import { deleteAllLogs } from "api-calls/game-log/deleteAllLogs"; import { deleteAllProgressTracks } from "api-calls/tracks/deleteAllProgressTracks"; import { deleteAllAssets } from "api-calls/assets/deleteAllAssets"; +import { removeCharacterPortrait } from "./removeCharacterPortrait"; +import { deleteImage } from "lib/storage.lib"; export const deleteCharacter = createApiFunction< - { characterId: string; campaignId?: string }, + { + uid: string; + characterId: string; + campaignId?: string; + portraitFilename?: string; + }, void >((params) => { return new Promise(async (resolve, reject) => { - const { characterId, campaignId } = params; + const { uid, characterId, campaignId, portraitFilename } = params; try { if (campaignId) { @@ -30,6 +40,15 @@ export const deleteCharacter = createApiFunction< } const promises: Promise[] = []; + if (portraitFilename) { + promises.push( + deleteImage( + constructCharacterPortraitFolderPath(uid, characterId), + portraitFilename + ) + ); + } + promises.push(deleteNotes({ characterId })); promises.push(deleteDoc(getCharacterSettingsDoc(characterId))); promises.push(deleteAllAssets({ characterId })); diff --git a/src/api-calls/character/removeCharacterPortrait.ts b/src/api-calls/character/removeCharacterPortrait.ts new file mode 100644 index 00000000..bd86269e --- /dev/null +++ b/src/api-calls/character/removeCharacterPortrait.ts @@ -0,0 +1,38 @@ +import { createApiFunction } from "api-calls/createApiFunction"; +import { deleteImage } from "lib/storage.lib"; +import { constructCharacterPortraitFolderPath } from "./_getRef"; +import { updateCharacter } from "./updateCharacter"; +import { FieldValue, deleteField } from "firebase/firestore"; + +export const removeCharacterPortrait = createApiFunction< + { + uid: string; + characterId: string; + oldPortraitFilename: string; + }, + void +>((params) => { + const { uid, characterId, oldPortraitFilename } = params; + + return new Promise((resolve, reject) => { + updateCharacter({ + characterId, + character: { + profileImage: deleteField() as unknown as undefined, + }, + }) + .then(() => { + deleteImage( + constructCharacterPortraitFolderPath(uid, characterId), + oldPortraitFilename + ) + .then(() => { + resolve(); + }) + .catch((e) => { + reject(e); + }); + }) + .catch((e) => reject(e)); + }); +}, "Failed to remove old character portrait"); diff --git a/src/api-calls/character/updateCharacterPortrait.ts b/src/api-calls/character/updateCharacterPortrait.ts index 91a038bb..8ef0c70b 100644 --- a/src/api-calls/character/updateCharacterPortrait.ts +++ b/src/api-calls/character/updateCharacterPortrait.ts @@ -5,20 +5,35 @@ import { getCharacterDoc, } from "./_getRef"; import { createApiFunction } from "api-calls/createApiFunction"; +import { removeCharacterPortrait } from "./removeCharacterPortrait"; export const updateCharacterPortrait = createApiFunction< { uid: string; characterId: string; + oldPortraitFilename?: string; portrait?: File; scale: number; position: { x: number; y: number }; }, void >((params) => { - const { uid, characterId, portrait, scale, position } = params; + const { uid, characterId, oldPortraitFilename, portrait, scale, position } = + params; return new Promise(async (resolve, reject) => { + try { + if (oldPortraitFilename) { + await removeCharacterPortrait({ + uid, + characterId, + oldPortraitFilename, + }); + } + } catch (e) { + reject(e); + return; + } try { if (portrait) { await uploadImage( diff --git a/src/components/features/characters/PortraitUploaderDialog/PortraitUploaderDialog.tsx b/src/components/features/characters/PortraitUploaderDialog/PortraitUploaderDialog.tsx index 1172e1bd..5b0eef45 100644 --- a/src/components/features/characters/PortraitUploaderDialog/PortraitUploaderDialog.tsx +++ b/src/components/features/characters/PortraitUploaderDialog/PortraitUploaderDialog.tsx @@ -17,6 +17,7 @@ import { useSnackbar } from "providers/SnackbarProvider/useSnackbar"; import { PortraitAvatarDisplay } from "components/features/characters/PortraitAvatar/PortraitAvatarDisplay"; import ZoomInIcon from "@mui/icons-material/ZoomIn"; import ZoomOutIcon from "@mui/icons-material/ZoomOut"; +import { LoadingButton } from "@mui/lab"; export interface PortraitUploaderDialogProps { open: boolean; @@ -155,13 +156,13 @@ export function PortraitUploaderDialog(props: PortraitUploaderDialogProps) { > Cancel - + ); diff --git a/src/lib/storage.lib.ts b/src/lib/storage.lib.ts index a9f00298..865be575 100644 --- a/src/lib/storage.lib.ts +++ b/src/lib/storage.lib.ts @@ -1,5 +1,10 @@ import { storage } from "config/firebase.config"; -import { getDownloadURL, ref, uploadBytes } from "firebase/storage"; +import { + getDownloadURL, + ref, + uploadBytes, + deleteObject, +} from "firebase/storage"; export const MAX_FILE_SIZE = 5 * 1024 * 1024; export const MAX_FILE_SIZE_LABEL = "5 MB"; @@ -19,6 +24,21 @@ export function uploadImage(path: string, image: File): Promise { }); } +export function deleteImage(path: string, filename: string): Promise { + return new Promise((resolve, reject) => { + const imageRef = ref(storage, `${path}/${filename}`); + + deleteObject(imageRef) + .then(() => { + resolve(); + }) + .catch((e) => { + console.error(e); + reject(`Failed to delete ${filename}.`); + }); + }); +} + export function getImageUrl(path: string): Promise { return new Promise((resolve, reject) => { const imageRef = ref(storage, path); diff --git a/src/stores/character/character.slice.ts b/src/stores/character/character.slice.ts index 2c09ea91..3a22d9fe 100644 --- a/src/stores/character/character.slice.ts +++ b/src/stores/character/character.slice.ts @@ -134,6 +134,7 @@ export const createCharacterSlice: CreateSliceType = ( }); }, deleteCharacter: (characterId) => { + const uid = getState().auth.uid; const character = getState().characters.characterMap[characterId]; if (!character) { return new Promise((res, reject) => @@ -141,8 +142,10 @@ export const createCharacterSlice: CreateSliceType = ( ); } return deleteCharacter({ + uid, characterId, campaignId: character.campaignId, + portraitFilename: character.profileImage?.filename, }); }, }; diff --git a/src/stores/character/currentCharacter/currentCharacter.slice.ts b/src/stores/character/currentCharacter/currentCharacter.slice.ts index 64183e57..ea355084 100644 --- a/src/stores/character/currentCharacter/currentCharacter.slice.ts +++ b/src/stores/character/currentCharacter/currentCharacter.slice.ts @@ -51,6 +51,9 @@ export const createCurrentCharacterSlice: CreateSliceType< return updateCharacterPortrait({ uid, characterId, + oldPortraitFilename: + state.characters.currentCharacter.currentCharacter?.profileImage + ?.filename, portrait, scale, position,