diff --git a/src/features/post/new/PhotoPreview.tsx b/src/features/post/new/PhotoPreview.tsx index d263e905d3..8fa313398e 100644 --- a/src/features/post/new/PhotoPreview.tsx +++ b/src/features/post/new/PhotoPreview.tsx @@ -1,11 +1,13 @@ import { styled } from "@linaria/react"; import { IonSpinner } from "@ionic/react"; +import { isUrlVideo } from "../../../helpers/url"; +import { useRef } from "react"; const Container = styled.div` position: relative; `; -const Img = styled.img<{ loadingImage: boolean }>` +const Img = styled.video<{ loadingImage: boolean }>` max-width: 100px; max-height: 100px; padding: 1rem; @@ -23,18 +25,36 @@ const OverlaySpinner = styled(IonSpinner)` interface PhotoPreviewProps { src: string; + isVideo?: boolean; loading: boolean; - onClick?: () => void; } export default function PhotoPreview({ src, + isVideo, loading, - onClick, }: PhotoPreviewProps) { + const videoTag = isVideo || isUrlVideo(src); + const ref = useRef(null); + return ( - + { + if (!(e.target instanceof HTMLVideoElement)) return; + + // iOS won't show preview unless the video plays + e.target.pause(); + }} + loadingImage={loading} + /* Just uploaded blob (can't detect type from url), or editing post w/ media lemmy url (can) */ + as={videoTag ? "video" : "img"} + /> {loading && } ); diff --git a/src/features/post/new/PostEditorRoot.tsx b/src/features/post/new/PostEditorRoot.tsx index a45caab6cc..bb4c451289 100644 --- a/src/features/post/new/PostEditorRoot.tsx +++ b/src/features/post/new/PostEditorRoot.tsx @@ -35,10 +35,8 @@ import { useOptimizedIonRouter } from "../../../helpers/useOptimizedIonRouter"; import { isAndroid } from "../../../helpers/device"; import { css } from "@linaria/core"; import AppHeader from "../../shared/AppHeader"; -import { - deletePendingImageUploads, - uploadImage, -} from "../../shared/markdown/editing/uploadImageSlice"; +import { deletePendingImageUploads } from "../../shared/markdown/editing/uploadImageSlice"; +import useUploadImage from "../../shared/markdown/editing/useUploadImage"; const Container = styled.div` position: absolute; @@ -81,7 +79,7 @@ const HiddenInput = styled.input` display: none; `; -type PostType = "photo" | "link" | "text"; +type PostType = "media" | "link" | "text"; const MAX_TITLE_LENGTH = 200; @@ -106,12 +104,14 @@ export default function PostEditorRoot({ const dispatch = useAppDispatch(); + const { uploadImage } = useUploadImage(); + const initialImage = isImage ? existingPost!.post.url : undefined; const initialPostType = (() => { - if (!existingPost) return "photo"; + if (!existingPost) return "media"; - if (initialImage) return "photo"; + if (initialImage) return "media"; if (existingPost.post.url) return "link"; @@ -139,6 +139,7 @@ export default function PostEditorRoot({ const [photoPreviewURL, setPhotoPreviewURL] = useState( initialImage, ); + const [isPreviewVideo, setIsPreviewVideo] = useState(false); const [photoUploading, setPhotoUploading] = useState(false); const router = useOptimizedIonRouter(); @@ -147,7 +148,7 @@ export default function PostEditorRoot({ const showAutofill = !!url && isValidUrl(url) && !title; const showNsfwToggle = !!( - (postType === "photo" && photoPreviewURL) || + (postType === "media" && photoPreviewURL) || (postType === "link" && url) ); @@ -177,6 +178,14 @@ export default function PostEditorRoot({ nsfw, ]); + useEffect(() => { + return () => { + if (!photoPreviewURL) return; + + URL.revokeObjectURL(photoPreviewURL); + }; + }, [photoPreviewURL]); + function canSubmit() { if (!title) return false; @@ -185,7 +194,7 @@ export default function PostEditorRoot({ if (!url) return false; break; - case "photo": + case "media": if (!photoUrl) return false; break; } @@ -209,7 +218,7 @@ export default function PostEditorRoot({ switch (postType) { case "link": return url || undefined; - case "photo": + case "media": return photoUrl || undefined; default: return; @@ -223,8 +232,8 @@ export default function PostEditorRoot({ } else if (postType === "link" && (!url || !validUrl(url))) { errorMessage = "Please add a valid URL to your post (start with https://)."; - } else if (postType === "photo" && !photoUrl) { - errorMessage = "Please add a photo to your post."; + } else if (postType === "media" && !photoUrl) { + errorMessage = "Please add a photo or video to your post."; } else if (!canSubmit()) { errorMessage = "It looks like you're missing some information to submit this post. Please double check."; @@ -302,6 +311,7 @@ export default function PostEditorRoot({ async function receivedImage(image: File) { setPhotoPreviewURL(URL.createObjectURL(image)); + setIsPreviewVideo(image.type.startsWith("video/")); setPhotoUploading(true); let imageUrl; @@ -310,15 +320,8 @@ export default function PostEditorRoot({ if (isAndroid()) await new Promise((resolve) => setTimeout(resolve, 250)); try { - imageUrl = await dispatch(uploadImage(image)); + imageUrl = await uploadImage(image); } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; - - presentToast({ - message: `Problem uploading image: ${message}. Please try again.`, - color: "danger", - fullscreen: true, - }); clearImage(); throw error; @@ -334,6 +337,7 @@ export default function PostEditorRoot({ function clearImage() { setPhotoUrl(""); setPhotoPreviewURL(undefined); + setIsPreviewVideo(false); } async function fetchPostTitle() { @@ -393,7 +397,7 @@ export default function PostEditorRoot({ value={postType} onIonChange={(e) => setPostType(e.target.value as PostType)} > - Photo + Media Link Text @@ -430,23 +434,26 @@ export default function PostEditorRoot({ )} - {postType === "photo" && ( + {postType === "media" && ( <>