Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .changeset/fix-inline-images-behaviors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
default: patch
---

Added a couple new settings for max incoming inline image height and default height for unspecified.
Added a couple new settings for max incoming inline image height and default height for unspecified. http://localhost:8080/settings/appearance?focus=incoming-inline-images-default-height&moe.sable.client.action=settings
5 changes: 5 additions & 0 deletions .changeset/fix-max-size-for-previews.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Added the ability to cap preview embed size. http://localhost:8080/settings/appearance?focus=link-preview-image-max-height&moe.sable.client.action=settings
91 changes: 86 additions & 5 deletions src/app/components/message/content/ImageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
TooltipProvider,
as,
config,
toRem,
} from 'folds';
import classNames from 'classnames';
import { BlurhashCanvas } from 'react-blurhash';
Expand All @@ -37,6 +38,23 @@
import * as css from './style.css';
import { MATRIX_UNSTABLE_BLUR_HASH_PROPERTY_NAME } from '../../../../unstable/prefixes';

function thumbnailDimsForMaxEdge(
maxEdge: number,
w?: number,
h?: number
): { tw: number; th: number } {
const safeEdge = Math.max(1, Math.round(maxEdge));
const iw = typeof w === 'number' && Number.isFinite(w) && w > 0 ? w : safeEdge;
const ih = typeof h === 'number' && Number.isFinite(h) && h > 0 ? h : safeEdge;
const longest = Math.max(iw, ih);
if (longest <= safeEdge) return { tw: Math.round(iw), th: Math.round(ih) };
const scale = safeEdge / longest;
return {
tw: Math.max(1, Math.round(iw * scale)),
th: Math.max(1, Math.round(ih * scale)),
};
}

type RenderViewerProps = {
src: string;
alt: string;
Expand All @@ -62,6 +80,10 @@
spoilerReason?: string;
renderViewer: (props: RenderViewerProps) => ReactNode;
renderImage: (props: RenderImageProps) => ReactNode;
matrixThumbnailMaxEdge?: number;
mediaLayout?: 'default' | 'contained';
containedStripMinPx?: number;
fillsPreviewSlot?: boolean;
};
export const ImageContent = as<'div', ImageContentProps>(
(
Expand All @@ -78,6 +100,10 @@
spoilerReason,
renderViewer,
renderImage,
matrixThumbnailMaxEdge,
mediaLayout = 'default',
containedStripMinPx,
fillsPreviewSlot,
...props
},
ref
Expand All @@ -89,13 +115,20 @@
const [load, setLoad] = useState(false);
const [error, setError] = useState(false);
const [viewer, setViewer] = useState(false);
const [viewerFullSrc, setViewerFullSrc] = useState<string | null>(null);
const [blurred, setBlurred] = useState(markedAsSpoiler ?? false);
const [isHovered, setIsHovered] = useState(false);

const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => {
if (url.startsWith('http')) return url;

if (typeof matrixThumbnailMaxEdge === 'number' && matrixThumbnailMaxEdge > 0 && !encInfo) {
const { tw, th } = thumbnailDimsForMaxEdge(matrixThumbnailMaxEdge, info?.w, info?.h);
const thumbUrl = mxcUrlToHttp(mx, url, useAuthentication, tw, th, 'scale', false);
if (thumbUrl) return thumbUrl;
}

const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
if (encInfo) {
Expand All @@ -105,9 +138,33 @@
return URL.createObjectURL(fileContent);
}
return mediaUrl;
}, [mx, url, useAuthentication, mimeType, encInfo])
}, [mx, url, useAuthentication, mimeType, encInfo, matrixThumbnailMaxEdge, info?.w, info?.h])
);

useEffect(() => {
if (!viewer) {
setViewerFullSrc(null);
return;
}
if (
typeof matrixThumbnailMaxEdge !== 'number' ||
matrixThumbnailMaxEdge <= 0 ||
encInfo ||
url.startsWith('http')
) {
return;
}
let cancelled = false;
void (async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl || cancelled) return;
setViewerFullSrc(mediaUrl);
})();
return () => {
cancelled = true;
};

Check warning on line 165 in src/app/components/message/content/ImageContent.tsx

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(consistent-return)

Function expected no return value.
}, [viewer, matrixThumbnailMaxEdge, encInfo, url, mx, useAuthentication]);

const handleLoad = () => {
setLoad(true);
};
Expand All @@ -126,12 +183,36 @@
}, [autoPlay, loadSrc]);

const hasDimensions = typeof info?.w === 'number' && typeof info?.h === 'number';
const isContained = mediaLayout === 'contained';
const fillsSlot = Boolean(fillsPreviewSlot && isContained);
const containedReserveStrip =
!fillsSlot &&
isContained &&
(srcState.status === AsyncStatus.Loading ||
srcState.status === AsyncStatus.Error ||
error ||
(srcState.status === AsyncStatus.Success && !load));

const rootClass = isContained ? css.ContainedMediaRoot : css.RelativeBase;
const stripMin = containedStripMinPx ?? 56;
const intrinsicSizingStyle = fillsSlot
? {}
: isContained
? { minHeight: containedReserveStrip ? toRem(stripMin) : undefined }
: hasDimensions
? { aspectRatio: `${info!.w} / ${info!.h}` }

Check warning on line 203 in src/app/components/message/content/ImageContent.tsx

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(no-unnecessary-type-assertion)

This assertion is unnecessary since it does not change the type of the expression.

Check warning on line 203 in src/app/components/message/content/ImageContent.tsx

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(no-unnecessary-type-assertion)

This assertion is unnecessary since it does not change the type of the expression.
: { minHeight: '150px' };

const fillPreviewSlotStyle = fillsSlot
? ({ width: '100%', height: '100%' } as const)
: undefined;

return (
<Box
className={classNames(css.RelativeBase, className)}
className={classNames(rootClass, className)}
style={{
...(hasDimensions ? { aspectRatio: `${info.w} / ${info.h}` } : { minHeight: '150px' }),
...fillPreviewSlotStyle,
...intrinsicSizingStyle,
...style,
}}
{...props}
Expand All @@ -156,7 +237,7 @@
onContextMenu={(evt: React.MouseEvent) => evt.stopPropagation()}
>
{renderViewer({
src: srcState.data,
src: viewerFullSrc ?? srcState.data,
alt: body,
requestClose: () => setViewer(false),
})}
Expand Down Expand Up @@ -196,7 +277,7 @@
{srcState.status === AsyncStatus.Success && (
<Box
className={classNames(
hasDimensions ? css.AbsoluteContainer : undefined,
hasDimensions && !isContained ? css.AbsoluteContainer : undefined,
blurred && css.Blur
)}
style={{ width: '100%' }}
Expand Down
9 changes: 9 additions & 0 deletions src/app/components/message/content/style.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ export const RelativeBase = style([
},
]);

export const ContainedMediaRoot = style([
DefaultReset,
{
position: 'relative',
width: '100%',
minHeight: 0,
},
]);

export const AbsoluteContainer = style([
DefaultReset,
{
Expand Down
7 changes: 7 additions & 0 deletions src/app/components/url-preview/UrlPreview.css.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export const UrlPreviewImg = style([
},
]);

export const UrlPreviewMediaWell = style([
DefaultReset,
{
backgroundColor: color.Surface.Container,
},
]);

export const UrlPreviewContent = style([
DefaultReset,
{
Expand Down
Loading
Loading