diff --git a/fluxer_app/src/features/channel/components/AutocompleteEmoji.tsx b/fluxer_app/src/features/channel/components/AutocompleteEmoji.tsx
index 0f8f51991..31520f70a 100644
--- a/fluxer_app/src/features/channel/components/AutocompleteEmoji.tsx
+++ b/fluxer_app/src/features/channel/components/AutocompleteEmoji.tsx
@@ -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}) => (
));
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 (
);
});
const AutocompleteStickerIcon = observer(
({sticker, isInteracting}: {sticker: GuildSticker; isInteracting: boolean}) => {
- const {shouldAnimate} = useStickerAnimation({isInteracting});
+ const {shouldAnimate} = useStickerAnimation({isInteracting, isAnimated: sticker.animated});
return (
);
},
);
-const AutocompleteMemeVideo = ({src}: {src: string}) => {
- const videoRef = useRef(null);
- const playbackAllowed = useAnimatedMediaVideoPlayback(videoRef);
- return (
-
- );
-};
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={
- {option.meme.contentType.startsWith('video/') || option.meme.contentType.includes('gif') ? (
-
- ) : option.meme.contentType.startsWith('audio/') ? (
-
-
-
- ) : (
-

- )}
+
}
isKeyboardSelected={currentIndex === keyboardFocusIndex}
diff --git a/fluxer_app/src/features/channel/components/AutocompleteGif.tsx b/fluxer_app/src/features/channel/components/AutocompleteGif.tsx
index 5656f5331..752df6120 100644
--- a/fluxer_app/src/features/channel/components/AutocompleteGif.tsx
+++ b/fluxer_app/src/features/channel/components/AutocompleteGif.tsx
@@ -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(null);
- const playbackAllowed = useAnimatedMediaVideoPlayback(videoRef);
+ const playbackAllowed = useAnimatedMediaVideoPlayback(videoRef, {shouldPlay: motionAllowed});
return (
);
};
+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 (
+
+ );
+ }
+ return (
+
+ );
+});
export const AutocompleteGif = observer(
({
onSelect,
@@ -117,9 +153,10 @@ export const AutocompleteGif = observer(
data-flx="channel.autocomplete-gif.gif-button.select"
>
diff --git a/fluxer_app/src/features/channel/components/AutocompleteMeme.tsx b/fluxer_app/src/features/channel/components/AutocompleteMeme.tsx
index 4f91a8ac5..12147c005 100644
--- a/fluxer_app/src/features/channel/components/AutocompleteMeme.tsx
+++ b/fluxer_app/src/features/channel/components/AutocompleteMeme.tsx
@@ -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(null);
- const playbackAllowed = useAnimatedMediaVideoPlayback(videoRef);
- return (
-
- );
-};
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={
- {option.meme.contentType.startsWith('video/') || option.meme.contentType.includes('gif') ? (
-
- ) : option.meme.contentType.startsWith('audio/') ? (
-
-
-
- ) : (
-

- )}
+
}
isKeyboardSelected={index === keyboardFocusIndex}
diff --git a/fluxer_app/src/features/channel/components/AutocompleteMemePreview.tsx b/fluxer_app/src/features/channel/components/AutocompleteMemePreview.tsx
new file mode 100644
index 000000000..58e12f29e
--- /dev/null
+++ b/fluxer_app/src/features/channel/components/AutocompleteMemePreview.tsx
@@ -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(null);
+ const playbackAllowed = useAnimatedMediaVideoPlayback(videoRef);
+ return (
+
+ );
+};
+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 (
+
+
+
+ );
+ }
+ if (isVideoMeme && motionAllowed) {
+ return (
+
+ );
+ }
+ const needsStillFrame = (isVideoMeme || isAnimatedImageMeme) && !motionAllowed;
+ return (
+
+ );
+});
diff --git a/fluxer_app/src/features/channel/components/AutocompleteSticker.tsx b/fluxer_app/src/features/channel/components/AutocompleteSticker.tsx
index 37c453295..676309559 100644
--- a/fluxer_app/src/features/channel/components/AutocompleteSticker.tsx
+++ b/fluxer_app/src/features/channel/components/AutocompleteSticker.tsx
@@ -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 (
@@ -58,6 +59,7 @@ export const AutocompleteSticker = observer(