diff --git a/fluxer_app/src/features/app/components/shared/custom_status_display/CustomStatusDisplay.tsx b/fluxer_app/src/features/app/components/shared/custom_status_display/CustomStatusDisplay.tsx
index e9906cd6f..6abfe8610 100644
--- a/fluxer_app/src/features/app/components/shared/custom_status_display/CustomStatusDisplay.tsx
+++ b/fluxer_app/src/features/app/components/shared/custom_status_display/CustomStatusDisplay.tsx
@@ -7,10 +7,10 @@ import {
getEmojiAttribution,
} from '@app/features/emoji/components/emojis/EmojiAttributionSubtext';
import Emoji from '@app/features/emoji/state/Emoji';
+import {buildCustomEmojiURL} from '@app/features/expressions/utils/CustomEmojiImageUrl';
import {getEmojiURL} from '@app/features/expressions/utils/EmojiUtils';
import UnicodeEmojis from '@app/features/expressions/utils/UnicodeEmojis';
import Guilds from '@app/features/guild/state/Guilds';
-import {setUrlQueryParams} from '@app/features/messaging/utils/MessagingUrlUtils';
import {usePresenceCustomStatus} from '@app/features/presence/hooks/usePresenceCustomStatus';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {EmojiTooltipContent} from '@app/features/ui/emoji_tooltip_content/EmojiTooltipContent';
@@ -22,7 +22,6 @@ import {HoverFloatingTooltipTrigger} from '@app/features/ui/tooltip/HoverFloatin
import {Tooltip, type TooltipPosition} from '@app/features/ui/tooltip/Tooltip';
import {useHoverFloatingTooltip} from '@app/features/ui/tooltip/useHoverFloatingTooltip';
import {type CustomStatus, getCustomStatusText, normalizeCustomStatus} from '@app/features/user/state/CustomStatus';
-import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
import {Trans} from '@lingui/react/macro';
import {PencilIcon, SmileyIcon} from '@phosphor-icons/react';
import {clsx} from 'clsx';
@@ -62,14 +61,17 @@ interface ClampedStyle extends React.CSSProperties {
const sanitizeText = (text: string): string => {
return text.replace(/[\r\n]+/g, ' ').trim();
};
+const getStatusEmojiAnimatable = (status: CustomStatus): boolean => {
+ if (!status.emojiId) {
+ return false;
+ }
+ return Emoji.getEmojiById(status.emojiId)?.animated ?? status.emojiAnimated ?? false;
+};
+
const getTooltipEmojiUrl = (status: CustomStatus, animationAllowed: boolean): string | null => {
if (status.emojiId) {
- const emoji = Emoji.getEmojiById(status.emojiId);
- const isAnimated = (emoji?.animated ?? status.emojiAnimated ?? false) && animationAllowed;
- return setUrlQueryParams(AvatarUtils.getEmojiURL({id: status.emojiId, animated: isAnimated}), {
- size: 96,
- quality: 'lossless',
- });
+ const isAnimatable = getStatusEmojiAnimatable(status);
+ return buildCustomEmojiURL({id: status.emojiId, animated: animationAllowed && isAnimatable});
}
if (status.emojiName) {
return getEmojiURL(status.emojiName);
@@ -104,7 +106,12 @@ const StatusEmojiWithTooltip = observer(
return '';
};
const emojiName = getEmojiDisplayName();
- const animationAllowed = useShouldAnimate({kind: 'custom_status_emoji', isHovering: true});
+ const isAnimatable = getStatusEmojiAnimatable(status);
+ const animationAllowed = useShouldAnimate({
+ kind: 'custom_status_emoji',
+ isAnimated: isAnimatable,
+ isHovering: isAnimatable,
+ });
const tooltipEmojiUrl = getTooltipEmojiUrl(status, animationAllowed);
const TriggerComponent = isButton ? 'button' : 'span';
const triggerProps = isButton
@@ -167,24 +174,15 @@ const renderStatusEmoji = (
animationAllowed: boolean = true,
): EmojiRenderResult | null => {
if (status.emojiId) {
- const emoji = Emoji.getEmojiById(status.emojiId);
const altText = `:${status.emojiName}:`;
- const isAnimated = (emoji?.animated ?? status.emojiAnimated ?? false) && animationAllowed;
- const staticUrl = setUrlQueryParams(AvatarUtils.getEmojiURL({id: status.emojiId, animated: false}), {
- size: 96,
- quality: 'lossless',
- });
- const animatedUrl = isAnimated
- ? setUrlQueryParams(AvatarUtils.getEmojiURL({id: status.emojiId, animated: true}), {
- size: 96,
- quality: 'lossless',
- })
- : null;
+ const isAnimatable = getStatusEmojiAnimatable(status);
+ const staticUrl = buildCustomEmojiURL({id: status.emojiId, animated: false});
+ const animatedUrl = isAnimatable ? buildCustomEmojiURL({id: status.emojiId, animated: true}) : null;
if (alwaysAnimate && animatedUrl) {
return {
node: (
-
+ {animationAllowed && (
+
+ )}
),
altText,
@@ -285,9 +285,11 @@ export const CustomStatusDisplay = observer(
checkVertical: maxLines > 1,
measureTextRange: true,
});
+ const emojiAnimatable = normalized ? getStatusEmojiAnimatable(normalized) : false;
const animationAllowed = useShouldAnimate({
kind: 'custom_status_emoji',
- isHovering: Boolean(animateOnParentHover) || Boolean(alwaysAnimate),
+ isAnimated: emojiAnimatable,
+ isHovering: animateOnParentHover || alwaysAnimate,
});
if (!normalized) {
if (showPlaceholder && isEditable && onEdit) {
diff --git a/fluxer_app/src/features/app/hooks/useAnimatedImageUrl.ts b/fluxer_app/src/features/app/hooks/useAnimatedImageUrl.ts
index f344243f9..30a6b6bcd 100644
--- a/fluxer_app/src/features/app/hooks/useAnimatedImageUrl.ts
+++ b/fluxer_app/src/features/app/hooks/useAnimatedImageUrl.ts
@@ -11,6 +11,7 @@ interface UseAnimatedImageUrlOptions {
staticUrl: string | null;
animatedUrl?: string | null;
kind: ShouldAnimateKind;
+ isAnimated?: boolean;
isFocused?: boolean;
}
@@ -23,6 +24,8 @@ interface AnimatedImageUrlState {
type AnimatedImageLoadStatus = 'idle' | 'loading' | 'loaded' | 'failed';
+const NOOP_HOVER_REF: React.RefCallback = () => {};
+
interface AnimatedImageLoadState {
url: string | null;
status: AnimatedImageLoadStatus;
@@ -32,13 +35,21 @@ export function useAnimatedImageUrl({
staticUrl,
animatedUrl: providedAnimatedUrl,
kind,
+ isAnimated,
isFocused,
}: UseAnimatedImageUrlOptions): AnimatedImageUrlState {
const animatedUrl: string | null = providedAnimatedUrl == null ? null : providedAnimatedUrl;
const [hoverRef, isHovering] = useHover();
- const shouldAnimate = useShouldAnimate({kind, isHovering, isFocused});
- const gifAutoPlayEnabled = kind === 'gif' && UserSettings.getGifAutoPlay();
const hasAnimatedUrl = Boolean(animatedUrl && animatedUrl !== staticUrl);
+ const isAnimatable = isAnimated ?? hasAnimatedUrl;
+ const shouldAnimate = useShouldAnimate({
+ kind,
+ isAnimated: isAnimatable,
+ isHovering: isAnimatable && isHovering,
+ isFocused: isAnimatable && isFocused,
+ });
+ const autoplaysWithoutInteraction = useShouldAnimate({kind, isAnimated: isAnimatable});
+ const gifAutoPlayEnabled = kind === 'gif' && UserSettings.getGifAutoPlay();
const initialAnimatedLoadState: AnimatedImageLoadState = {
url: animatedUrl,
status: animatedUrl != null && ImageCacheUtils.hasImage(animatedUrl) ? 'loaded' : 'idle',
@@ -104,7 +115,9 @@ export function useAnimatedImageUrl({
animatedUrl != null &&
animatedLoadState.url === animatedUrl &&
animatedLoadState.status === 'loaded';
- const imageUrl = animatedSourceReady ? animatedUrl : staticUrl;
- const showGifIndicator = kind === 'gif' && hasAnimatedUrl && !gifAutoPlayEnabled && !shouldAnimate;
- return {hoverRef, imageUrl, shouldAnimate, showGifIndicator};
+ const animatedSourceFailed = animatedLoadState.url === animatedUrl && animatedLoadState.status === 'failed';
+ const skipsStaticFirstPaint = autoplaysWithoutInteraction && !animatedSourceFailed;
+ const imageUrl = animatedUrl != null && (animatedSourceReady || skipsStaticFirstPaint) ? animatedUrl : staticUrl;
+ const showGifIndicator = kind === 'gif' && isAnimatable && !gifAutoPlayEnabled && !shouldAnimate;
+ return {hoverRef: isAnimatable ? hoverRef : NOOP_HOVER_REF, imageUrl, shouldAnimate, showGifIndicator};
}
diff --git a/fluxer_app/src/features/app/hooks/useAnimatedMediaPlayback.ts b/fluxer_app/src/features/app/hooks/useAnimatedMediaPlayback.ts
index 8f4dcb551..8d4b03216 100644
--- a/fluxer_app/src/features/app/hooks/useAnimatedMediaPlayback.ts
+++ b/fluxer_app/src/features/app/hooks/useAnimatedMediaPlayback.ts
@@ -10,6 +10,7 @@ type AnimatedMediaPlaybackListener = () => void;
interface UseAnimatedMediaPlaybackAllowedOptions {
enabled?: boolean;
+ isAnimated?: boolean;
}
const listeners = new Set();
@@ -17,8 +18,9 @@ const listeners = new Set();
let teardownGlobalListeners: (() => void) | null = null;
let lastAllowedSnapshot = true;
-const subscribeAlwaysAllowed = (): (() => void) => () => {};
+const subscribeConstant = (): (() => void) => () => {};
const getAlwaysAllowed = (): boolean => true;
+const getNeverAllowed = (): boolean => false;
export function getAnimatedMediaPlaybackAllowed(root?: HTMLElement): boolean {
if (typeof document === 'undefined') return true;
@@ -66,19 +68,26 @@ export function subscribeAnimatedMediaPlaybackChange(listener: AnimatedMediaPlay
export function useAnimatedMediaPlaybackAllowed({
enabled = true,
+ isAnimated = true,
}: UseAnimatedMediaPlaybackAllowedOptions = {}): boolean {
+ const getConstant = isAnimated ? getAlwaysAllowed : getNeverAllowed;
+ const live = enabled && isAnimated;
return useSyncExternalStore(
- enabled ? subscribeAnimatedMediaPlaybackChange : subscribeAlwaysAllowed,
- enabled ? getAnimatedMediaPlaybackAllowed : getAlwaysAllowed,
- getAlwaysAllowed,
+ live ? subscribeAnimatedMediaPlaybackChange : subscribeConstant,
+ live ? getAnimatedMediaPlaybackAllowed : getConstant,
+ getConstant,
);
}
export function useAnimatedMediaVideoPlayback(
videoRef: RefObject,
- {enabled = true, shouldPlay = true}: {enabled?: boolean; shouldPlay?: boolean} = {},
+ {
+ enabled = true,
+ shouldPlay = true,
+ isAnimated = true,
+ }: {enabled?: boolean; shouldPlay?: boolean; isAnimated?: boolean} = {},
): boolean {
- const playbackAllowed = useAnimatedMediaPlaybackAllowed();
+ const playbackAllowed = useAnimatedMediaPlaybackAllowed({isAnimated});
useEffect(() => {
const video = videoRef.current;
if (!video) return;