From 77ab28f58c740e9b925f5c3c1cb04706a6bf3dc8 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Sun, 14 Jul 2024 13:05:41 -0500 Subject: [PATCH 1/4] Fix large animated GIFs being converted to JPEG (#1535) Resolves #1524 --- src/helpers/gif.ts | 37 ++++++++++++++++++++++++++++++++++++ src/helpers/imageCompress.ts | 16 ++++++++++++---- 2 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 src/helpers/gif.ts diff --git a/src/helpers/gif.ts b/src/helpers/gif.ts new file mode 100644 index 000000000..a539dbe7b --- /dev/null +++ b/src/helpers/gif.ts @@ -0,0 +1,37 @@ +// https://gist.github.com/zakirt/faa4a58cec5a7505b10e3686a226f285 +export function determineIsAnimatedGif(file: File): Promise { + return new Promise((resolve, reject) => { + const HEADER_LEN = 6; + const LOGICAL_SCREEN_DESC_LEN = 7; + const fileReader = new FileReader(); + + fileReader.onload = function () { + const buffer = fileReader.result as ArrayBuffer; + const dv = new DataView(buffer, HEADER_LEN + LOGICAL_SCREEN_DESC_LEN - 3); + let offset = 0; + const globalColorTable = dv.getUint8(0); + let globalColorTableSize = 0; + + if (globalColorTable & 0x80) { + globalColorTableSize = 3 * Math.pow(2, (globalColorTable & 0x7) + 1); + } + + offset = 3 + globalColorTableSize; + const extensionIntroducer = dv.getUint8(offset); + const graphicsControlLabel = dv.getUint8(offset + 1); + let delayTime = 0; + + if (extensionIntroducer & 0x21 && graphicsControlLabel & 0xf9) { + delayTime = dv.getUint16(offset + 4); + } + + resolve(delayTime !== 0); + }; + + fileReader.onerror = function (error) { + reject(error); + }; + + fileReader.readAsArrayBuffer(file); + }); +} diff --git a/src/helpers/imageCompress.ts b/src/helpers/imageCompress.ts index e92f81fe8..96b94885c 100644 --- a/src/helpers/imageCompress.ts +++ b/src/helpers/imageCompress.ts @@ -2,6 +2,8 @@ * Source: https://stackoverflow.com/a/44849182/1319878 */ +import { determineIsAnimatedGif } from "./gif"; + function imgToCanvas( img: HTMLImageElement, rawWidth: number, @@ -26,11 +28,17 @@ export async function reduceFileSize( maxHeight: number, quality = 0.7, ): Promise { + if (file.size <= acceptFileSize) { + return file; + } + + if (file.type === "image/gif" && (await determineIsAnimatedGif(file))) { + // If its animated, our compression code won't work (will only keep the first frame) + // So just bail and hope the Lemmy instance allows the filesize of the upload + return file; + } + return new Promise((resolve) => { - if (file.size <= acceptFileSize) { - resolve(file); - return; - } const img = new Image(); img.addEventListener("error", function () { From 24b5cfb9ab539f4ddc1769bed1523e28195eea54 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:07:36 -0500 Subject: [PATCH 2/4] Fix certain post videos displaying as images (#1538) Resolves #1537 --- src/features/media/gallery/Media.tsx | 13 +++++++-- src/features/post/useIsPostUrlMedia.ts | 4 +-- src/helpers/url.ts | 37 ++++++++++++++++++++++---- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/features/media/gallery/Media.tsx b/src/features/media/gallery/Media.tsx index 0607aff13..d29f45ff0 100644 --- a/src/features/media/gallery/Media.tsx +++ b/src/features/media/gallery/Media.tsx @@ -1,6 +1,6 @@ import { PostView } from "lemmy-js-client"; import { findLoneImage } from "../../../helpers/markdown"; -import { isUrlMedia, isUrlVideo } from "../../../helpers/url"; +import { findUrlMediaType, isUrlVideo } from "../../../helpers/url"; import { PlayerProps } from "../video/Player"; import { ComponentProps, ComponentRef, forwardRef, memo, useMemo } from "react"; import GalleryMedia, { GalleryMediaProps } from "./GalleryMedia"; @@ -41,8 +41,17 @@ export default memo(Media); export function getPostMedia( post: PostView, ): [string] | [string, string] | undefined { - if (post.post.url && isUrlMedia(post.post.url)) { + const urlType = post.post.url && findUrlMediaType(post.post.url); + + if (post.post.url && urlType) { + const thumbnailType = + post.post.thumbnail_url && findUrlMediaType(post.post.thumbnail_url); + if (post.post.thumbnail_url) { + // Sometimes Lemmy will cache the video, sometimes the thumbnail will be a still frame of the video + if (urlType === "video" && thumbnailType === "image") + return [post.post.url]; + return [post.post.thumbnail_url, post.post.url]; } diff --git a/src/features/post/useIsPostUrlMedia.ts b/src/features/post/useIsPostUrlMedia.ts index d42c056cd..ad69debd3 100644 --- a/src/features/post/useIsPostUrlMedia.ts +++ b/src/features/post/useIsPostUrlMedia.ts @@ -2,7 +2,7 @@ import { useCallback } from "react"; import { useAppSelector } from "../../store"; import { PostView } from "lemmy-js-client"; import { isRedgif } from "../media/external/redgifs/helpers"; -import { isUrlMedia } from "../../helpers/url"; +import { findUrlMediaType } from "../../helpers/url"; export default function useIsPostUrlMedia() { const embedExternalMedia = useAppSelector( @@ -19,7 +19,7 @@ export default function useIsPostUrlMedia() { if (isRedgif(url)) return true; } - return isUrlMedia(url); + return !!findUrlMediaType(url); }, [embedExternalMedia], ); diff --git a/src/helpers/url.ts b/src/helpers/url.ts index a569c6685..9f21f9cbb 100644 --- a/src/helpers/url.ts +++ b/src/helpers/url.ts @@ -26,10 +26,35 @@ export function getPathname(url: string): string | undefined { } } +export function parseUrl(url: string): URL | undefined { + try { + return new URL(url); + } catch { + return; + } +} + +export function getPotentialImageProxyPathname( + url: string, +): string | undefined { + const parsedURL = parseUrl(url); + + if (!parsedURL) return; + + if (parsedURL.pathname === "/api/v3/image_proxy") { + const actualImageURL = parsedURL.searchParams.get("url"); + + if (!actualImageURL) return; + return getPathname(actualImageURL); + } + + return parsedURL.pathname; +} + const imageExtensions = ["jpeg", "png", "gif", "jpg", "webp", "jxl"]; export function isUrlImage(url: string): boolean { - const pathname = getPathname(url); + const pathname = getPotentialImageProxyPathname(url); if (!pathname) return false; @@ -41,7 +66,7 @@ export function isUrlImage(url: string): boolean { const animatedImageExtensions = ["gif", "webp", "jxl"]; export function isUrlPotentialAnimatedImage(url: string): boolean { - const pathname = getPathname(url); + const pathname = getPotentialImageProxyPathname(url); if (!pathname) return false; @@ -53,7 +78,7 @@ export function isUrlPotentialAnimatedImage(url: string): boolean { const videoExtensions = ["mp4", "webm", "gifv"]; export function isUrlVideo(url: string): boolean { - const pathname = getPathname(url); + const pathname = getPotentialImageProxyPathname(url); if (!pathname) return false; return videoExtensions.some((extension) => @@ -61,8 +86,10 @@ export function isUrlVideo(url: string): boolean { ); } -export function isUrlMedia(url: string): boolean { - return isUrlImage(url) || isUrlVideo(url); +export function findUrlMediaType(url: string): "video" | "image" | undefined { + if (isUrlImage(url)) return "image"; + + if (isUrlVideo(url)) return "video"; } // https://github.com/miguelmota/is-valid-hostname From 8db79e08b277fd0723dec1522d422442163918b8 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Mon, 22 Jul 2024 12:28:36 -0500 Subject: [PATCH 3/4] Add support for AVIF images (#1540) --- src/helpers/url.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/url.ts b/src/helpers/url.ts index 9f21f9cbb..8bb0486e3 100644 --- a/src/helpers/url.ts +++ b/src/helpers/url.ts @@ -51,7 +51,7 @@ export function getPotentialImageProxyPathname( return parsedURL.pathname; } -const imageExtensions = ["jpeg", "png", "gif", "jpg", "webp", "jxl"]; +const imageExtensions = ["jpeg", "png", "gif", "jpg", "webp", "jxl", "avif"]; export function isUrlImage(url: string): boolean { const pathname = getPotentialImageProxyPathname(url); @@ -63,7 +63,7 @@ export function isUrlImage(url: string): boolean { ); } -const animatedImageExtensions = ["gif", "webp", "jxl"]; +const animatedImageExtensions = ["gif", "webp", "jxl", "avif"]; export function isUrlPotentialAnimatedImage(url: string): boolean { const pathname = getPotentialImageProxyPathname(url); From a56d17aed8a49c1719bb257f41a75cb4239fc0a0 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Tue, 23 Jul 2024 20:43:09 -0500 Subject: [PATCH 4/4] Release 2.13.1 --- android/app/build.gradle | 4 ++-- ios/App/App/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 43b653181..2828340ea 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "app.vger.voyager" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 255 - versionName "2.13.0" + versionCode 256 + versionName "2.13.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index c35160a6d..149f8e3a6 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.13.0 + 2.13.1 CFBundleURLTypes @@ -32,7 +32,7 @@ CFBundleVersion - 255 + 256 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/package.json b/package.json index 511453b2c..9b1a6b64b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "voyager", "description": "A progressive webapp Lemmy client", "private": true, - "version": "2.13.0", + "version": "2.13.1", "type": "module", "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a", "scripts": {