mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(app): honour motion settings in composer autocomplete previews (#1912)
This commit is contained in:
1 parent
0670380360
commit
7d02b7959f
5 files changed
+143
-100
No files matched your search
@@ -1,10 +1,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {useAnimatedMediaVideoPlayback} from '@app/features/app/hooks/useAnimatedMediaPlayback';
|
||||
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
|
||||
import {type AutocompleteOption, isEmoji, isMeme, isSticker} from '@app/features/channel/components/Autocomplete';
|
||||
import styles from '@app/features/channel/components/AutocompleteEmoji.module.css';
|
||||
import {AutocompleteItem} from '@app/features/channel/components/AutocompleteItem';
|
||||
import {AutocompleteMemePreview} from '@app/features/channel/components/AutocompleteMemePreview';
|
||||
import * as EmojiPickerCommands from '@app/features/emoji/commands/EmojiPickerCommands';
|
||||
import {useStickerAnimation} from '@app/features/emoji/hooks/useStickerAnimation';
|
||||
import type {FlatEmoji} from '@app/features/emoji/types/EmojiTypes';
|
||||
@@ -16,13 +16,12 @@ import {
|
||||
MEDIA_DESCRIPTOR,
|
||||
STICKERS_DESCRIPTOR,
|
||||
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {getEmojiRenderUrl} from '@app/features/messaging/utils/markdown/EmojiDetector';
|
||||
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {MusicNoteIcon} from '@phosphor-icons/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {useRef} from 'react';
|
||||
|
||||
const DEFAULT_EMOJI_DESCRIPTOR = msg({
|
||||
message: 'Default emoji',
|
||||
@@ -34,53 +33,53 @@ const SectionHeading = observer(({children}: {children: React.ReactNode}) => (
|
||||
</div>
|
||||
));
|
||||
const AutocompleteEmojiIcon = observer(({emoji}: {emoji: FlatEmoji}) => {
|
||||
const shouldAnimate = useShouldAnimate({kind: 'emoji'});
|
||||
const isAnimatable = Boolean(emoji.animated);
|
||||
const shouldAnimate = useShouldAnimate({
|
||||
kind: 'emoji',
|
||||
isAnimated: isAnimatable,
|
||||
});
|
||||
const {url: fallbackDisplayUrl} = getEmojiDisplayData(emoji);
|
||||
const displayUrl = emoji.id
|
||||
? AvatarUtils.getEmojiURL({id: emoji.id, animated: Boolean(emoji.animated) && shouldAnimate})
|
||||
: (fallbackDisplayUrl ?? '');
|
||||
const displayUrl =
|
||||
getEmojiRenderUrl({
|
||||
id: emoji.id,
|
||||
surrogateUrl: fallbackDisplayUrl ?? null,
|
||||
isAnimatable,
|
||||
animated: shouldAnimate,
|
||||
jumbo: false,
|
||||
}) ?? '';
|
||||
return (
|
||||
<img
|
||||
draggable={false}
|
||||
className={styles.emojiIcon}
|
||||
src={displayUrl}
|
||||
alt={emoji.name}
|
||||
aria-hidden={true}
|
||||
data-flx="channel.autocomplete-emoji.emoji-icon"
|
||||
/>
|
||||
);
|
||||
});
|
||||
const AutocompleteStickerIcon = observer(
|
||||
({sticker, isInteracting}: {sticker: GuildSticker; isInteracting: boolean}) => {
|
||||
const {shouldAnimate} = useStickerAnimation({isInteracting});
|
||||
const {shouldAnimate} = useStickerAnimation({isInteracting, isAnimated: sticker.animated});
|
||||
return (
|
||||
<div className={styles.stickerIconWrapper} data-flx="channel.autocomplete-emoji.sticker-icon-wrapper">
|
||||
<img
|
||||
draggable={false}
|
||||
className={styles.stickerIcon}
|
||||
src={AvatarUtils.getStickerURL({id: sticker.id, animated: shouldAnimate, size: 320})}
|
||||
src={AvatarUtils.getStickerURL({
|
||||
id: sticker.id,
|
||||
animated: shouldAnimate,
|
||||
isAnimatable: sticker.animated,
|
||||
size: 320,
|
||||
})}
|
||||
alt={sticker.name}
|
||||
aria-hidden={true}
|
||||
data-flx="channel.autocomplete-emoji.sticker-icon"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
const AutocompleteMemeVideo = ({src}: {src: string}) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const playbackAllowed = useAnimatedMediaVideoPlayback(videoRef);
|
||||
return (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={src}
|
||||
className={styles.memeVideo}
|
||||
muted
|
||||
autoPlay={playbackAllowed}
|
||||
loop
|
||||
playsInline
|
||||
data-flx="channel.autocomplete-emoji.meme-video"
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const AutocompleteEmoji = observer(
|
||||
({
|
||||
onSelect,
|
||||
@@ -214,31 +213,10 @@ export const AutocompleteEmoji = observer(
|
||||
description={option.meme.tags.length > 0 ? option.meme.tags.join(', ') : undefined}
|
||||
icon={
|
||||
<div className={styles.memeIconWrapper} data-flx="channel.autocomplete-emoji.meme-icon-wrapper">
|
||||
{option.meme.contentType.startsWith('video/') || option.meme.contentType.includes('gif') ? (
|
||||
<AutocompleteMemeVideo
|
||||
src={option.meme.url}
|
||||
data-flx="channel.autocomplete-emoji.autocomplete-meme-video"
|
||||
/>
|
||||
) : option.meme.contentType.startsWith('audio/') ? (
|
||||
<div
|
||||
className={styles.audioIconWrapper}
|
||||
data-flx="channel.autocomplete-emoji.audio-icon-wrapper"
|
||||
>
|
||||
<MusicNoteIcon
|
||||
className={styles.audioIcon}
|
||||
weight="fill"
|
||||
data-flx="channel.autocomplete-emoji.audio-icon"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
draggable={false}
|
||||
className={styles.memeIcon}
|
||||
src={option.meme.url}
|
||||
alt={option.meme.name}
|
||||
data-flx="channel.autocomplete-emoji.meme-icon"
|
||||
/>
|
||||
)}
|
||||
<AutocompleteMemePreview
|
||||
meme={option.meme}
|
||||
data-flx="channel.autocomplete-emoji.autocomplete-meme-preview"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
isKeyboardSelected={currentIndex === keyboardFocusIndex}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {useAnimatedMediaVideoPlayback} from '@app/features/app/hooks/useAnimatedMediaPlayback';
|
||||
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
|
||||
import {type AutocompleteOption, isGif} from '@app/features/channel/components/Autocomplete';
|
||||
import styles from '@app/features/channel/components/AutocompleteGif.module.css';
|
||||
import * as KlipyUtils from '@app/features/expressions/utils/KlipyUtils';
|
||||
import {GIFS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {buildStaticGifPreviewURL} from '@app/features/messaging/utils/MediaProxyUtils';
|
||||
import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
@@ -16,22 +18,56 @@ const NO_GIFS_FOUND_DESCRIPTOR = msg({
|
||||
message: 'No GIFs match',
|
||||
comment: 'Empty-state text in the channel and chat autocomplete gif.',
|
||||
});
|
||||
const AutocompleteGifVideo = ({src}: {src: string}) => {
|
||||
const PLAYABLE_PREVIEW_EXTENSION_PATTERN = /\.(mp4|webm|mov|m4v)(?:$|\?)/iu;
|
||||
|
||||
function isPlayablePreviewSource(value: string): boolean {
|
||||
try {
|
||||
return PLAYABLE_PREVIEW_EXTENSION_PATTERN.test(new URL(value).pathname);
|
||||
} catch {
|
||||
return PLAYABLE_PREVIEW_EXTENSION_PATTERN.test(value);
|
||||
}
|
||||
}
|
||||
const AutocompleteGifVideo = ({src, motionAllowed}: {src: string; motionAllowed: boolean}) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const playbackAllowed = useAnimatedMediaVideoPlayback(videoRef);
|
||||
const playbackAllowed = useAnimatedMediaVideoPlayback(videoRef, {shouldPlay: motionAllowed});
|
||||
return (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={src}
|
||||
className={styles.gifVideo}
|
||||
muted
|
||||
autoPlay={playbackAllowed}
|
||||
autoPlay={motionAllowed && playbackAllowed}
|
||||
loop
|
||||
playsInline
|
||||
preload={motionAllowed ? 'auto' : 'metadata'}
|
||||
src={src}
|
||||
data-flx="channel.autocomplete-gif.gif-video"
|
||||
/>
|
||||
);
|
||||
};
|
||||
const AutocompleteGifPreview = observer(({src, isActive}: {src: string; isActive: boolean}) => {
|
||||
const motionAllowed = useShouldAnimate({kind: 'gif', isHovering: isActive});
|
||||
if (src.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (isPlayablePreviewSource(src)) {
|
||||
return (
|
||||
<AutocompleteGifVideo
|
||||
src={src}
|
||||
motionAllowed={motionAllowed}
|
||||
data-flx="channel.autocomplete-gif.autocomplete-gif-video"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<img
|
||||
draggable={false}
|
||||
src={motionAllowed ? src : buildStaticGifPreviewURL(src)}
|
||||
alt=""
|
||||
className={styles.gifVideo}
|
||||
data-flx="channel.autocomplete-gif.gif-image"
|
||||
/>
|
||||
);
|
||||
});
|
||||
export const AutocompleteGif = observer(
|
||||
({
|
||||
onSelect,
|
||||
@@ -117,9 +153,10 @@ export const AutocompleteGif = observer(
|
||||
data-flx="channel.autocomplete-gif.gif-button.select"
|
||||
>
|
||||
<div className={styles.gifVideoWrapper} data-flx="channel.autocomplete-gif.gif-video-wrapper">
|
||||
<AutocompleteGifVideo
|
||||
<AutocompleteGifPreview
|
||||
src={gif.proxy_src}
|
||||
data-flx="channel.autocomplete-gif.autocomplete-gif-video"
|
||||
isActive={isActive}
|
||||
data-flx="channel.autocomplete-gif.autocomplete-gif-preview"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {useAnimatedMediaVideoPlayback} from '@app/features/app/hooks/useAnimatedMediaPlayback';
|
||||
import {type AutocompleteOption, isMeme} from '@app/features/channel/components/Autocomplete';
|
||||
import styles from '@app/features/channel/components/AutocompleteEmoji.module.css';
|
||||
import {AutocompleteItem} from '@app/features/channel/components/AutocompleteItem';
|
||||
import {MusicNoteIcon} from '@phosphor-icons/react';
|
||||
import {AutocompleteMemePreview} from '@app/features/channel/components/AutocompleteMemePreview';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {useRef} from 'react';
|
||||
|
||||
const AutocompleteMemeVideo = ({src}: {src: string}) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const playbackAllowed = useAnimatedMediaVideoPlayback(videoRef);
|
||||
return (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={src}
|
||||
className={styles.memeVideo}
|
||||
muted
|
||||
autoPlay={playbackAllowed}
|
||||
loop
|
||||
playsInline
|
||||
data-flx="channel.autocomplete-meme.meme-video"
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const AutocompleteMeme = observer(
|
||||
({
|
||||
onSelect,
|
||||
@@ -54,28 +36,10 @@ export const AutocompleteMeme = observer(
|
||||
description={option.meme.tags.length > 0 ? option.meme.tags.join(', ') : undefined}
|
||||
icon={
|
||||
<div className={styles.memeIconWrapper} data-flx="channel.autocomplete-meme.meme-icon-wrapper">
|
||||
{option.meme.contentType.startsWith('video/') || option.meme.contentType.includes('gif') ? (
|
||||
<AutocompleteMemeVideo
|
||||
src={option.meme.url}
|
||||
data-flx="channel.autocomplete-meme.autocomplete-meme-video"
|
||||
/>
|
||||
) : option.meme.contentType.startsWith('audio/') ? (
|
||||
<div className={styles.audioIconWrapper} data-flx="channel.autocomplete-meme.audio-icon-wrapper">
|
||||
<MusicNoteIcon
|
||||
className={styles.audioIcon}
|
||||
weight="fill"
|
||||
data-flx="channel.autocomplete-meme.audio-icon"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
draggable={false}
|
||||
className={styles.memeIcon}
|
||||
src={option.meme.url}
|
||||
alt={option.meme.name}
|
||||
data-flx="channel.autocomplete-meme.meme-icon"
|
||||
/>
|
||||
)}
|
||||
<AutocompleteMemePreview
|
||||
meme={option.meme}
|
||||
data-flx="channel.autocomplete-meme.autocomplete-meme-preview"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
isKeyboardSelected={index === keyboardFocusIndex}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {useAnimatedMediaVideoPlayback} from '@app/features/app/hooks/useAnimatedMediaPlayback';
|
||||
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
|
||||
import styles from '@app/features/channel/components/AutocompleteEmoji.module.css';
|
||||
import type {FavoriteMeme} from '@app/features/expressions/models/FavoriteMeme';
|
||||
import {buildStaticGifPreviewURL} from '@app/features/messaging/utils/MediaProxyUtils';
|
||||
import {MusicNoteIcon} from '@phosphor-icons/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import {useRef} from 'react';
|
||||
|
||||
const AutocompleteMemeVideo = ({src}: {src: string}) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const playbackAllowed = useAnimatedMediaVideoPlayback(videoRef);
|
||||
return (
|
||||
<video
|
||||
ref={videoRef}
|
||||
className={styles.memeVideo}
|
||||
muted
|
||||
autoPlay={playbackAllowed}
|
||||
loop
|
||||
playsInline
|
||||
preload="auto"
|
||||
src={src}
|
||||
data-flx="channel.autocomplete-meme-preview.meme-video"
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const AutocompleteMemePreview = observer(({meme}: {meme: FavoriteMeme}) => {
|
||||
const contentType = meme.contentType.toLowerCase();
|
||||
const isAudioMeme = contentType.startsWith('audio/');
|
||||
const isVideoMeme = contentType.startsWith('video/');
|
||||
const isAnimatedImageMeme = !isVideoMeme && !isAudioMeme && contentType.includes('gif');
|
||||
const motionAllowed = useShouldAnimate({kind: 'gif', isAnimated: isVideoMeme || isAnimatedImageMeme});
|
||||
if (isAudioMeme) {
|
||||
return (
|
||||
<div className={styles.audioIconWrapper} data-flx="channel.autocomplete-meme-preview.audio-icon-wrapper">
|
||||
<MusicNoteIcon
|
||||
className={styles.audioIcon}
|
||||
weight="fill"
|
||||
data-flx="channel.autocomplete-meme-preview.audio-icon"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isVideoMeme && motionAllowed) {
|
||||
return (
|
||||
<AutocompleteMemeVideo src={meme.url} data-flx="channel.autocomplete-meme-preview.autocomplete-meme-video" />
|
||||
);
|
||||
}
|
||||
const needsStillFrame = (isVideoMeme || isAnimatedImageMeme) && !motionAllowed;
|
||||
return (
|
||||
<img
|
||||
draggable={false}
|
||||
className={styles.memeIcon}
|
||||
src={needsStillFrame ? buildStaticGifPreviewURL(meme.url) : meme.url}
|
||||
alt={meme.name}
|
||||
aria-hidden={true}
|
||||
data-flx="channel.autocomplete-meme-preview.meme-icon"
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -10,15 +10,16 @@ import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
|
||||
const AutocompleteStickerIcon = observer(
|
||||
({id, name, isInteracting}: {id: string; name: string; isInteracting: boolean}) => {
|
||||
const {shouldAnimate} = useStickerAnimation({isInteracting});
|
||||
({id, name, animated, isInteracting}: {id: string; name: string; animated: boolean; isInteracting: boolean}) => {
|
||||
const {shouldAnimate} = useStickerAnimation({isInteracting, isAnimated: animated});
|
||||
return (
|
||||
<div className={styles.stickerIconWrapper} data-flx="channel.autocomplete-sticker.sticker-icon-wrapper">
|
||||
<img
|
||||
draggable={false}
|
||||
className={styles.stickerIcon}
|
||||
src={AvatarUtils.getStickerURL({id, animated: shouldAnimate, size: 320})}
|
||||
src={AvatarUtils.getStickerURL({id, animated: shouldAnimate, isAnimatable: animated, size: 320})}
|
||||
alt={name}
|
||||
aria-hidden={true}
|
||||
data-flx="channel.autocomplete-sticker.sticker-icon"
|
||||
/>
|
||||
</div>
|
||||
@@ -58,6 +59,7 @@ export const AutocompleteSticker = observer(
|
||||
<AutocompleteStickerIcon
|
||||
id={option.sticker.id}
|
||||
name={option.sticker.name}
|
||||
animated={option.sticker.animated}
|
||||
isInteracting={index === keyboardFocusIndex || index === hoverIndex}
|
||||
data-flx="channel.autocomplete-sticker.autocomplete-sticker-icon"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user