Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.0.0",
"@fontsource-variable/nunito": "5.2.7",
"@fontsource/space-mono": "5.2.9",
"@lottiefiles/dotlottie-react": "^0.12.0",
"@phosphor-icons/react": "^2.1.10",
"@sableclient/twemoji-font": "^1.0.4",
"@sentry/react": "^10.63.0",
Expand Down
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/app/components/editor/Elements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -91,7 +92,7 @@ function RenderEmoticonElement({
contentEditable={false}
>
{element.key.startsWith('mxc://') ? (
<img
<MediaImage
className={css.EmoticonImg}
src={mxcUrlToHttp(mx, element.key, useAuthentication) ?? element.key}
alt={element.shortcode}
Expand Down
15 changes: 12 additions & 3 deletions src/app/components/emoji-board/EmojiBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -192,12 +193,17 @@ const useItemRenderer = (tab: EmojiBoardTab, saveStickerEmojiBandwidth: boolean)
gif={gif}
style={{ aspectRatio }}
>
<img
<MediaImage
loading="lazy"
alt=""
aria-hidden
src={gifUrl}
style={{ display: 'block', width: '100%', height: '100%', objectFit: 'cover' }}
style={{
display: 'block',
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
</GifItem>
);
Expand Down Expand Up @@ -428,7 +434,10 @@ function EmojiGroupHolder({
);
}

const DefaultEmojiPreview: PreviewData = { key: '🙂', shortcode: 'slight_smile' };
const DefaultEmojiPreview: PreviewData = {
key: '🙂',
shortcode: 'slight_smile',
};

const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 1000,
Expand Down
14 changes: 11 additions & 3 deletions src/app/components/emoji-board/components/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { MatrixClient } from '$types/matrix-sdk';
import type { PackImageReader } from '$plugins/custom-emoji';
import type { IEmoji } from '$plugins/emoji';
import { mxcUrlToHttp } from '$utils/matrix';
import { Image as MediaImage } from '$components/media';
import type { EmojiItemInfo, GifData } from '$components/emoji-board/types';
import { EmojiType } from '$components/emoji-board/types';
import type { CSSProperties, ReactNode } from 'react';
Expand Down Expand Up @@ -103,7 +104,7 @@ export function CustomEmojiItem({
data-emoji-data={image.url}
data-emoji-shortcode={image.shortcode}
>
<img
<MediaImage
loading="lazy"
className={css.CustomEmojiImg}
alt={image.body || image.shortcode}
Expand Down Expand Up @@ -139,7 +140,7 @@ export function StickerItem({
data-emoji-data={image.url}
data-emoji-shortcode={image.shortcode}
>
<img
<MediaImage
loading="lazy"
className={css.StickerImg}
alt={image.body || image.shortcode}
Expand Down Expand Up @@ -210,7 +211,14 @@ export function GifItem({
>
{children}
{isHovered && (
<Box style={{ padding: config.space.S200, right: 0, top: 0, position: 'absolute' }}>
<Box
style={{
padding: config.space.S200,
right: 0,
top: 0,
position: 'absolute',
}}
>
<Menu style={{ padding: config.space.S0 }}>
<Box>
<MenuItem
Expand Down
3 changes: 2 additions & 1 deletion src/app/components/emoji-board/components/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -35,7 +36,7 @@ export function Preview({ previewAtom }: PreviewProps) {
justifyContent="Center"
>
{key.startsWith('mxc://') ? (
<img
<MediaImage
className={css.PreviewImg}
src={mxcUrlToHttp(mx, key, useAuthentication) ?? key}
alt={shortcode}
Expand Down
7 changes: 4 additions & 3 deletions src/app/components/emoji-board/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { ReactNode } from 'react';
import { Box, Scroll, Line, as, TooltipProvider, Tooltip, Text, IconButton } from 'folds';
import classNames from 'classnames';
import { sizedIcon, Image, type PhosphorIcon } from '$components/icons/phosphor';
import { Image as MediaImage } from '$components/media';
import { sizedIcon, Image as ImageIcon, type PhosphorIcon } from '$components/icons/phosphor';
import * as css from './styles.css';

export function Sidebar({ children }: { children: ReactNode }) {
Expand Down Expand Up @@ -110,9 +111,9 @@ export function ImageGroupIcon<T extends string>({
return (
<SidebarBtn active={active} id={id} label={label} onClick={onClick}>
{url ? (
<img className={css.SidebarBtnImg} src={url} alt={label} />
<MediaImage className={css.SidebarBtnImg} src={url} alt={label} />
) : (
sizedIcon(Image, '200', { filled: active })
sizedIcon(ImageIcon, '200', { filled: active })
)}
</SidebarBtn>
);
Expand Down
3 changes: 2 additions & 1 deletion src/app/components/image-editor/ImageEditor.tsx
Original file line number Diff line number Diff line change
@@ -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';

export type ImageEditorProps = {
Expand Down Expand Up @@ -43,7 +44,7 @@ export const ImageEditor = as<'div', ImageEditorProps>(
justifyContent="Center"
alignItems="Center"
>
<img className={css.Image} src={url} alt={name} />
<MediaImage className={css.Image} src={url} alt={name} />
</Box>
</Box>
);
Expand Down
7 changes: 4 additions & 3 deletions src/app/components/image-pack-view/ImageTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -40,7 +41,7 @@ export function ImageTile({
return (
<SettingTile
before={
<img
<MediaImage
className={css.ImagePackImage}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
alt={image.shortcode}
Expand Down Expand Up @@ -102,7 +103,7 @@ export function ImageTileUpload({ file, children }: ImageTileUploadProps) {
const uploadAtom = useMemo(() => createUploadAtom(file), [file]);

return (
<SettingTile before={<img className={css.ImagePackImage} src={url} alt={file.name} />}>
<SettingTile before={<MediaImage className={css.ImagePackImage} src={url} alt={file.name} />}>
{children(uploadAtom)}
</SettingTile>
);
Expand Down Expand Up @@ -164,7 +165,7 @@ export function ImageTileEdit({
return (
<SettingTile
before={
<img
<MediaImage
className={css.ImagePackImage}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
alt={image.shortcode}
Expand Down
34 changes: 34 additions & 0 deletions src/app/components/image-viewer/ImageViewer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type { ImgHTMLAttributes } from 'react';
import { ImageViewer } from './ImageViewer';

vi.mock('$components/media', () => ({
Image: ({ alt, ...props }: ImgHTMLAttributes<HTMLImageElement>) => <img alt={alt} {...props} />,
}));

vi.mock('$hooks/useImageGestures', () => ({
useImageGestures: () => ({
transforms: { zoom: 1, pan: { x: 0, y: 0 } },
cursor: 'initial',
handleWheel: () => {},
onPointerDown: () => {},
resetTransforms: () => {},
zoomIn: () => {},
zoomOut: () => {},
setZoom: () => {},
fitRatio: 1,
imageRef: { current: null },
containerRef: { current: null },
handleImageLoad: () => {},
enableResizeWithWindow: () => {},
}),
}));

describe('ImageViewer', () => {
it('renders the fullscreen image without crashing', () => {
render(<ImageViewer alt="demo" src="https://example.com/demo.png" requestClose={() => {}} />);

expect(screen.getByAltText('demo')).toBeInTheDocument();
});
});
3 changes: 2 additions & 1 deletion src/app/components/image-viewer/ImageViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
phosphorSizeRem,
sizedIcon,
} from '$components/icons/phosphor';
import { Image as MediaImage } from '$components/media';
import { useImageGestures } from '$hooks/useImageGestures';
import { useSetting } from '$state/hooks/settings';
import { isPixelatedRendering, settingsAtom } from '$state/settings';
Expand Down Expand Up @@ -340,7 +341,7 @@ export const ImageViewer = as<'div', ImageViewerProps>(
onPointerDown={onPointerDown}
onContextMenu={handleContextMenu}
>
<img
<MediaImage
className={classNames(css.ImageViewerImg, isPixelated && css.ImageViewerImgPixelated)}
draggable={false}
data-gestures="ignore"
Expand Down
29 changes: 29 additions & 0 deletions src/app/components/media/Image.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Image } from './Image';
import { gzipSync } from 'node:zlib';

// file from https://github.com/LottieFiles/test-files/blob/main/data/shapes/rectangle.json
const lottieJson =
'{"v":"5.11.0","fr":60,"ip":0,"op":120,"w":100,"h":50,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,30],"ix":2},"p":{"a":0,"k":[50,25],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.196077997544,0.313725998822,0.690195958755,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}}';

describe('Image', () => {
it('renders a regular img for non-lottie sources', () => {
render(<Image src="data:image/png;base64,ZmFrZQ==" alt="demo" />);

const image = screen.getByAltText('demo');
expect(image.tagName).toBe('IMG');
expect(image).toHaveAttribute('src', 'data:image/png;base64,ZmFrZQ==');
});

it('uses the lottie renderer for gzipped lottie data', async () => {
const gzipped = Buffer.from(gzipSync(lottieJson)).toString('base64');
render(
<Image src={`data:application/gzip;base64,${gzipped}`} height={123} aria-label="demo" />
);

const rendered = screen.getByLabelText('demo');
expect(rendered).toBeInTheDocument();
expect(rendered).toHaveAttribute('height', '123');
});
});
Loading
Loading