Skip to content
Closed
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
26 changes: 23 additions & 3 deletions src/app/components/url-preview/UrlPreview.css.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export const UrlPreview = style([
DefaultReset,
{
width: toRem(400),
minHeight: toRem(102),
display: 'flex',
flexDirection: 'column',
backgroundColor: color.SurfaceVariant.Container,
color: color.SurfaceVariant.OnContainer,
border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
Expand All @@ -18,21 +19,40 @@ export const UrlPreviewImg = style([
DefaultReset,
{
width: toRem(100),
height: toRem(100),
minHeight: toRem(100),
height: '100%',
objectFit: 'cover',
objectPosition: 'center',
flexShrink: 0,
overflow: 'hidden',
cursor: 'pointer',
},
]);

export const UrlPreviewHeroImg = style([
DefaultReset,
{
width: '100%',
maxHeight: toRem(300),
objectFit: 'contain',
backgroundColor: '#000',
cursor: 'pointer',
},
]);

export const UrlPreviewContent = style([
DefaultReset,
{
padding: config.space.S200,
flexGrow: 1,
},
]);

export const UrlPreviewCardRow = style({
display: 'flex',
flexDirection: 'row',
alignItems: 'stretch',
});

export const UrlPreviewDescription = style([
DefaultReset,
{
Expand Down
4 changes: 4 additions & 0 deletions src/app/components/url-preview/UrlPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export const UrlPreviewImg = as<'img'>(({ className, alt, ...props }, ref) => (
<img className={classNames(css.UrlPreviewImg, className)} alt={alt} {...props} ref={ref} />
));

export const UrlPreviewHeroImg = as<'img'>(({ className, alt, ...props }, ref) => (
<img className={classNames(css.UrlPreviewHeroImg, className)} alt={alt} {...props} ref={ref} />
));

export const UrlPreviewContent = as<'div'>(({ className, ...props }, ref) => (
<Box
grow="Yes"
Expand Down
16 changes: 16 additions & 0 deletions src/app/components/url-preview/UrlPreviewCard.css.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { recipe } from '@vanilla-extract/recipes';
import { style } from '@vanilla-extract/css';
import { DefaultReset, color, toRem } from 'folds';

export const UrlPreviewHolderGradient = recipe({
Expand All @@ -24,6 +25,7 @@ export const UrlPreviewHolderGradient = recipe({
},
},
});

export const UrlPreviewHolderBtn = recipe({
base: [
DefaultReset,
Expand All @@ -45,3 +47,17 @@ export const UrlPreviewHolderBtn = recipe({
},
},
});

export const UrlPreviewModal = style([
DefaultReset,
{
width: '90vw',
height: '85vh',
maxWidth: toRem(1200),
maxHeight: toRem(1000),
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
backgroundColor: color.Background.Container,
},
]);
130 changes: 114 additions & 16 deletions src/app/components/url-preview/UrlPreviewCard.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { IPreviewUrlResponse } from 'matrix-js-sdk';
import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds';
import {
Box,
Icon,
IconButton,
Icons,
Modal,
Overlay,
OverlayBackdrop,
OverlayCenter,
Scroll,
Spinner,
Text,
as,
color,
config,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview';
import {
UrlPreview,
UrlPreviewContent,
UrlPreviewDescription,
UrlPreviewHeroImg,
UrlPreviewImg,
} from './UrlPreview';
import {
getIntersectionObserverEntry,
useIntersectionObserver,
} from '../../hooks/useIntersectionObserver';
import * as css from './UrlPreviewCard.css';
import * as baseCss from './UrlPreview.css';
import { tryDecodeURIComponent } from '../../utils/dom';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { ImageViewer } from '../image-viewer';
import { stopPropagation } from '../../utils/keyboard';

const linkStyles = { color: color.Success.Main };

export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
({ url, ts, ...props }, ref) => {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [viewImage, setViewImage] = useState<string | null>(null);

const [previewStatus, loadPreview] = useAsyncCallback(
useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
);
Expand All @@ -31,10 +58,53 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(

const renderContent = (prev: IPreviewUrlResponse) => {
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication, 256, 256, 'scale', false);
const rawImgUrl = prev['og:image'] ? mxcUrlToHttp(mx, prev['og:image'], useAuthentication) : null;

const title = prev['og:title'];
const description = prev['og:description'];
const siteName = prev['og:site_name'];

const isHeroImage =
rawImgUrl &&
(prev['og:type']?.startsWith('image') || !description || title === url);

if (isHeroImage) {
return (
<>
<UrlPreviewHeroImg
src={rawImgUrl ?? ''}
alt={title}
title={title}
onClick={() => setViewImage(rawImgUrl)}
/>
<UrlPreviewContent>
<Text
style={linkStyles}
truncate
as="a"
href={url}
target="_blank"
rel="no-referrer"
size="T200"
priority="300"
>
{tryDecodeURIComponent(url)}
</Text>
</UrlPreviewContent>
</>
);
}

return (
<>
{imgUrl && <UrlPreviewImg src={imgUrl} alt={prev['og:title']} title={prev['og:title']} />}
<div className={baseCss.UrlPreviewCardRow}>
{imgUrl && (
<UrlPreviewImg
src={imgUrl}
alt={title}
title={title}
onClick={() => setViewImage(rawImgUrl || imgUrl)}
/>
)}
<UrlPreviewContent>
<Text
style={linkStyles}
Expand All @@ -46,30 +116,58 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
size="T200"
priority="300"
>
{typeof prev['og:site_name'] === 'string' && `${prev['og:site_name']} | `}
{typeof siteName === 'string' && `${siteName} | `}
{tryDecodeURIComponent(url)}
</Text>
<Text truncate priority="400">
<b>{prev['og:title']}</b>
<b>{title}</b>
</Text>
<Text size="T200" priority="300">
<UrlPreviewDescription>{prev['og:description']}</UrlPreviewDescription>
<UrlPreviewDescription>{description}</UrlPreviewDescription>
</Text>
</UrlPreviewContent>
</>
</div>
);
};

return (
<UrlPreview {...props} ref={ref}>
{previewStatus.status === AsyncStatus.Success ? (
renderContent(previewStatus.data)
) : (
<Box grow="Yes" alignItems="Center" justifyContent="Center">
<Spinner variant="Secondary" size="400" />
</Box>
<>
<UrlPreview {...props} ref={ref}>
{previewStatus.status === AsyncStatus.Success ? (
renderContent(previewStatus.data)
) : (
<Box grow="Yes" alignItems="Center" justifyContent="Center" style={{ minHeight: '102px' }}>
<Spinner variant="Secondary" size="400" />
</Box>
)}
</UrlPreview>

{viewImage && (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setViewImage(null),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal
className={css.UrlPreviewModal}
onContextMenu={(evt: React.MouseEvent) => evt.stopPropagation()}
>
<ImageViewer
src={viewImage}
alt="Image Preview"
requestClose={() => setViewImage(null)}
/>
</Modal>
</FocusTrap>
</OverlayCenter>
</Overlay>
)}
</UrlPreview>
</>
);
}
);
Expand Down