diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6727711a3a..74f781c7e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,9 +6,11 @@ on: push: branches: - main + - release/* pull_request: branches: - main + - release/* workflow_dispatch: {} jobs: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ecb5d31ecc..cbdb71bdf8 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,11 +3,13 @@ on: push: branches: - main + - release/* tags: - "*" pull_request: branches: - main + - release/* jobs: docker: runs-on: ubuntu-latest diff --git a/android/app/build.gradle b/android/app/build.gradle index 655642f71a..628d3ff6a3 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 266 - versionName "2.18.4" + versionCode 269 + versionName "2.18.5" 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 fa472d58ac..c192d9584f 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.18.4 + 2.18.5 CFBundleURLTypes @@ -32,7 +32,7 @@ CFBundleVersion - 266 + 269 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/package.json b/package.json index 31783a3a70..38bf15a7fb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "voyager", "description": "A progressive webapp Lemmy client", "private": true, - "version": "2.18.4", + "version": "2.18.5", "type": "module", "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e", "scripts": { diff --git a/src/features/media/gallery/GalleryMedia.tsx b/src/features/media/gallery/GalleryMedia.tsx index 92028347ad..d1be21e7eb 100644 --- a/src/features/media/gallery/GalleryMedia.tsx +++ b/src/features/media/gallery/GalleryMedia.tsx @@ -34,8 +34,10 @@ export default forwardRef< ref, ) { const isGif = useMemo( - () => props.src && isUrlPotentialAnimatedImage(props.src), - [props.src], + () => + props.src && + isUrlPotentialAnimatedImage(props.src, post?.post.url_content_type), + [props.src, post], ); const shouldAutoplay = useShouldAutoplay(); diff --git a/src/features/media/gallery/Media.tsx b/src/features/media/gallery/Media.tsx index 5ca4021664..2f6ae34f66 100644 --- a/src/features/media/gallery/Media.tsx +++ b/src/features/media/gallery/Media.tsx @@ -13,7 +13,10 @@ const Media = forwardRef< ComponentRef | ComponentRef, PostGalleryImgProps >(function Media({ nativeControls, src, ...props }, ref) { - const isVideo = useMemo(() => src && isUrlVideo(src), [src]); + const isVideo = useMemo( + () => src && isUrlVideo(src, props.post?.post.url_content_type), + [src, props.post], + ); if (isVideo) return ( diff --git a/src/features/post/inFeed/compact/CompactPost.tsx b/src/features/post/inFeed/compact/CompactPost.tsx index 809caa87cc..90a8595518 100644 --- a/src/features/post/inFeed/compact/CompactPost.tsx +++ b/src/features/post/inFeed/compact/CompactPost.tsx @@ -173,7 +173,7 @@ export default function CompactPost({ post }: PostProps) { const [domain] = useMemo( () => - post.post.url && !isUrlImage(post.post.url) + post.post.url && !isUrlImage(post.post.url, post.post.url_content_type) ? parseUrlForDisplay(post.post.url) : [], [post], diff --git a/src/features/post/inFeed/compact/Thumbnail.tsx b/src/features/post/inFeed/compact/Thumbnail.tsx index 47763c0274..7bbb78c548 100644 --- a/src/features/post/inFeed/compact/Thumbnail.tsx +++ b/src/features/post/inFeed/compact/Thumbnail.tsx @@ -98,10 +98,11 @@ export default function Thumbnail({ post }: ImgProps) { ); const postImageSrc = useMemo(() => { - if (post.post.url && isUrlImage(post.post.url)) return post.post.url; + if (post.post.url && isUrlImage(post.post.url, post.post.url_content_type)) + return post.post.url; if (markdownLoneImage) return markdownLoneImage.url; - }, [markdownLoneImage, post.post.url]); + }, [markdownLoneImage, post.post]); const blurNsfw = useAppSelector( (state) => state.settings.appearance.posts.blurNsfw, diff --git a/src/features/post/inFeed/usePostSrc.ts b/src/features/post/inFeed/usePostSrc.ts index 1a0fc6f9e4..dfcc7eb805 100644 --- a/src/features/post/inFeed/usePostSrc.ts +++ b/src/features/post/inFeed/usePostSrc.ts @@ -4,9 +4,15 @@ import { useAppSelector } from "../../../store"; import { IMAGE_FAILED } from "./large/imageSlice"; import { findUrlMediaType } from "../../../helpers/url"; import { findLoneImage } from "../../../helpers/markdown"; +import useSupported from "../../../helpers/useSupported"; export default function usePostSrc(post: PostView): string | undefined { - const src = useMemo(() => getPostMedia(post), [post]); + const thumbnailIsFullsize = useSupported("Fullsize thumbnails"); + + const src = useMemo( + () => getPostMedia(post, thumbnailIsFullsize), + [post, thumbnailIsFullsize], + ); const primaryFailed = useAppSelector( (state) => src && state.image.loadedBySrc[src[0]] === IMAGE_FAILED, ); @@ -18,24 +24,26 @@ export default function usePostSrc(post: PostView): string | undefined { return src[0]; } -export function getPostMedia( +function getPostMedia( post: PostView, + thumbnailIsFullsize: boolean, ): [string] | [string, string] | undefined { - 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]; + if (post.post.url) { + const isUrlMedia = findUrlMediaType( + post.post.url, + post.post.url_content_type, + ); + + if (isUrlMedia) { + if (post.post.thumbnail_url) { + if (thumbnailIsFullsize) + return [post.post.thumbnail_url, post.post.url]; + } + + // no fallback now for newer lemmy versions + // in the future might unwrap lemmy proxy_image param here + return [post.post.url]; } - - return [post.post.url]; } if (post.post.thumbnail_url) return [post.post.thumbnail_url]; diff --git a/src/features/post/link/Link.tsx b/src/features/post/link/Link.tsx index 135c9c9b82..ca489be70c 100644 --- a/src/features/post/link/Link.tsx +++ b/src/features/post/link/Link.tsx @@ -158,7 +158,7 @@ export default function Link({ () => determineObjectTypeFromUrl(url) ?? determineTypeFromUrl(url), [url, determineObjectTypeFromUrl], ); - const isImage = useMemo(() => isUrlImage(url), [url]); + const isImage = useMemo(() => isUrlImage(url, undefined), [url]); const handleLinkClick = (e: MouseEvent) => { e.stopPropagation(); diff --git a/src/features/post/new/PostEditorRoot.tsx b/src/features/post/new/PostEditorRoot.tsx index f65ca866cd..9af5bfbf60 100644 --- a/src/features/post/new/PostEditorRoot.tsx +++ b/src/features/post/new/PostEditorRoot.tsx @@ -107,7 +107,9 @@ export default function PostEditorRoot({ const existingPost = "existingPost" in props ? props.existingPost : undefined; const isImage = useMemo( - () => existingPost?.post.url && isUrlImage(existingPost.post.url), + () => + existingPost?.post.url && + isUrlImage(existingPost.post.url, existingPost.post.url_content_type), [existingPost], ); diff --git a/src/features/post/useIsPostUrlMedia.ts b/src/features/post/useIsPostUrlMedia.ts index ad69debd32..b67832cff5 100644 --- a/src/features/post/useIsPostUrlMedia.ts +++ b/src/features/post/useIsPostUrlMedia.ts @@ -19,7 +19,7 @@ export default function useIsPostUrlMedia() { if (isRedgif(url)) return true; } - return !!findUrlMediaType(url); + return !!findUrlMediaType(url, post.post.url_content_type); }, [embedExternalMedia], ); diff --git a/src/features/shared/markdown/MarkdownImg.tsx b/src/features/shared/markdown/MarkdownImg.tsx index efa00c2b84..71da142bd1 100644 --- a/src/features/shared/markdown/MarkdownImg.tsx +++ b/src/features/shared/markdown/MarkdownImg.tsx @@ -20,7 +20,7 @@ interface MarkdownImgProps extends GalleryMediaProps { export default function MarkdownImg({ small, ...props }: MarkdownImgProps) { const sharedStyles = small ? smallStyles : undefined; const isVideo = useMemo( - () => props.src && isUrlVideo(props.src), + () => props.src && isUrlVideo(props.src, undefined), [props.src], ); diff --git a/src/helpers/url.ts b/src/helpers/url.ts index 8bb0486e34..965377e7ba 100644 --- a/src/helpers/url.ts +++ b/src/helpers/url.ts @@ -53,7 +53,12 @@ export function getPotentialImageProxyPathname( const imageExtensions = ["jpeg", "png", "gif", "jpg", "webp", "jxl", "avif"]; -export function isUrlImage(url: string): boolean { +export function isUrlImage( + url: string, + contentType: string | undefined, +): boolean { + if (contentType?.startsWith("image/")) return true; + const pathname = getPotentialImageProxyPathname(url); if (!pathname) return false; @@ -63,9 +68,18 @@ export function isUrlImage(url: string): boolean { ); } -const animatedImageExtensions = ["gif", "webp", "jxl", "avif"]; +const animatedImageExtensions = ["gif", "webp", "jxl", "avif", "apng"]; +const animatedImageContentTypes = animatedImageExtensions.map( + (extension) => `image/${extension}`, +); + +export function isUrlPotentialAnimatedImage( + url: string, + contentType: string | undefined, +): boolean { + if (contentType && animatedImageContentTypes.includes(contentType)) + return true; -export function isUrlPotentialAnimatedImage(url: string): boolean { const pathname = getPotentialImageProxyPathname(url); if (!pathname) return false; @@ -77,7 +91,12 @@ export function isUrlPotentialAnimatedImage(url: string): boolean { const videoExtensions = ["mp4", "webm", "gifv"]; -export function isUrlVideo(url: string): boolean { +export function isUrlVideo( + url: string, + contentType: string | undefined, +): boolean { + if (contentType?.startsWith("video/")) return true; + const pathname = getPotentialImageProxyPathname(url); if (!pathname) return false; @@ -86,10 +105,12 @@ export function isUrlVideo(url: string): boolean { ); } -export function findUrlMediaType(url: string): "video" | "image" | undefined { - if (isUrlImage(url)) return "image"; - - if (isUrlVideo(url)) return "video"; +export function findUrlMediaType( + url: string, + contentType: string | undefined, // not available on older lemmy instances <0.19.6? +): "video" | "image" | undefined { + if (isUrlImage(url, contentType)) return "image"; + if (isUrlVideo(url, contentType)) return "video"; } // https://github.com/miguelmota/is-valid-hostname diff --git a/src/helpers/useSupported.ts b/src/helpers/useSupported.ts index 5ebfa2aa6f..3080285431 100644 --- a/src/helpers/useSupported.ts +++ b/src/helpers/useSupported.ts @@ -1,13 +1,18 @@ -import { compare } from "compare-versions"; +import { CompareOperator, compare } from "compare-versions"; +import { CommentSortType, PostSortType } from "lemmy-js-client"; + import { lemmyVersionSelector } from "../features/auth/siteSlice"; import { useAppSelector } from "../store"; -import { CommentSortType, PostSortType } from "lemmy-js-client"; + +const SUPPORTED_ON_OLDER_EXCLUSIVE = ">"; +const SUPPORTED_ON_NEWER_INCLUSIVE = "<="; /** * What Lemmy version was support added? */ const featureVersionSupported = { - // "Instance Blocking": "0.19.0-rc.3", + // https://github.com/LemmyNet/lemmy-ui/issues/2796 + "Fullsize thumbnails": ["0.19.6", SUPPORTED_ON_OLDER_EXCLUSIVE], } as const; type Feature = keyof typeof featureVersionSupported; @@ -17,7 +22,18 @@ export default function useSupported(feature: Feature): boolean { if (!lemmyVersion) return false; - return compare(featureVersionSupported[feature], lemmyVersion, "<="); + const supported = featureVersionSupported[feature]; + + let comparator: CompareOperator = SUPPORTED_ON_NEWER_INCLUSIVE; + let version: string; + if (typeof supported === "string") { + version = supported; + } else { + version = supported[0]; + comparator = supported[1]; + } + + return compare(version, lemmyVersion, comparator); } export function is019Sort(