Native status embeds from Soapbox
This commit is contained in:
parent
f338a761ef
commit
5f8a22b452
8 changed files with 80 additions and 73 deletions
|
@ -284,7 +284,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
|
||||
const handleEmbed = () => {
|
||||
dispatch(openModal('EMBED', {
|
||||
url: status.get('url'),
|
||||
status,
|
||||
onError: (error: any) => dispatch(showAlertForError(error)),
|
||||
}));
|
||||
};
|
||||
|
@ -362,13 +362,11 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
icon: require('@tabler/icons/link.svg'),
|
||||
});
|
||||
|
||||
if (features.embeds) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.embed),
|
||||
action: handleEmbed,
|
||||
icon: require('@tabler/icons/share.svg'),
|
||||
});
|
||||
}
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.embed),
|
||||
action: handleEmbed,
|
||||
icon: require('@tabler/icons/share.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (!me) {
|
||||
|
|
|
@ -18,7 +18,7 @@ import StatusActionBar from './status-action-bar';
|
|||
import StatusMedia from './status-media';
|
||||
import StatusReplyMentions from './status-reply-mentions';
|
||||
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 {
|
||||
|
@ -47,6 +47,7 @@ export interface IStatus {
|
|||
featured?: boolean,
|
||||
hideActionBar?: boolean,
|
||||
hoverable?: boolean,
|
||||
variant?: 'default' | 'rounded',
|
||||
}
|
||||
|
||||
const Status: React.FC<IStatus> = (props) => {
|
||||
|
@ -63,6 +64,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
unread,
|
||||
group,
|
||||
hideActionBar,
|
||||
variant = 'rounded',
|
||||
} = props;
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
@ -318,7 +320,8 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
>
|
||||
{prepend}
|
||||
|
||||
<div
|
||||
<Card
|
||||
variant={variant}
|
||||
className={classNames('status__wrapper', `status-${actualStatus.visibility}`, {
|
||||
'status-reply': !!status.in_reply_to_id,
|
||||
muted,
|
||||
|
@ -378,7 +381,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ const messages = defineMessages({
|
|||
|
||||
interface ICard {
|
||||
/** The type of card. */
|
||||
variant?: 'rounded',
|
||||
variant?: 'default' | 'rounded',
|
||||
/** Card size preset. */
|
||||
size?: 'md' | 'lg' | 'xl',
|
||||
/** Extra classnames for the <div> element. */
|
||||
|
@ -28,7 +28,7 @@ interface ICard {
|
|||
}
|
||||
|
||||
/** 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
|
||||
ref={ref}
|
||||
{...filteredProps}
|
||||
|
|
|
@ -18,6 +18,7 @@ import GdprBanner from 'soapbox/components/gdpr-banner';
|
|||
import Helmet from 'soapbox/components/helmet';
|
||||
import LoadingScreen from 'soapbox/components/loading-screen';
|
||||
import AuthLayout from 'soapbox/features/auth_layout';
|
||||
import EmbeddedStatus from 'soapbox/features/embedded-status';
|
||||
import PublicLayout from 'soapbox/features/public_layout';
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||
import {
|
||||
|
@ -148,6 +149,12 @@ const SoapboxMount = () => {
|
|||
<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='/edit-password' component={AuthLayout} />
|
||||
<Route path='/invite/:token' component={AuthLayout} />
|
||||
|
|
50
app/soapbox/features/embedded-status/index.tsx
Normal file
50
app/soapbox/features/embedded-status/index.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { fetchStatus } from 'soapbox/actions/statuses';
|
||||
import MissingIndicator from 'soapbox/components/missing_indicator';
|
||||
import Status from 'soapbox/components/status';
|
||||
import { Spinner } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
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 status = useAppSelector(state => getStatus(state, { id: params.statusId }));
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchStatus(params.statusId))
|
||||
.then(() => setLoading(false))
|
||||
.catch(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const renderInner = () => {
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
} else if (status) {
|
||||
return <Status status={status} variant='default' />;
|
||||
} else {
|
||||
return <MissingIndicator nested />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='bg-white dark:bg-gray-800'>
|
||||
<div className='p-4 sm:p-6 max-w-3xl'>
|
||||
{renderInner()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmbeddedStatus;
|
|
@ -1,52 +1,17 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React 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 } });
|
||||
};
|
||||
};
|
||||
import type { Status } from 'soapbox/types/entities';
|
||||
|
||||
interface IEmbedModal {
|
||||
url: string,
|
||||
onError: (error: any) => void,
|
||||
status: Status,
|
||||
}
|
||||
|
||||
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 EmbedModal: React.FC<IEmbedModal> = ({ status }) => {
|
||||
const url = `${location.origin}/embed/${status.id}`;
|
||||
const embed = `<iframe src="${url}" width="100%" height="300" frameborder="0" />`;
|
||||
|
||||
const handleInputClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
|
||||
e.currentTarget.select();
|
||||
|
@ -63,21 +28,15 @@ const EmbedModal: React.FC<IEmbedModal> = ({ url, onError }) => {
|
|||
<Input
|
||||
type='text'
|
||||
readOnly
|
||||
value={oembed?.html || ''}
|
||||
value={embed}
|
||||
onClick={handleInputClick}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<iframe
|
||||
className='inline-flex rounded-xl overflow-hidden max-w-full'
|
||||
frameBorder='0'
|
||||
ref={iframe}
|
||||
sandbox='allow-same-origin'
|
||||
title='preview'
|
||||
/>
|
||||
<div dangerouslySetInnerHTML={{ __html: embed }} />
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmbedModal;
|
||||
export default EmbedModal;
|
||||
|
|
|
@ -238,12 +238,6 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
*/
|
||||
emailList: features.includes('email_list'),
|
||||
|
||||
/**
|
||||
* Ability to embed posts on external sites.
|
||||
* @see GET /api/oembed
|
||||
*/
|
||||
embeds: v.software === MASTODON,
|
||||
|
||||
/**
|
||||
* Ability to add emoji reactions to a status.
|
||||
* @see PUT /api/v1/pleroma/statuses/:id/reactions/:emoji
|
||||
|
|
|
@ -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-placeholder {
|
||||
@apply rounded-none shadow-none p-4;
|
||||
|
|
Loading…
Reference in a new issue