Show image preview in upload window (#2231)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Wed, 26 Feb 2025 10:43:43 +0000 (21:43 +1100)
committerGitHub <noreply@github.com>
Wed, 26 Feb 2025 10:43:43 +0000 (21:43 +1100)
* memoize metadata callback properly

* add image preview on upload

* show spoiler image button inside image preview

src/app/components/upload-card/UploadCardRenderer.tsx
src/app/features/room/RoomInput.tsx

index 4cc8a00ccf6827e304e7a53f4a2fbdc479b8d113..4383e2044d50889b62ed31d2225b9d116fe00165 100644 (file)
@@ -1,5 +1,5 @@
-import React, { useCallback, useEffect } from 'react';
-import { Chip, Icon, IconButton, Icons, Text, Tooltip, TooltipProvider, color } from 'folds';
+import React, { useEffect } from 'react';
+import { Box, Chip, Icon, IconButton, Icons, Text, color, config, toRem } from 'folds';
 import { UploadCard, UploadCardError, UploadCardProgress } from './UploadCard';
 import { UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
 import { useMatrixClient } from '../../hooks/useMatrixClient';
@@ -10,11 +10,60 @@ import {
   TUploadItem,
   TUploadMetadata,
 } from '../../state/room/roomInputDrafts';
+import { useObjectURL } from '../../hooks/useObjectURL';
+
+type ImagePreviewProps = { fileItem: TUploadItem; onSpoiler: (marked: boolean) => void };
+function ImagePreview({ fileItem, onSpoiler }: ImagePreviewProps) {
+  const { originalFile, metadata } = fileItem;
+  const fileUrl = useObjectURL(originalFile);
+
+  return fileUrl ? (
+    <Box
+      style={{
+        borderRadius: config.radii.R300,
+        overflow: 'hidden',
+        backgroundColor: 'black',
+        position: 'relative',
+      }}
+    >
+      <img
+        style={{
+          objectFit: 'contain',
+          width: '100%',
+          height: toRem(152),
+          filter: fileItem.metadata.markedAsSpoiler ? 'blur(44px)' : undefined,
+        }}
+        src={fileUrl}
+        alt={originalFile.name}
+      />
+      <Box
+        justifyContent="End"
+        style={{
+          position: 'absolute',
+          bottom: config.space.S100,
+          left: config.space.S100,
+          right: config.space.S100,
+        }}
+      >
+        <Chip
+          variant={metadata.markedAsSpoiler ? 'Warning' : 'Secondary'}
+          fill="Soft"
+          radii="Pill"
+          aria-pressed={metadata.markedAsSpoiler}
+          before={<Icon src={Icons.EyeBlind} size="50" />}
+          onClick={() => onSpoiler(!metadata.markedAsSpoiler)}
+        >
+          <Text size="B300">Spoiler</Text>
+        </Chip>
+      </Box>
+    </Box>
+  ) : null;
+}
 
 type UploadCardRendererProps = {
   isEncrypted?: boolean;
   fileItem: TUploadItem;
-  setMetadata: (metadata: TUploadMetadata) => void;
+  setMetadata: (fileItem: TUploadItem, metadata: TUploadMetadata) => void;
   onRemove: (file: TUploadContent) => void;
   onComplete?: (upload: UploadSuccess) => void;
 };
@@ -33,9 +82,9 @@ export function UploadCardRenderer({
 
   if (upload.status === UploadStatus.Idle) startUpload();
 
-  const toggleSpoiler = useCallback(() => {
-    setMetadata({ ...metadata, markedAsSpoiler: !metadata.markedAsSpoiler });
-  }, [setMetadata, metadata]);
+  const handleSpoiler = (marked: boolean) => {
+    setMetadata(fileItem, { ...metadata, markedAsSpoiler: marked });
+  };
 
   const removeUpload = () => {
     cancelUpload();
@@ -66,31 +115,6 @@ export function UploadCardRenderer({
               <Text size="B300">Retry</Text>
             </Chip>
           )}
-          {file.type.startsWith('image') && (
-            <TooltipProvider
-              tooltip={
-                <Tooltip variant="SurfaceVariant">
-                  <Text>Mark as Spoiler</Text>
-                </Tooltip>
-              }
-              position="Top"
-              align="Center"
-            >
-              {(triggerRef) => (
-                <IconButton
-                  ref={triggerRef}
-                  onClick={toggleSpoiler}
-                  aria-label="Mark as Spoiler"
-                  variant="SurfaceVariant"
-                  radii="Pill"
-                  size="300"
-                  aria-pressed={metadata.markedAsSpoiler}
-                >
-                  <Icon src={Icons.EyeBlind} size="200" />
-                </IconButton>
-              )}
-            </TooltipProvider>
-          )}
           <IconButton
             onClick={removeUpload}
             aria-label="Cancel Upload"
@@ -104,6 +128,9 @@ export function UploadCardRenderer({
       }
       bottom={
         <>
+          {fileItem.originalFile.type.startsWith('image') && (
+            <ImagePreview fileItem={fileItem} onSpoiler={handleSpoiler} />
+          )}
           {upload.status === UploadStatus.Idle && (
             <UploadCardProgress sentBytes={0} totalBytes={file.size} />
           )}
index 3141a5d5d048ab7e2b08e9fbe6fd0d0cd01a69a3..faaba90c22c8e9517e7a417639c2dbbb4ed69183 100644 (file)
@@ -70,6 +70,7 @@ import { useFilePasteHandler } from '../../hooks/useFilePasteHandler';
 import { useFileDropZone } from '../../hooks/useFileDrop';
 import {
   TUploadItem,
+  TUploadMetadata,
   roomIdToMsgDraftAtomFamily,
   roomIdToReplyDraftAtomFamily,
   roomIdToUploadItemsAtomFamily,
@@ -220,6 +221,17 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
       [roomId, editor, setMsgDraft]
     );
 
+    const handleFileMetadata = useCallback(
+      (fileItem: TUploadItem, metadata: TUploadMetadata) => {
+        setSelectedFiles({
+          type: 'REPLACE',
+          item: fileItem,
+          replacement: { ...fileItem, metadata },
+        });
+      },
+      [setSelectedFiles]
+    );
+
     const handleRemoveUpload = useCallback(
       (upload: TUploadContent | TUploadContent[]) => {
         const uploads = Array.isArray(upload) ? upload : [upload];
@@ -433,13 +445,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
                         key={index}
                         isEncrypted={!!fileItem.encInfo}
                         fileItem={fileItem}
-                        setMetadata={(metadata) =>
-                          setSelectedFiles({
-                            type: 'REPLACE',
-                            item: fileItem,
-                            replacement: { ...fileItem, metadata },
-                          })
-                        }
+                        setMetadata={handleFileMetadata}
                         onRemove={handleRemoveUpload}
                       />
                     ))}