Merge branch 'embedded-status' into 'develop'
Native status embeds from Soapbox See merge request soapbox-pub/soapbox-fe!1730
This commit is contained in:
commit
6ee6c8aa44
|
@ -1,5 +1,8 @@
|
||||||
import loadPolyfills from './soapbox/load_polyfills';
|
import loadPolyfills from './soapbox/load_polyfills';
|
||||||
|
|
||||||
|
// Load iframe event listener
|
||||||
|
require('./soapbox/iframe');
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
require.context('./images/', true);
|
require.context('./images/', true);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
interface ISafeEmbed {
|
||||||
|
/** Styles for the outer frame element. */
|
||||||
|
className?: string,
|
||||||
|
/** Space-separate list of restrictions to ALLOW for the iframe. */
|
||||||
|
sandbox?: string,
|
||||||
|
/** Unique title for the iframe. */
|
||||||
|
title: string,
|
||||||
|
/** HTML body to embed. */
|
||||||
|
html?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Safely embeds arbitrary HTML content on the page (by putting it in an iframe). */
|
||||||
|
const SafeEmbed: React.FC<ISafeEmbed> = ({
|
||||||
|
className,
|
||||||
|
sandbox,
|
||||||
|
title,
|
||||||
|
html,
|
||||||
|
}) => {
|
||||||
|
const iframe = useRef<HTMLIFrameElement>(null);
|
||||||
|
const [height, setHeight] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
const handleMessage = useCallback((e: MessageEvent) => {
|
||||||
|
if (e.data?.type === 'setHeight') {
|
||||||
|
setHeight(e.data?.height);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const iframeDocument = iframe.current?.contentWindow?.document;
|
||||||
|
|
||||||
|
if (iframeDocument && html) {
|
||||||
|
iframeDocument.open();
|
||||||
|
iframeDocument.write(html);
|
||||||
|
iframeDocument.close();
|
||||||
|
iframeDocument.body.style.margin = '0';
|
||||||
|
|
||||||
|
iframe.current?.contentWindow?.addEventListener('message', handleMessage);
|
||||||
|
|
||||||
|
const innerFrame = iframeDocument.querySelector('iframe');
|
||||||
|
if (innerFrame) {
|
||||||
|
innerFrame.width = '100%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
iframe.current?.contentWindow?.removeEventListener('message', handleMessage);
|
||||||
|
};
|
||||||
|
}, [iframe.current, html]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
ref={iframe}
|
||||||
|
className={className}
|
||||||
|
sandbox={sandbox}
|
||||||
|
height={height}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SafeEmbed;
|
|
@ -18,6 +18,7 @@ import EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper';
|
||||||
import StatusActionButton from 'soapbox/components/status-action-button';
|
import StatusActionButton from 'soapbox/components/status-action-button';
|
||||||
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
|
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
|
||||||
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
import { isLocal } from 'soapbox/utils/accounts';
|
||||||
import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji_reacts';
|
import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji_reacts';
|
||||||
|
|
||||||
import type { Menu } from 'soapbox/components/dropdown_menu';
|
import type { Menu } from 'soapbox/components/dropdown_menu';
|
||||||
|
@ -362,7 +363,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
icon: require('@tabler/icons/link.svg'),
|
icon: require('@tabler/icons/link.svg'),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (features.embeds) {
|
if (features.embeds && isLocal(status.account as Account)) {
|
||||||
menu.push({
|
menu.push({
|
||||||
text: intl.formatMessage(messages.embed),
|
text: intl.formatMessage(messages.embed),
|
||||||
action: handleEmbed,
|
action: handleEmbed,
|
||||||
|
|
|
@ -18,7 +18,7 @@ import StatusActionBar from './status-action-bar';
|
||||||
import StatusMedia from './status-media';
|
import StatusMedia from './status-media';
|
||||||
import StatusReplyMentions from './status-reply-mentions';
|
import StatusReplyMentions from './status-reply-mentions';
|
||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
import { HStack, Text } from './ui';
|
import { Card, HStack, Text } from './ui';
|
||||||
|
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
import type {
|
import type {
|
||||||
|
@ -47,7 +47,9 @@ export interface IStatus {
|
||||||
featured?: boolean,
|
featured?: boolean,
|
||||||
hideActionBar?: boolean,
|
hideActionBar?: boolean,
|
||||||
hoverable?: boolean,
|
hoverable?: boolean,
|
||||||
|
variant?: 'default' | 'rounded',
|
||||||
withDismiss?: boolean,
|
withDismiss?: boolean,
|
||||||
|
accountAction?: React.ReactElement,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Status: React.FC<IStatus> = (props) => {
|
const Status: React.FC<IStatus> = (props) => {
|
||||||
|
@ -64,8 +66,10 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
unread,
|
unread,
|
||||||
group,
|
group,
|
||||||
hideActionBar,
|
hideActionBar,
|
||||||
|
variant = 'rounded',
|
||||||
withDismiss,
|
withDismiss,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
@ -293,6 +297,8 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
|
|
||||||
const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`;
|
const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`;
|
||||||
|
|
||||||
|
const accountAction = props.accountAction || reblogElement;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers} data-testid='status'>
|
<HotKeys handlers={handlers} data-testid='status'>
|
||||||
<div
|
<div
|
||||||
|
@ -316,8 +322,10 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<Card
|
||||||
|
variant={variant}
|
||||||
className={classNames('status__wrapper', `status-${actualStatus.visibility}`, {
|
className={classNames('status__wrapper', `status-${actualStatus.visibility}`, {
|
||||||
|
'py-6 sm:p-5': variant === 'rounded',
|
||||||
'status-reply': !!status.in_reply_to_id,
|
'status-reply': !!status.in_reply_to_id,
|
||||||
muted,
|
muted,
|
||||||
read: unread === false,
|
read: unread === false,
|
||||||
|
@ -332,8 +340,8 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
id={String(actualStatus.getIn(['account', 'id']))}
|
id={String(actualStatus.getIn(['account', 'id']))}
|
||||||
timestamp={actualStatus.created_at}
|
timestamp={actualStatus.created_at}
|
||||||
timestampUrl={statusUrl}
|
timestampUrl={statusUrl}
|
||||||
action={reblogElement}
|
action={accountAction}
|
||||||
hideActions={!reblogElement}
|
hideActions={!accountAction}
|
||||||
showEdit={!!actualStatus.edited_at}
|
showEdit={!!actualStatus.edited_at}
|
||||||
showProfileHoverCard={hoverable}
|
showProfileHoverCard={hoverable}
|
||||||
withLinkToProfile={hoverable}
|
withLinkToProfile={hoverable}
|
||||||
|
@ -376,7 +384,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,7 +18,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
interface ICard {
|
interface ICard {
|
||||||
/** The type of card. */
|
/** The type of card. */
|
||||||
variant?: 'rounded',
|
variant?: 'default' | 'rounded',
|
||||||
/** Card size preset. */
|
/** Card size preset. */
|
||||||
size?: 'md' | 'lg' | 'xl',
|
size?: 'md' | 'lg' | 'xl',
|
||||||
/** Extra classnames for the <div> element. */
|
/** Extra classnames for the <div> element. */
|
||||||
|
@ -28,12 +28,11 @@ interface ICard {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An opaque backdrop to hold a collection of related elements. */
|
/** An opaque backdrop to hold a collection of related elements. */
|
||||||
const Card = React.forwardRef<HTMLDivElement, ICard>(({ children, variant, size = 'md', className, ...filteredProps }, ref): JSX.Element => (
|
const Card = React.forwardRef<HTMLDivElement, ICard>(({ children, variant = 'default', size = 'md', className, ...filteredProps }, ref): JSX.Element => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...filteredProps}
|
{...filteredProps}
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'space-y-4': true,
|
|
||||||
'bg-white dark:bg-primary-900 text-gray-900 dark:text-gray-100 shadow-lg dark:shadow-none overflow-hidden': variant === 'rounded',
|
'bg-white dark:bg-primary-900 text-gray-900 dark:text-gray-100 shadow-lg dark:shadow-none overflow-hidden': variant === 'rounded',
|
||||||
[sizes[size]]: variant === 'rounded',
|
[sizes[size]]: variant === 'rounded',
|
||||||
}, className)}
|
}, className)}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import GdprBanner from 'soapbox/components/gdpr-banner';
|
||||||
import Helmet from 'soapbox/components/helmet';
|
import Helmet from 'soapbox/components/helmet';
|
||||||
import LoadingScreen from 'soapbox/components/loading-screen';
|
import LoadingScreen from 'soapbox/components/loading-screen';
|
||||||
import AuthLayout from 'soapbox/features/auth_layout';
|
import AuthLayout from 'soapbox/features/auth_layout';
|
||||||
|
import EmbeddedStatus from 'soapbox/features/embedded-status';
|
||||||
import PublicLayout from 'soapbox/features/public_layout';
|
import PublicLayout from 'soapbox/features/public_layout';
|
||||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||||
import {
|
import {
|
||||||
|
@ -148,6 +149,12 @@ const SoapboxMount = () => {
|
||||||
<Route path='/verify' component={AuthLayout} />
|
<Route path='/verify' component={AuthLayout} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path='/embed/:statusId'
|
||||||
|
render={(props) => <EmbeddedStatus params={props.match.params} />}
|
||||||
|
/>
|
||||||
|
<Redirect from='/@:username/:statusId/embed' to='/embed/:statusId' />
|
||||||
|
|
||||||
<Route path='/reset-password' component={AuthLayout} />
|
<Route path='/reset-password' component={AuthLayout} />
|
||||||
<Route path='/edit-password' component={AuthLayout} />
|
<Route path='/edit-password' component={AuthLayout} />
|
||||||
<Route path='/invite/:token' component={AuthLayout} />
|
<Route path='/invite/:token' component={AuthLayout} />
|
||||||
|
|
|
@ -53,7 +53,7 @@ const Ad: React.FC<IAd> = ({ card, impression }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<Card className='p-5' variant='rounded'>
|
<Card className='py-6 sm:p-5' variant='rounded'>
|
||||||
<Stack space={4}>
|
<Stack space={4}>
|
||||||
<HStack alignItems='center' space={3}>
|
<HStack alignItems='center' space={3}>
|
||||||
<Avatar src={instance.thumbnail} size={42} />
|
<Avatar src={instance.thumbnail} size={42} />
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { fetchStatus } from 'soapbox/actions/statuses';
|
||||||
|
import MissingIndicator from 'soapbox/components/missing_indicator';
|
||||||
|
import SiteLogo from 'soapbox/components/site-logo';
|
||||||
|
import Status from 'soapbox/components/status';
|
||||||
|
import { Spinner } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
import { iframeId } from 'soapbox/iframe';
|
||||||
|
import { makeGetStatus } from 'soapbox/selectors';
|
||||||
|
|
||||||
|
interface IEmbeddedStatus {
|
||||||
|
params: {
|
||||||
|
statusId: string,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatus = makeGetStatus();
|
||||||
|
|
||||||
|
/** Status to be presented in an iframe for embeds on external websites. */
|
||||||
|
const EmbeddedStatus: React.FC<IEmbeddedStatus> = ({ params }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const history = useHistory();
|
||||||
|
const status = useAppSelector(state => getStatus(state, { id: params.statusId }));
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Prevent navigation for UX and security.
|
||||||
|
// https://stackoverflow.com/a/71531211
|
||||||
|
history.block();
|
||||||
|
|
||||||
|
dispatch(fetchStatus(params.statusId))
|
||||||
|
.then(() => setLoading(false))
|
||||||
|
.catch(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'setHeight',
|
||||||
|
id: iframeId,
|
||||||
|
height: document.getElementsByTagName('html')[0].scrollHeight,
|
||||||
|
}, '*');
|
||||||
|
}, [status, loading]);
|
||||||
|
|
||||||
|
const logo = (
|
||||||
|
<div className='flex align-middle justify-center ml-4'>
|
||||||
|
<SiteLogo className='max-h-[20px] max-w-[112px]' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderInner = () => {
|
||||||
|
if (loading) {
|
||||||
|
return <Spinner />;
|
||||||
|
} else if (status) {
|
||||||
|
return <Status status={status} accountAction={logo} variant='default' />;
|
||||||
|
} else {
|
||||||
|
return <MissingIndicator nested />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className='block bg-white dark:bg-primary-900'
|
||||||
|
href={status?.url || '#'}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
<div className='p-4 sm:p-6 max-w-3xl pointer-events-none'>
|
||||||
|
{renderInner()}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmbeddedStatus;
|
|
@ -60,7 +60,7 @@ const Settings = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.settings)} transparent withHeader={false}>
|
<Column label={intl.formatMessage(messages.settings)} transparent withHeader={false}>
|
||||||
<Card variant='rounded'>
|
<Card className='space-y-4' variant='rounded'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle title={intl.formatMessage(messages.profile)} />
|
<CardTitle title={intl.formatMessage(messages.profile)} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
|
@ -380,9 +380,9 @@ const Thread: React.FC<IThread> = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PendingStatus
|
<PendingStatus
|
||||||
className='thread__status'
|
|
||||||
key={id}
|
key={id}
|
||||||
idempotencyKey={idempotencyKey}
|
idempotencyKey={idempotencyKey}
|
||||||
|
thread
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { closeModal } from 'soapbox/actions/modals';
|
||||||
|
import SafeEmbed from 'soapbox/components/safe-embed';
|
||||||
|
import { Modal, Stack, Text, Input, Divider } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
import useEmbed from 'soapbox/queries/embed';
|
||||||
|
|
||||||
|
interface IEmbedModal {
|
||||||
|
url: string,
|
||||||
|
onError: (error: any) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmbedModal: React.FC<IEmbedModal> = ({ url, onError }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { data: embed, error, isError } = useEmbed(url);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error && isError) {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
}, [isError]);
|
||||||
|
|
||||||
|
const handleInputClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
e.currentTarget.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dispatch(closeModal('EMBED'));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={<FormattedMessage id='status.embed' defaultMessage='Embed post' />}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<Stack space={4}>
|
||||||
|
<Text theme='muted'>
|
||||||
|
<FormattedMessage id='embed.instructions' defaultMessage='Embed this post on your website by copying the code below.' />
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type='text'
|
||||||
|
readOnly
|
||||||
|
value={embed?.html || ''}
|
||||||
|
onClick={handleInputClick}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<div className='py-9'>
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SafeEmbed
|
||||||
|
className='rounded-xl overflow-hidden w-full'
|
||||||
|
sandbox='allow-same-origin allow-scripts'
|
||||||
|
title='embedded-status'
|
||||||
|
html={embed?.html}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmbedModal;
|
|
@ -1,83 +0,0 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import api from 'soapbox/api';
|
|
||||||
import { Modal, Stack, Text, Input } from 'soapbox/components/ui';
|
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
|
||||||
|
|
||||||
import type { RootState } from 'soapbox/store';
|
|
||||||
|
|
||||||
const fetchEmbed = (url: string) => {
|
|
||||||
return (dispatch: any, getState: () => RootState) => {
|
|
||||||
return api(getState).get('/api/oembed', { params: { url } });
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IEmbedModal {
|
|
||||||
url: string,
|
|
||||||
onError: (error: any) => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
const EmbedModal: React.FC<IEmbedModal> = ({ url, onError }) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const iframe = useRef<HTMLIFrameElement>(null);
|
|
||||||
const [oembed, setOembed] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
dispatch(fetchEmbed(url)).then(({ data }) => {
|
|
||||||
if (!iframe.current?.contentWindow) return;
|
|
||||||
setOembed(data);
|
|
||||||
|
|
||||||
const iframeDocument = iframe.current.contentWindow.document;
|
|
||||||
|
|
||||||
iframeDocument.open();
|
|
||||||
iframeDocument.write(data.html);
|
|
||||||
iframeDocument.close();
|
|
||||||
|
|
||||||
const innerFrame = iframeDocument.querySelector('iframe');
|
|
||||||
|
|
||||||
iframeDocument.body.style.margin = '0';
|
|
||||||
|
|
||||||
if (innerFrame) {
|
|
||||||
innerFrame.width = '100%';
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
onError(error);
|
|
||||||
});
|
|
||||||
}, [!!iframe.current]);
|
|
||||||
|
|
||||||
const handleInputClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
e.currentTarget.select();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal title={<FormattedMessage id='status.embed' defaultMessage='Embed' />}>
|
|
||||||
<Stack space={4}>
|
|
||||||
<Stack>
|
|
||||||
<Text theme='muted' size='sm'>
|
|
||||||
<FormattedMessage id='embed.instructions' defaultMessage='Embed this post on your website by copying the code below.' />
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
type='text'
|
|
||||||
readOnly
|
|
||||||
value={oembed?.html || ''}
|
|
||||||
onClick={handleInputClick}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<iframe
|
|
||||||
className='inline-flex rounded-xl overflow-hidden max-w-full'
|
|
||||||
frameBorder='0'
|
|
||||||
ref={iframe}
|
|
||||||
sandbox='allow-same-origin'
|
|
||||||
title='preview'
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmbedModal;
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||||
|
|
||||||
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
||||||
import StatusContent from 'soapbox/components/status_content';
|
import StatusContent from 'soapbox/components/status_content';
|
||||||
import { HStack } from 'soapbox/components/ui';
|
import { Card, HStack } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account_container';
|
import AccountContainer from 'soapbox/containers/account_container';
|
||||||
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card';
|
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card';
|
||||||
import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder_media_gallery';
|
import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder_media_gallery';
|
||||||
|
@ -24,6 +24,7 @@ interface IPendingStatus {
|
||||||
className?: string,
|
className?: string,
|
||||||
idempotencyKey: string,
|
idempotencyKey: string,
|
||||||
muted?: boolean,
|
muted?: boolean,
|
||||||
|
thread?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPendingStatusMedia {
|
interface IPendingStatusMedia {
|
||||||
|
@ -44,7 +45,7 @@ const PendingStatusMedia: React.FC<IPendingStatusMedia> = ({ status }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, muted }) => {
|
const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, muted, thread = false }) => {
|
||||||
const status = useAppSelector((state) => {
|
const status = useAppSelector((state) => {
|
||||||
const pendingStatus = state.pending_statuses.get(idempotencyKey);
|
const pendingStatus = state.pending_statuses.get(idempotencyKey);
|
||||||
return pendingStatus ? buildStatus(state, pendingStatus, idempotencyKey) : null;
|
return pendingStatus ? buildStatus(state, pendingStatus, idempotencyKey) : null;
|
||||||
|
@ -58,7 +59,10 @@ const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, mu
|
||||||
return (
|
return (
|
||||||
<div className={classNames('opacity-50', className)}>
|
<div className={classNames('opacity-50', className)}>
|
||||||
<div className={classNames('status', { 'status-reply': !!status.in_reply_to_id, muted })} data-id={status.id}>
|
<div className={classNames('status', { 'status-reply': !!status.in_reply_to_id, muted })} data-id={status.id}>
|
||||||
<div className={classNames('status__wrapper', `status-${status.visibility}`, { 'status-reply': !!status.in_reply_to_id })} tabIndex={muted ? undefined : 0}>
|
<Card
|
||||||
|
className={classNames('py-6 sm:p-5', `status-${status.visibility}`, { 'status-reply': !!status.in_reply_to_id })}
|
||||||
|
variant={thread ? 'default' : 'rounded'}
|
||||||
|
>
|
||||||
<div className='mb-4'>
|
<div className='mb-4'>
|
||||||
<HStack justifyContent='between' alignItems='start'>
|
<HStack justifyContent='between' alignItems='start'>
|
||||||
<AccountContainer
|
<AccountContainer
|
||||||
|
@ -88,7 +92,7 @@ const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, mu
|
||||||
|
|
||||||
{/* TODO */}
|
{/* TODO */}
|
||||||
{/* <PlaceholderActionBar /> */}
|
{/* <PlaceholderActionBar /> */}
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -191,7 +191,7 @@ export function EditFederationModal() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmbedModal() {
|
export function EmbedModal() {
|
||||||
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
|
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed-modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComponentModal() {
|
export function ComponentModal() {
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
/** ID of this iframe (given by embed.js) when embedded on a page. */
|
||||||
|
let iframeId: any;
|
||||||
|
|
||||||
|
/** Receive iframe messages. */
|
||||||
|
// https://github.com/mastodon/mastodon/pull/4853
|
||||||
|
const handleMessage = (e: MessageEvent) => {
|
||||||
|
if (e.data?.type === 'setHeight') {
|
||||||
|
iframeId = e.data?.id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
|
||||||
|
export { iframeId };
|
|
@ -986,7 +986,7 @@
|
||||||
"status.delete": "Delete",
|
"status.delete": "Delete",
|
||||||
"status.detailed_status": "Detailed conversation view",
|
"status.detailed_status": "Detailed conversation view",
|
||||||
"status.direct": "Direct message @{name}",
|
"status.direct": "Direct message @{name}",
|
||||||
"status.embed": "Embed",
|
"status.embed": "Embed post",
|
||||||
"status.favourite": "Like",
|
"status.favourite": "Like",
|
||||||
"status.filtered": "Filtered",
|
"status.filtered": "Filtered",
|
||||||
"status.load_more": "Load more",
|
"status.load_more": "Load more",
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useApi } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
type Embed = {
|
||||||
|
type: string,
|
||||||
|
version: string,
|
||||||
|
author_name: string,
|
||||||
|
author_url: string,
|
||||||
|
provider_name: string,
|
||||||
|
provider_url: string,
|
||||||
|
cache_age: number,
|
||||||
|
html: string,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fetch OEmbed information for a status by its URL. */
|
||||||
|
// https://github.com/mastodon/mastodon/blob/main/app/controllers/api/oembed_controller.rb
|
||||||
|
// https://github.com/mastodon/mastodon/blob/main/app/serializers/oembed_serializer.rb
|
||||||
|
export default function useEmbed(url: string) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const getEmbed = async() => {
|
||||||
|
const { data } = await api.get('/api/oembed', { params: { url } });
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useQuery<Embed>(['embed', url], getEmbed);
|
||||||
|
}
|
|
@ -177,10 +177,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__wrapper {
|
|
||||||
@apply bg-white dark:bg-primary-900 px-4 py-6 shadow-xl dark:shadow-none sm:p-5 sm:rounded-xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
[column-type=filled] .status__wrapper,
|
[column-type=filled] .status__wrapper,
|
||||||
[column-type=filled] .status-placeholder {
|
[column-type=filled] .status-placeholder {
|
||||||
@apply rounded-none shadow-none p-4;
|
@apply rounded-none shadow-none p-4;
|
||||||
|
|
Loading…
Reference in New Issue