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(