Native status embeds from Soapbox

environments/review-embedded-s-jkh70t/deployments/771
Alex Gleason 2 years ago
parent f338a761ef
commit 5f8a22b452
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7

@ -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,14 +362,12 @@ 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'),
});
}
}
if (!me) {
return menu;

@ -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} />

@ -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,18 +28,12 @@ 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>
);

@ -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…
Cancel
Save