import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { as, Box, Text, config, Button, Menu, Spinner } from 'folds'; import { ImagePack, ImageUsage, PackContent, PackImage, PackImageReader, packMetaEqual, PackMetaReader, } from '../../plugins/custom-emoji'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { SequenceCard } from '../sequence-card'; import { ImageTile, ImageTileEdit, ImageTileUpload } from './ImageTile'; import { SettingTile } from '../setting-tile'; import { UsageSwitcher } from './UsageSwitcher'; import { ImagePackProfile, ImagePackProfileEdit } from './PackMeta'; import * as css from './style.css'; import { useFilePicker } from '../../hooks/useFilePicker'; import { CompactUploadCardRenderer } from '../upload-card'; import { UploadSuccess } from '../../state/upload'; import { getImageInfo, TUploadContent } from '../../utils/matrix'; import { getImageFileUrl, loadImageElement, renameFile } from '../../utils/dom'; import { replaceSpaceWithDash, suffixRename } from '../../utils/common'; import { getFileNameWithoutExt } from '../../utils/mimeTypes'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; export type ImagePackContentProps = { imagePack: ImagePack; canEdit?: boolean; onUpdate?: (packContent: PackContent) => Promise; }; export const ImagePackContent = as<'div', ImagePackContentProps>( ({ imagePack, canEdit, onUpdate, ...props }, ref) => { const useAuthentication = useMediaAuthentication(); const [metaEditing, setMetaEditing] = useState(false); const [savedMeta, setSavedMeta] = useState(); const currentMeta = savedMeta ?? imagePack.meta; const images = useMemo(() => Array.from(imagePack.images.collection.values()), [imagePack]); const [files, setFiles] = useState([]); const [uploadedImages, setUploadedImages] = useState([]); const [imagesEditing, setImagesEditing] = useState>(new Set()); const [savedImages, setSavedImages] = useState>(new Map()); const [deleteImages, setDeleteImages] = useState>(new Set()); const hasImageWithShortcode = useCallback( (shortcode: string): boolean => { const hasInPack = imagePack.images.collection.has(shortcode); if (hasInPack) return true; const hasInUploaded = uploadedImages.find((img) => img.shortcode === shortcode) !== undefined; if (hasInUploaded) return true; const hasInSaved = Array.from(savedImages).find(([, img]) => img.shortcode === shortcode) !== undefined; return hasInSaved; }, [imagePack, savedImages, uploadedImages] ); const pickFiles = useFilePicker( useCallback( (pickedFiles: File[]) => { const uniqueFiles = pickedFiles.map((file) => { const fileName = replaceSpaceWithDash(file.name); if (hasImageWithShortcode(fileName)) { const uniqueName = suffixRename(fileName, hasImageWithShortcode); return renameFile(file, uniqueName); } return fileName !== file.name ? renameFile(file, fileName) : file; }); setFiles((f) => [...f, ...uniqueFiles]); }, [hasImageWithShortcode] ), true ); const handleMetaSave = useCallback( (editedMeta: PackMetaReader) => { setMetaEditing(false); setSavedMeta( (m) => new PackMetaReader({ ...imagePack.meta.content, ...m?.content, ...editedMeta.content, }) ); }, [imagePack.meta] ); const handleMetaCancel = () => setMetaEditing(false); const handlePackUsageChange = useCallback( (usg: ImageUsage[]) => { setSavedMeta( (m) => new PackMetaReader({ ...imagePack.meta.content, ...m?.content, usage: usg, }) ); }, [imagePack.meta] ); const handleUploadRemove = useCallback((file: TUploadContent) => { setFiles((fs) => fs.filter((f) => f !== file)); }, []); const handleUploadComplete = useCallback( async (data: UploadSuccess) => { const imgEl = await loadImageElement(getImageFileUrl(data.file)); const packImage: PackImage = { url: data.mxc, info: getImageInfo(imgEl, data.file), }; const image = PackImageReader.fromPackImage( getFileNameWithoutExt(data.file.name), packImage ); if (!image) return; handleUploadRemove(data.file); setUploadedImages((imgs) => [image, ...imgs]); }, [handleUploadRemove] ); const handleImageEdit = (shortcode: string) => { setImagesEditing((shortcodes) => { const shortcodeSet = new Set(shortcodes); shortcodeSet.add(shortcode); return shortcodeSet; }); }; const handleDeleteToggle = (shortcode: string) => { setDeleteImages((shortcodes) => { const shortcodeSet = new Set(shortcodes); if (shortcodeSet.has(shortcode)) shortcodeSet.delete(shortcode); else shortcodeSet.add(shortcode); return shortcodeSet; }); }; const handleImageEditCancel = (shortcode: string) => { setImagesEditing((shortcodes) => { const shortcodeSet = new Set(shortcodes); shortcodeSet.delete(shortcode); return shortcodeSet; }); }; const handleImageEditSave = (shortcode: string, image: PackImageReader) => { handleImageEditCancel(shortcode); const saveImage = shortcode !== image.shortcode && hasImageWithShortcode(image.shortcode) ? new PackImageReader( suffixRename(image.shortcode, hasImageWithShortcode), image.url, image.content ) : image; setSavedImages((sImgs) => { const imgs = new Map(sImgs); imgs.set(shortcode, saveImage); return imgs; }); }; const handleResetSavedChanges = () => { setSavedMeta(undefined); setFiles([]); setUploadedImages([]); setSavedImages(new Map()); setDeleteImages(new Set()); }; const [applyState, applyChanges] = useAsyncCallback( useCallback(async () => { const pack: PackContent = { pack: savedMeta?.content ?? imagePack.meta.content, images: {}, }; const pushImage = (img: PackImageReader) => { if (deleteImages.has(img.shortcode)) return; if (!pack.images) return; const imgToPush = savedImages.get(img.shortcode) ?? img; pack.images[imgToPush.shortcode] = imgToPush.content; }; uploadedImages.forEach((img) => pushImage(img)); images.forEach((img) => pushImage(img)); return onUpdate?.(pack); }, [imagePack, images, savedMeta, uploadedImages, savedImages, deleteImages, onUpdate]) ); useEffect(() => { if (applyState.status === AsyncStatus.Success) { handleResetSavedChanges(); } }, [applyState]); const savedChanges = (savedMeta && !packMetaEqual(imagePack.meta, savedMeta)) || uploadedImages.length > 0 || savedImages.size > 0 || deleteImages.size > 0; const canApplyChanges = !metaEditing && imagesEditing.size === 0 && files.length === 0; const applying = applyState.status === AsyncStatus.Loading; const renderImage = (image: PackImageReader) => ( {imagesEditing.has(image.shortcode) ? ( ) : ( )} ); return ( {savedChanges && ( {applyState.status === AsyncStatus.Error ? ( Failed to apply changes! Please try again. ) : ( Changes saved! Apply when ready. )} )} Pack {metaEditing ? ( ) : ( setMetaEditing(true)} /> )} } /> {images.length === 0 && !canEdit ? null : ( Images {canEdit && ( pickFiles('image/*')} > Select } /> )} {files.map((file) => ( {(uploadAtom) => ( )} ))} {uploadedImages.map(renderImage)} {images.map(renderImage)} )} ); } );