diff --git a/.changeset/gzipped-lottie.md b/.changeset/gzipped-lottie.md
new file mode 100644
index 0000000000..9732f554a3
--- /dev/null
+++ b/.changeset/gzipped-lottie.md
@@ -0,0 +1,5 @@
+---
+default: patch
+---
+
+Add support for displaying gzipped lottie (e.g. tgs) files.
diff --git a/package.json b/package.json
index ab63531984..5b7ed49784 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"@choochmeque/tauri-plugin-sharekit-api": "0.4.0-rc.5",
"@fontsource-variable/nunito": "5.2.7",
"@fontsource/space-mono": "5.2.9",
+ "@lottiefiles/dotlottie-react": "^0.12.0",
"@noble/hashes": "^2.2.0",
"@phosphor-icons/react": "^2.1.10",
"@sableclient/twemoji-font": "^1.0.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3f742b21cf..aa52fec46f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -43,6 +43,9 @@ importers:
'@fontsource/space-mono':
specifier: 5.2.9
version: 5.2.9
+ '@lottiefiles/dotlottie-react':
+ specifier: ^0.12.0
+ version: 0.12.3(react@18.3.1)
'@noble/hashes':
specifier: ^2.2.0
version: 2.2.0
@@ -1613,6 +1616,14 @@ packages:
'@kwsites/file-exists@1.1.1':
resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==}
+ '@lottiefiles/dotlottie-react@0.12.3':
+ resolution: {integrity: sha512-b0k0Lakj2hmhyIkwJSpKySh6xoelpgWRaijyrCb6fraOCnzuitKglVTtYs0VWf72rk+2aSmC/0IK8j8wruVfOw==}
+ peerDependencies:
+ react: ^17 || ^18 || ^19
+
+ '@lottiefiles/dotlottie-web@0.40.1':
+ resolution: {integrity: sha512-iNz1rr8zRTSJ0xCZEPeoCX6YnaNhBdkF7gJWhSa8bwB4NTrYZm0vAuC0jF/aLLhU7q9nf6ykxffrz0Sf4JPSPg==}
+
'@matrix-org/matrix-sdk-crypto-wasm@18.3.1':
resolution: {integrity: sha512-VRjWhE1UgHnPpJ3b9B5+8z71ZC/HICFngPPFIN6ktzmUBKI5RusPujzbAQUoB3CgZ0yU58L99AfSQS4YTztSWw==}
engines: {node: '>= 18'}
@@ -7087,6 +7098,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@lottiefiles/dotlottie-react@0.12.3(react@18.3.1)':
+ dependencies:
+ '@lottiefiles/dotlottie-web': 0.40.1
+ react: 18.3.1
+
+ '@lottiefiles/dotlottie-web@0.40.1': {}
+
'@matrix-org/matrix-sdk-crypto-wasm@18.3.1': {}
'@napi-rs/canvas-android-arm64@1.0.2':
diff --git a/scripts/utils/console-style.js b/scripts/utils/console-style.js
index ae7e29b792..433a362027 100644
--- a/scripts/utils/console-style.js
+++ b/scripts/utils/console-style.js
@@ -11,7 +11,7 @@ export const ANSI = {
export function shouldUseColor() {
if (process.env.NO_COLOR !== undefined) return false;
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== '0') return true;
- return Boolean(process.stdout.isTTY);
+ return process.stdout.isTTY;
}
export function styleText(text, color, enabled) {
diff --git a/src/app/components/editor/Elements.tsx b/src/app/components/editor/Elements.tsx
index d2475b123d..2b14ced9d3 100644
--- a/src/app/components/editor/Elements.tsx
+++ b/src/app/components/editor/Elements.tsx
@@ -4,6 +4,7 @@ import { useFocused, useSelected, useSlate } from 'slate-react';
import { useAtomValue } from 'jotai';
import * as css from '$styles/CustomHtml.css';
+import { Image as MediaImage } from '$components/media';
import { useMatrixClient } from '$hooks/useMatrixClient';
import { mxcUrlToHttp } from '$utils/matrix';
import { useMediaAuthentication } from '$hooks/useMediaAuthentication';
@@ -91,8 +92,9 @@ function RenderEmoticonElement({
contentEditable={false}
>
{element.key.startsWith('mxc://') ? (
-
diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx
index a1208d7ebd..45f1ac781d 100644
--- a/src/app/components/emoji-board/EmojiBoard.tsx
+++ b/src/app/components/emoji-board/EmojiBoard.tsx
@@ -33,6 +33,7 @@ import { ImageUsage } from '$plugins/custom-emoji';
import { getEmoticonSearchStr } from '$plugins/utils';
import { VirtualTile } from '$components/virtualizer';
import { useSetting } from '$state/hooks/settings';
+import { Image as MediaImage } from '$components/media';
import { settingsAtom } from '$state/settings';
import { useEmojiGroupIcons } from './useEmojiGroupIcons';
import { useEmojiGroupLabels } from './useEmojiGroupLabels';
@@ -189,7 +190,7 @@ const useItemRenderer = (tab: EmojiBoardTab, saveStickerEmojiBandwidth: boolean)
gif={gif}
style={{ aspectRatio }}
>
-
-
@@ -139,10 +141,11 @@ export function StickerItem({
data-emoji-data={image.url}
data-emoji-shortcode={image.shortcode}
>
-
diff --git a/src/app/components/emoji-board/components/Preview.tsx b/src/app/components/emoji-board/components/Preview.tsx
index 623557517b..b5b681029f 100644
--- a/src/app/components/emoji-board/components/Preview.tsx
+++ b/src/app/components/emoji-board/components/Preview.tsx
@@ -3,6 +3,7 @@ import type { Atom } from 'jotai';
import { atom, useAtomValue } from 'jotai';
import { useMatrixClient } from '$hooks/useMatrixClient';
import { useMediaAuthentication } from '$hooks/useMediaAuthentication';
+import { Image as MediaImage } from '$components/media';
import { mxcUrlToHttp } from '$utils/matrix';
import * as css from './styles.css';
@@ -35,7 +36,7 @@ export function Preview({ previewAtom }: PreviewProps) {
justifyContent="Center"
>
{key.startsWith('mxc://') ? (
-
({
return (
{url ? (
-
+
) : (
- sizedIcon(Image, '200', { filled: active })
+ sizedIcon(ImageIcon, '200', { filled: active })
)}
);
diff --git a/src/app/components/image-editor/ImageEditor.tsx b/src/app/components/image-editor/ImageEditor.tsx
index f55ff66027..48e0c9dc09 100644
--- a/src/app/components/image-editor/ImageEditor.tsx
+++ b/src/app/components/image-editor/ImageEditor.tsx
@@ -1,6 +1,7 @@
import classNames from 'classnames';
import { Box, Chip, Header, IconButton, Text, as } from 'folds';
import { ArrowLeft, sizedIcon } from '$components/icons/phosphor';
+import { Image as MediaImage } from '$components/media';
import * as css from './ImageEditor.css';
type ImageEditorProps = {
@@ -43,7 +44,7 @@ export const ImageEditor = as<'div', ImageEditorProps>(
justifyContent="Center"
alignItems="Center"
>
-
+
);
diff --git a/src/app/components/image-pack-view/ImageTile.tsx b/src/app/components/image-pack-view/ImageTile.tsx
index 0f84a3907b..a14dd871d6 100644
--- a/src/app/components/image-pack-view/ImageTile.tsx
+++ b/src/app/components/image-pack-view/ImageTile.tsx
@@ -11,6 +11,7 @@ import type { TUploadAtom } from '$state/upload';
import { createUploadAtom } from '$state/upload';
import { replaceSpaceWithDash } from '$utils/common';
import { SettingTile } from '$components/setting-tile';
+import { Image as MediaImage } from '$components/media';
import * as css from './style.css';
import { UsageSwitcher, useUsageStr } from './UsageSwitcher';
@@ -40,7 +41,7 @@ export function ImageTile({
return (
createUploadAtom(file), [file]);
return (
- }>
+ }>
{children(uploadAtom)}
);
@@ -164,7 +165,7 @@ export function ImageTileEdit({
return (
Promise>();
+const gestureMocks = vi.hoisted(() => ({
+ onPointerDown: vi.fn<(event: React.PointerEvent) => void>(),
+}));
vi.mock('$hooks/useImageGestures', () => ({
useImageGestures: () => ({
@@ -14,7 +17,7 @@ vi.mock('$hooks/useImageGestures', () => ({
imageRef: { current: null },
containerRef: { current: null },
handleWheel: vi.fn<(event: WheelEvent) => void>(),
- onPointerDown: vi.fn<(event: PointerEvent) => void>(),
+ onPointerDown: gestureMocks.onPointerDown,
handleImageLoad: vi.fn<(event: SyntheticEvent) => void>(),
setZoom: vi.fn<(next: number) => void>(),
resetTransforms: vi.fn<() => void>(),
@@ -60,3 +63,48 @@ describe('ImageViewer', () => {
expect(FileSaver.saveAs).toHaveBeenCalledWith(expect.any(Blob), 'kitten.png');
});
});
+
+vi.mock('$components/media', async () => {
+ const { forwardRef } = await import('react');
+ return {
+ Image: forwardRef<
+ HTMLImageElement | HTMLCanvasElement,
+ React.ImgHTMLAttributes & { info?: { mimetype?: string } }
+ >(({ alt, info, ...props }, ref) =>
+ info?.mimetype === 'application/x-tgsticker' ? (
+