mirror of
https://github.com/urbit/shrub.git
synced 2024-11-28 13:54:20 +03:00
RemoteContent: more refactor
This commit is contained in:
parent
42e2025e96
commit
5ffef3c0f4
@ -34,7 +34,10 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
|
||||
return <Redirect to="/~404" />;
|
||||
}
|
||||
|
||||
const remoteRef = useRef<typeof RemoteContent | null>(null);
|
||||
const remoteRef = useRef<HTMLDivElement>(null);
|
||||
const setRef = useCallback((el: HTMLDivElement | null ) => {
|
||||
remoteRef.current = el;
|
||||
}, []);
|
||||
const index = node.post.index.split('/')[1];
|
||||
|
||||
const markRead = useCallback(() => {
|
||||
@ -133,10 +136,7 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
|
||||
) : (
|
||||
<>
|
||||
<RemoteContent
|
||||
ref={(r) => {
|
||||
// @ts-ignore RemoteContent weirdness
|
||||
remoteRef.current = r;
|
||||
}}
|
||||
embedRef={setRef}
|
||||
// @ts-ignore RemoteContent weirdness
|
||||
renderUrl={false}
|
||||
url={href}
|
||||
|
@ -1,373 +0,0 @@
|
||||
import { BaseAnchor, BaseImage, Box, Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import { hasProvider } from 'oembed-parser';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import EmbedContainer from 'react-oembed-container';
|
||||
import styled from 'styled-components';
|
||||
import { VirtualContextProps, withVirtual } from '~/logic/lib/virtualContext';
|
||||
import withState from '~/logic/lib/withState';
|
||||
import useSettingsState from '~/logic/state/settings';
|
||||
import { RemoteContentPolicy } from '~/types/local-update';
|
||||
|
||||
export type RemoteContentProps = VirtualContextProps & {
|
||||
url: string;
|
||||
text?: string;
|
||||
unfold?: boolean;
|
||||
renderUrl?: boolean;
|
||||
remoteContentPolicy?: RemoteContentPolicy;
|
||||
imageProps?: any;
|
||||
audioProps?: any;
|
||||
videoProps?: any;
|
||||
oembedProps?: any;
|
||||
textProps?: any;
|
||||
style?: any;
|
||||
transcluded?: any;
|
||||
className?: string;
|
||||
tall?: boolean;
|
||||
}
|
||||
|
||||
interface RemoteContentState {
|
||||
unfold: boolean;
|
||||
embed: any | undefined;
|
||||
noCors: boolean;
|
||||
showArrow: boolean;
|
||||
}
|
||||
|
||||
export const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
|
||||
export const AUDIO_REGEX = new RegExp(/(mp3|wav|ogg|m4a)$/i);
|
||||
export const VIDEO_REGEX = new RegExp(/(mov|mp4|ogv)$/i);
|
||||
|
||||
const TruncatedText = styled(Text)`
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
class RemoteContent extends Component<RemoteContentProps, RemoteContentState> {
|
||||
private fetchController: AbortController | undefined;
|
||||
containerRef: HTMLDivElement | null = null;
|
||||
private saving = false;
|
||||
private isOembed = false;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
unfold: props.unfold || false,
|
||||
embed: undefined,
|
||||
noCors: false,
|
||||
showArrow: false
|
||||
};
|
||||
this.unfoldEmbed = this.unfoldEmbed.bind(this);
|
||||
this.loadOembed = this.loadOembed.bind(this);
|
||||
this.wrapInLink = this.wrapInLink.bind(this);
|
||||
this.onError = this.onError.bind(this);
|
||||
this.toggleArrow = this.toggleArrow.bind(this);
|
||||
this.isOembed = hasProvider(props.url);
|
||||
}
|
||||
|
||||
save = () => {
|
||||
if(this.saving) {
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
this.props.save();
|
||||
};
|
||||
|
||||
restore = () => {
|
||||
this.saving = false;
|
||||
this.props.restore();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if(this.saving) {
|
||||
this.restore();
|
||||
}
|
||||
if (this.fetchController) {
|
||||
this.fetchController.abort();
|
||||
}
|
||||
}
|
||||
|
||||
unfoldEmbed(event: Event) {
|
||||
event.stopPropagation();
|
||||
let unfoldState = this.state.unfold;
|
||||
unfoldState = !unfoldState;
|
||||
this.save();
|
||||
this.setState({ unfold: unfoldState });
|
||||
requestAnimationFrame(() => {
|
||||
this.restore();
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if(prevState.embed !== this.state.embed) {
|
||||
// console.log('remotecontent: restoring');
|
||||
// prevProps.shiftLayout.restore();
|
||||
}
|
||||
const { url } = this.props;
|
||||
if(url !== prevProps.url && (IMAGE_REGEX.test(url) || AUDIO_REGEX.test(url) || VIDEO_REGEX.test(url))) {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
}
|
||||
|
||||
onLoad = () => {
|
||||
window.requestAnimationFrame(() => {
|
||||
const { restore } = this;
|
||||
restore();
|
||||
});
|
||||
}
|
||||
|
||||
loadOembed() {
|
||||
this.fetchController = new AbortController();
|
||||
fetch(`https://noembed.com/embed?url=${this.props.url}`, {
|
||||
signal: this.fetchController.signal
|
||||
})
|
||||
.then(response => response.clone().json())
|
||||
.then((result) => {
|
||||
this.setState({ embed: result });
|
||||
}).catch((error) => {
|
||||
if (error.name === 'AbortError')
|
||||
return;
|
||||
this.setState({ embed: 'error' });
|
||||
});
|
||||
}
|
||||
|
||||
wrapInLink(contents, textOnly = false, unfold = false, unfoldEmbed = null, embedContainer = null, flushPadding = false, noOp = false) {
|
||||
const { style, tall = false } = this.props;
|
||||
const maxWidth = tall ? '100%' : 'min(500px, 100%)';
|
||||
return (
|
||||
<Box borderRadius={1} backgroundColor="washedGray" maxWidth={maxWidth}>
|
||||
<Row
|
||||
alignItems="center"
|
||||
gapX={1}
|
||||
>
|
||||
{ textOnly && (<Icon ml={2} display="block" icon="ArrowExternal" />)}
|
||||
{ !textOnly && unfoldEmbed && (
|
||||
<Icon
|
||||
ml={2}
|
||||
display='block'
|
||||
onClick={unfoldEmbed}
|
||||
icon={unfold ? 'TriangleSouth' : 'TriangleEast'}
|
||||
/>
|
||||
)}
|
||||
<BaseAnchor
|
||||
display="flex"
|
||||
p={flushPadding ? 0 : 2}
|
||||
onClick={(e) => {
|
||||
noOp ? e.preventDefault() : e.stopPropagation();
|
||||
}}
|
||||
href={this.props.url}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
minWidth={0}
|
||||
width={textOnly ? 'calc(100% - 24px)' : 'fit-content'}
|
||||
maxWidth={maxWidth}
|
||||
style={{ color: 'inherit', textDecoration: 'none', ...style }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
cursor={noOp ? 'default' : 'pointer'}
|
||||
>
|
||||
{contents}
|
||||
</BaseAnchor>
|
||||
</Row>
|
||||
{embedContainer}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
onError(e: Event) {
|
||||
this.restore();
|
||||
this.setState({ noCors: true });
|
||||
}
|
||||
|
||||
toggleArrow() {
|
||||
this.setState({ showArrow: !this.state.showArrow });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
remoteContentPolicy,
|
||||
url,
|
||||
text,
|
||||
transcluded,
|
||||
renderUrl = true,
|
||||
imageProps = {},
|
||||
audioProps = {},
|
||||
videoProps = {},
|
||||
oembedProps = {},
|
||||
textProps = {},
|
||||
style = {},
|
||||
...props
|
||||
} = this.props;
|
||||
const { onLoad } = this;
|
||||
const { noCors } = this.state;
|
||||
const isImage = IMAGE_REGEX.test(url);
|
||||
const isAudio = AUDIO_REGEX.test(url);
|
||||
const isVideo = VIDEO_REGEX.test(url);
|
||||
|
||||
const isTranscluded = () => {
|
||||
return transcluded;
|
||||
};
|
||||
|
||||
if (isImage && remoteContentPolicy.imageShown) {
|
||||
return this.wrapInLink(
|
||||
<Box
|
||||
position='relative'
|
||||
onMouseEnter={this.toggleArrow}
|
||||
onMouseLeave={this.toggleArrow}
|
||||
>
|
||||
<BaseAnchor
|
||||
position='absolute'
|
||||
top={2}
|
||||
right={2}
|
||||
display={this.state.showArrow ? 'block' : 'none'}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
href={url}
|
||||
>
|
||||
<Box
|
||||
backgroundColor='white'
|
||||
padding={2}
|
||||
borderRadius='50%'
|
||||
display='flex'
|
||||
>
|
||||
<Icon icon='ArrowNorthEast' />
|
||||
</Box>
|
||||
</BaseAnchor>
|
||||
<BaseImage
|
||||
{...(noCors ? {} : { crossOrigin: 'anonymous' })}
|
||||
referrerPolicy='no-referrer'
|
||||
flexShrink={0}
|
||||
src={url}
|
||||
style={style}
|
||||
onLoad={onLoad}
|
||||
onError={this.onError}
|
||||
height='100%'
|
||||
width='100%'
|
||||
objectFit='contain'
|
||||
borderRadius={2}
|
||||
{...imageProps}
|
||||
{...props}
|
||||
/>
|
||||
</Box>,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
isTranscluded()
|
||||
);
|
||||
} else if (isAudio && remoteContentPolicy.audioShown) {
|
||||
return (
|
||||
<>
|
||||
{renderUrl
|
||||
? this.wrapInLink(
|
||||
<TruncatedText {...textProps}>{url}</TruncatedText>,
|
||||
false,
|
||||
this.state.unfold,
|
||||
this.unfoldEmbed,
|
||||
<audio
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
controls
|
||||
className={this.state.unfold ? 'db' : 'dn'}
|
||||
src={url}
|
||||
style={style}
|
||||
onLoad={onLoad}
|
||||
objectFit="contain"
|
||||
height="100%"
|
||||
width="100%"
|
||||
{...audioProps}
|
||||
{...props}
|
||||
/>)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
} else if (isVideo && remoteContentPolicy.videoShown) {
|
||||
return (
|
||||
<>
|
||||
{renderUrl
|
||||
? this.wrapInLink(
|
||||
<TruncatedText {...textProps}>{url}</TruncatedText>,
|
||||
false,
|
||||
this.state.unfold,
|
||||
this.unfoldEmbed,
|
||||
<video
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
controls
|
||||
className={this.state.unfold ? 'db' : 'dn pa2'}
|
||||
src={url}
|
||||
style={style}
|
||||
onLoad={onLoad}
|
||||
objectFit="contain"
|
||||
height="100%"
|
||||
width="100%"
|
||||
{...videoProps}
|
||||
{...props}
|
||||
/>)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
} else if (this.isOembed && remoteContentPolicy.oembedShown) {
|
||||
if (!this.state.embed || this.state.embed?.html === '') {
|
||||
this.loadOembed();
|
||||
}
|
||||
const renderEmbed = !(this.state.embed !== 'error' && this.state.embed?.html);
|
||||
const embed = <Box
|
||||
mb={2}
|
||||
width='100%'
|
||||
flexShrink={0}
|
||||
display={this.state.unfold ? 'block' : 'none'}
|
||||
className='embed-container'
|
||||
style={style}
|
||||
onLoad={this.onLoad}
|
||||
{...oembedProps}
|
||||
{...props}
|
||||
>
|
||||
<TruncatedText
|
||||
display={(renderUrl && this.state.embed?.title && this.state.embed.title !== url) ? 'inline-block' : 'none'}
|
||||
fontWeight='bold' width='100%'
|
||||
>
|
||||
{this.state.embed?.title}
|
||||
</TruncatedText>
|
||||
{this.state.embed && this.state.embed.html && this.state.unfold
|
||||
? <EmbedContainer markup={this.state.embed.html}>
|
||||
<div className="embed-container" ref={(el) => {
|
||||
this.onLoad();
|
||||
this.containerRef = el;
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: this.state.embed.html }}
|
||||
></div>
|
||||
</EmbedContainer>
|
||||
: null}
|
||||
</Box>;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{renderUrl
|
||||
? this.wrapInLink(
|
||||
<TruncatedText {...textProps}>{url}</TruncatedText>,
|
||||
renderEmbed,
|
||||
this.state.unfold,
|
||||
this.unfoldEmbed,
|
||||
embed
|
||||
) : embed}
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return renderUrl
|
||||
? this.wrapInLink(<TruncatedText {...textProps}>{text || url}</TruncatedText>, true)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withState(withVirtual(RemoteContent), [[useSettingsState, ['remoteContentPolicy']]]) as React.ComponentType<Omit<RemoteContentProps, 'save' | 'restore' | 'remoteContentPolicy'> & { ref?: any }>;
|
@ -15,8 +15,7 @@ import {
|
||||
allSystemStyle,
|
||||
Icon,
|
||||
Row,
|
||||
Col,
|
||||
Center
|
||||
Col
|
||||
} from '@tlon/indigo-react';
|
||||
|
||||
import { TruncatedText } from '~/views/components/TruncatedText';
|
||||
@ -144,6 +143,15 @@ const EmbedContainer = styled(UnstyledEmbedContainer)`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const EmbedBox = styled.div`
|
||||
& > * {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-width: 100% !important;
|
||||
max-height: 100% !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export function RemoteContentPermalinkEmbed(props: {
|
||||
reference: ReferenceContent;
|
||||
}) {
|
||||
@ -210,22 +218,28 @@ function RemoteContentPermalinkEmbedBase(props: {
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
type RemoteContentOembedProps = {
|
||||
renderUrl?: boolean;
|
||||
thumbnail?: boolean;
|
||||
} & RemoteContentEmbedProps &
|
||||
PropFunc<typeof Box>;
|
||||
|
||||
export function RemoteContentOembed(
|
||||
props: {
|
||||
renderUrl?: boolean;
|
||||
thumbnail?: boolean;
|
||||
} & RemoteContentEmbedProps &
|
||||
PropFunc<typeof Box>
|
||||
) {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
export const RemoteContentOembed = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
RemoteContentOembedProps
|
||||
>((props, ref) => {
|
||||
const ourRef = useRef<HTMLDivElement>();
|
||||
const setRef = useCallback((r: HTMLDivElement | null) => {
|
||||
typeof ref === 'function' ? ref(r) : (ref.current = r);
|
||||
ourRef.current = r;
|
||||
}, []);
|
||||
const { url, renderUrl = false, thumbnail = false, ...rest } = props;
|
||||
const [embed, setEmbed] = useState<any>();
|
||||
|
||||
useEffect(() => {
|
||||
const getEmbed = async () => {
|
||||
try {
|
||||
const { width, height } = ref.current.getBoundingClientRect();
|
||||
const { width, height } = ourRef.current.getBoundingClientRect();
|
||||
|
||||
const oembed = await extract(url, {
|
||||
maxheight: Math.floor(height),
|
||||
@ -240,9 +254,13 @@ export function RemoteContentOembed(
|
||||
getEmbed();
|
||||
}, [url]);
|
||||
|
||||
if (!renderUrl && !embed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Col
|
||||
ref={ref}
|
||||
ref={setRef}
|
||||
mb={2}
|
||||
width="100%"
|
||||
flexShrink={0}
|
||||
@ -270,18 +288,14 @@ export function RemoteContentOembed(
|
||||
/>
|
||||
) : !thumbnail && embed?.html ? (
|
||||
<EmbedContainer markup={embed.html}>
|
||||
<Center
|
||||
height="100%"
|
||||
width="100%"
|
||||
dangerouslySetInnerHTML={{ __html: embed.html }}
|
||||
></Center>
|
||||
<EmbedBox dangerouslySetInnerHTML={{ __html: embed.html }}></EmbedBox>
|
||||
</EmbedContainer>
|
||||
) : (
|
||||
) : renderUrl ? (
|
||||
<RemoteContentEmbedFallback url={url} />
|
||||
)}
|
||||
) : null}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export function RemoteContentEmbedFallback(props: RemoteContentEmbedProps) {
|
||||
const { url } = props;
|
||||
|
@ -1,20 +1,23 @@
|
||||
import { BaseAnchor, Box, Icon, Row } from '@tlon/indigo-react';
|
||||
import { hasProvider } from 'oembed-parser';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import EmbedContainer from 'react-oembed-container';
|
||||
import { VirtualContextProps, withVirtual } from '~/logic/lib/virtualContext';
|
||||
import withState from '~/logic/lib/withState';
|
||||
import React from 'react';
|
||||
import useSettingsState from '~/logic/state/settings';
|
||||
import { RemoteContentPolicy } from '~/types/local-update';
|
||||
import { RemoteContentAudioEmbed, RemoteContentImageEmbed, RemoteContentVideoEmbed } from './embed';
|
||||
import {
|
||||
RemoteContentAudioEmbed,
|
||||
RemoteContentImageEmbed,
|
||||
RemoteContentOembed,
|
||||
RemoteContentVideoEmbed
|
||||
} from './embed';
|
||||
import { TruncatedText } from '~/views/components/TruncatedText';
|
||||
import { RemoteContentWrapper } from './wrapper';
|
||||
|
||||
type RemoteContentProps = VirtualContextProps & {
|
||||
export interface RemoteContentProps {
|
||||
url: string;
|
||||
text?: string;
|
||||
unfold?: boolean;
|
||||
renderUrl?: boolean;
|
||||
remoteContentPolicy?: RemoteContentPolicy;
|
||||
embedRef?: (el: HTMLDivElement | null ) => void;
|
||||
imageProps?: any;
|
||||
audioProps?: any;
|
||||
videoProps?: any;
|
||||
@ -26,284 +29,82 @@ type RemoteContentProps = VirtualContextProps & {
|
||||
tall?: boolean;
|
||||
}
|
||||
|
||||
interface RemoteContentState {
|
||||
unfold: boolean;
|
||||
embed: any | undefined;
|
||||
noCors: boolean;
|
||||
showArrow: boolean;
|
||||
}
|
||||
|
||||
export const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
|
||||
export const IMAGE_REGEX = new RegExp(
|
||||
/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i
|
||||
);
|
||||
export const AUDIO_REGEX = new RegExp(/(mp3|wav|ogg|m4a)$/i);
|
||||
export const VIDEO_REGEX = new RegExp(/(mov|mp4|ogv)$/i);
|
||||
|
||||
class RemoteContent extends Component<RemoteContentProps, RemoteContentState> {
|
||||
private fetchController: AbortController | undefined;
|
||||
containerRef: HTMLDivElement | null = null;
|
||||
private saving = false;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
unfold: props.unfold || false,
|
||||
embed: undefined,
|
||||
noCors: false,
|
||||
showArrow: false
|
||||
};
|
||||
this.unfoldEmbed = this.unfoldEmbed.bind(this);
|
||||
this.loadOembed = this.loadOembed.bind(this);
|
||||
this.wrapInLink = this.wrapInLink.bind(this);
|
||||
this.onError = this.onError.bind(this);
|
||||
this.toggleArrow = this.toggleArrow.bind(this);
|
||||
}
|
||||
const emptyRef = () => {};
|
||||
export function RemoteContent(props: RemoteContentProps) {
|
||||
const {
|
||||
url,
|
||||
embedRef = emptyRef,
|
||||
text,
|
||||
transcluded,
|
||||
tall = false,
|
||||
renderUrl = true,
|
||||
imageProps = {},
|
||||
audioProps = {},
|
||||
videoProps = {},
|
||||
textProps = {}
|
||||
} = props;
|
||||
|
||||
save = () => {
|
||||
if(this.saving) {
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
this.props.save();
|
||||
const remoteContentPolicy = useSettingsState(s => s.remoteContentPolicy);
|
||||
const isImage = IMAGE_REGEX.test(url);
|
||||
const isAudio = AUDIO_REGEX.test(url);
|
||||
const isVideo = VIDEO_REGEX.test(url);
|
||||
const isOembed = hasProvider(url);
|
||||
const wrapperProps = {
|
||||
url,
|
||||
tall,
|
||||
embedOnly: !renderUrl
|
||||
};
|
||||
|
||||
restore = () => {
|
||||
this.saving = false;
|
||||
this.props.restore();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if(this.saving) {
|
||||
this.restore();
|
||||
}
|
||||
if (this.fetchController) {
|
||||
this.fetchController.abort();
|
||||
}
|
||||
}
|
||||
|
||||
unfoldEmbed(event: Event) {
|
||||
event.stopPropagation();
|
||||
let unfoldState = this.state.unfold;
|
||||
unfoldState = !unfoldState;
|
||||
this.save();
|
||||
this.setState({ unfold: unfoldState });
|
||||
requestAnimationFrame(() => {
|
||||
this.restore();
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if(prevState.embed !== this.state.embed) {
|
||||
// console.log('remotecontent: restoring');
|
||||
// prevProps.shiftLayout.restore();
|
||||
}
|
||||
const { url } = this.props;
|
||||
if(url !== prevProps.url && (IMAGE_REGEX.test(url) || AUDIO_REGEX.test(url) || VIDEO_REGEX.test(url))) {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
onLoad = () => {
|
||||
window.requestAnimationFrame(() => {
|
||||
const { restore } = this;
|
||||
restore();
|
||||
});
|
||||
}
|
||||
|
||||
loadOembed() {
|
||||
this.fetchController = new AbortController();
|
||||
fetch(`https://noembed.com/embed?url=${this.props.url}`, {
|
||||
signal: this.fetchController.signal
|
||||
})
|
||||
.then(response => response.clone().json())
|
||||
.then((result) => {
|
||||
this.setState({ embed: result });
|
||||
}).catch((error) => {
|
||||
if (error.name === 'AbortError')
|
||||
return;
|
||||
this.setState({ embed: 'error' });
|
||||
});
|
||||
}
|
||||
|
||||
wrapInLink(contents, textOnly = false, unfold = false, unfoldEmbed = null, embedContainer = null, flushPadding = false, noOp = false) {
|
||||
const { style, tall = false } = this.props;
|
||||
const maxWidth = tall ? '100%' : 'min(500px, 100%)';
|
||||
if (isImage && remoteContentPolicy.imageShown) {
|
||||
return (
|
||||
<Box borderRadius={1} backgroundColor="washedGray" maxWidth={maxWidth}>
|
||||
<Row
|
||||
alignItems="center"
|
||||
gapX={1}
|
||||
>
|
||||
{ textOnly && (<Icon ml={2} display="block" icon="ArrowExternal" />)}
|
||||
{ !textOnly && unfoldEmbed && (
|
||||
<Icon
|
||||
ml={2}
|
||||
display='block'
|
||||
onClick={unfoldEmbed}
|
||||
icon={unfold ? 'TriangleSouth' : 'TriangleEast'}
|
||||
/>
|
||||
)}
|
||||
<BaseAnchor
|
||||
display="flex"
|
||||
p={flushPadding ? 0 : 2}
|
||||
onClick={(e) => {
|
||||
noOp ? e.preventDefault() : e.stopPropagation();
|
||||
}}
|
||||
href={this.props.url}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
minWidth={0}
|
||||
width={textOnly ? 'calc(100% - 24px)' : 'fit-content'}
|
||||
maxWidth={maxWidth}
|
||||
style={{ color: 'inherit', textDecoration: 'none', ...style }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
cursor={noOp ? 'default' : 'pointer'}
|
||||
>
|
||||
{contents}
|
||||
</BaseAnchor>
|
||||
</Row>
|
||||
{embedContainer}
|
||||
</Box>
|
||||
<RemoteContentWrapper {...wrapperProps} noOp={transcluded}>
|
||||
<RemoteContentImageEmbed url={url} {...imageProps} />
|
||||
</RemoteContentWrapper>
|
||||
);
|
||||
} else if (isAudio && remoteContentPolicy.audioShown) {
|
||||
return (
|
||||
<RemoteContentWrapper
|
||||
{...wrapperProps}
|
||||
detail={<RemoteContentAudioEmbed url={url} {...audioProps} />}
|
||||
>
|
||||
<TruncatedText {...textProps}>{url}</TruncatedText>
|
||||
</RemoteContentWrapper>
|
||||
);
|
||||
} else if (isVideo && remoteContentPolicy.videoShown) {
|
||||
return (
|
||||
<RemoteContentWrapper
|
||||
{...wrapperProps}
|
||||
detail={
|
||||
<RemoteContentVideoEmbed url={url} {...videoProps} />
|
||||
}
|
||||
>
|
||||
<TruncatedText {...textProps}>{url}</TruncatedText>
|
||||
</RemoteContentWrapper>
|
||||
);
|
||||
} else if (isOembed && remoteContentPolicy.oembedShown) {
|
||||
return (
|
||||
<RemoteContentWrapper
|
||||
{...wrapperProps}
|
||||
detail={(
|
||||
<RemoteContentOembed ref={embedRef} url={url} renderUrl={renderUrl} />
|
||||
)}
|
||||
>
|
||||
<TruncatedText {...textProps}>{url}</TruncatedText>
|
||||
</RemoteContentWrapper>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<RemoteContentWrapper {...wrapperProps}>
|
||||
<TruncatedText {...textProps}>{text || url}</TruncatedText>
|
||||
</RemoteContentWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
onError(e: Event) {
|
||||
this.restore();
|
||||
this.setState({ noCors: true });
|
||||
}
|
||||
|
||||
toggleArrow() {
|
||||
this.setState({ showArrow: !this.state.showArrow });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
remoteContentPolicy,
|
||||
url,
|
||||
text,
|
||||
transcluded,
|
||||
renderUrl = true,
|
||||
imageProps = {},
|
||||
audioProps = {},
|
||||
videoProps = {},
|
||||
oembedProps = {},
|
||||
textProps = {},
|
||||
style = {},
|
||||
...props
|
||||
} = this.props;
|
||||
const { noCors } = this.state;
|
||||
const isImage = IMAGE_REGEX.test(url);
|
||||
const isAudio = AUDIO_REGEX.test(url);
|
||||
const isVideo = VIDEO_REGEX.test(url);
|
||||
const isOembed = hasProvider(url);
|
||||
|
||||
const isTranscluded = () => {
|
||||
return transcluded;
|
||||
};
|
||||
|
||||
if (isImage && remoteContentPolicy.imageShown) {
|
||||
return this.wrapInLink(
|
||||
<RemoteContentImageEmbed
|
||||
url={url}
|
||||
noCors={noCors}
|
||||
{...imageProps}
|
||||
/>,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
isTranscluded()
|
||||
);
|
||||
} else if (isAudio && remoteContentPolicy.audioShown) {
|
||||
return (
|
||||
<>
|
||||
{renderUrl
|
||||
? this.wrapInLink(
|
||||
<TruncatedText {...textProps}>{url}</TruncatedText>,
|
||||
false,
|
||||
this.state.unfold,
|
||||
this.unfoldEmbed,
|
||||
<RemoteContentAudioEmbed
|
||||
url={url}
|
||||
noCors={noCors}
|
||||
{...audioProps}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
} else if (isVideo && remoteContentPolicy.videoShown) {
|
||||
return (
|
||||
<>
|
||||
{renderUrl
|
||||
? this.wrapInLink(
|
||||
<TruncatedText {...textProps}>{url}</TruncatedText>,
|
||||
false,
|
||||
this.state.unfold,
|
||||
this.unfoldEmbed,
|
||||
<RemoteContentVideoEmbed
|
||||
url={url}
|
||||
noCors={noCors}
|
||||
{...videoProps}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
} else if (isOembed && remoteContentPolicy.oembedShown) {
|
||||
if (!this.state.embed || this.state.embed?.html === '') {
|
||||
this.loadOembed();
|
||||
}
|
||||
const renderEmbed = !(this.state.embed !== 'error' && this.state.embed?.html);
|
||||
const embed = (
|
||||
<Box
|
||||
mb={2}
|
||||
width='100%'
|
||||
flexShrink={0}
|
||||
display={this.state.unfold ? 'block' : 'none'}
|
||||
className='embed-container'
|
||||
style={style}
|
||||
onLoad={this.onLoad}
|
||||
{...oembedProps}
|
||||
{...props}
|
||||
>
|
||||
<TruncatedText
|
||||
display={(renderUrl && this.state.embed?.title && this.state.embed.title !== url) ? 'inline-block' : 'none'}
|
||||
fontWeight='bold' width='100%'
|
||||
>
|
||||
{this.state.embed?.title}
|
||||
</TruncatedText>
|
||||
{this.state.embed && this.state.embed.html && this.state.unfold
|
||||
? <EmbedContainer markup={this.state.embed.html}>
|
||||
<div className="embed-container" ref={(el) => {
|
||||
this.onLoad();
|
||||
this.containerRef = el;
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: this.state.embed.html }}
|
||||
></div>
|
||||
</EmbedContainer>
|
||||
: null}
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{renderUrl
|
||||
? this.wrapInLink(
|
||||
<TruncatedText {...textProps}>{url}</TruncatedText>,
|
||||
renderEmbed,
|
||||
this.state.unfold,
|
||||
this.unfoldEmbed,
|
||||
embed
|
||||
) : embed}
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return renderUrl
|
||||
? this.wrapInLink(<TruncatedText {...textProps}>{text || url}</TruncatedText>, true)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withState(withVirtual(RemoteContent), [[useSettingsState, ['remoteContentPolicy']]]) as React.ComponentType<Omit<RemoteContentProps, 'save' | 'restore' | 'remoteContentPolicy'> & { ref?: any }>;
|
||||
export default React.memo(RemoteContent);
|
||||
|
78
pkg/interface/src/views/components/RemoteContent/wrapper.tsx
Normal file
78
pkg/interface/src/views/components/RemoteContent/wrapper.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import React, { MouseEvent, useCallback, useState } from 'react';
|
||||
import { Row, Box, Icon, BaseAnchor } from '@tlon/indigo-react';
|
||||
|
||||
interface RemoteContentWrapperProps {
|
||||
children: JSX.Element;
|
||||
url: string;
|
||||
detail?: JSX.Element;
|
||||
flushPadding?: boolean;
|
||||
noOp?: boolean;
|
||||
tall?: boolean;
|
||||
embedOnly?: boolean;
|
||||
}
|
||||
|
||||
export function RemoteContentWrapper(props: RemoteContentWrapperProps) {
|
||||
const {
|
||||
url,
|
||||
children,
|
||||
detail = null,
|
||||
flushPadding = false,
|
||||
noOp = false,
|
||||
tall = false,
|
||||
embedOnly = false
|
||||
} = props;
|
||||
|
||||
const [unfold, setUnfold] = useState(false);
|
||||
const toggleUnfold = useCallback(() => {
|
||||
setUnfold(s => !s);
|
||||
}, []);
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
noOp ? e.preventDefault() : e.stopPropagation();
|
||||
},
|
||||
[noOp]
|
||||
);
|
||||
|
||||
const maxWidth = tall ? '100%' : 'min(500px, 100%)';
|
||||
|
||||
if(embedOnly) {
|
||||
return detail || children;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box borderRadius={1} backgroundColor="washedGray" maxWidth={maxWidth}>
|
||||
<Row alignItems="center" gapX={1}>
|
||||
{!detail ? (
|
||||
<Icon ml={2} display="block" icon="ArrowExternal" />
|
||||
) : (
|
||||
<Icon
|
||||
ml={2}
|
||||
display="block"
|
||||
onClick={toggleUnfold}
|
||||
icon={unfold ? 'TriangleSouth' : 'TriangleEast'}
|
||||
/>
|
||||
)}
|
||||
<BaseAnchor
|
||||
display="flex"
|
||||
p={flushPadding ? 0 : 2}
|
||||
onClick={onClick}
|
||||
href={url}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
minWidth={0}
|
||||
width={!detail ? 'calc(100% - 24px)' : 'fit-content'}
|
||||
maxWidth={maxWidth}
|
||||
style={{ color: 'inherit', textDecoration: 'none' }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
cursor={noOp ? 'default' : 'pointer'}
|
||||
>
|
||||
{children}
|
||||
</BaseAnchor>
|
||||
</Row>
|
||||
{detail}
|
||||
</Box>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user