Native status embeds from Soapbox
This commit is contained in:
parent
f338a761ef
commit
5f8a22b452
|
@ -284,7 +284,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
|
|
||||||
const handleEmbed = () => {
|
const handleEmbed = () => {
|
||||||
dispatch(openModal('EMBED', {
|
dispatch(openModal('EMBED', {
|
||||||
url: status.get('url'),
|
status,
|
||||||
onError: (error: any) => dispatch(showAlertForError(error)),
|
onError: (error: any) => dispatch(showAlertForError(error)),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
@ -362,14 +362,12 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
icon: require('@tabler/icons/link.svg'),
|
icon: require('@tabler/icons/link.svg'),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (features.embeds) {
|
|
||||||
menu.push({
|
menu.push({
|
||||||
text: intl.formatMessage(messages.embed),
|
text: intl.formatMessage(messages.embed),
|
||||||
action: handleEmbed,
|
action: handleEmbed,
|
||||||
icon: require('@tabler/icons/share.svg'),
|
icon: require('@tabler/icons/share.svg'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!me) {
|
if (!me) {
|
||||||
return menu;
|
return menu;
|
||||||
|
|
|
@ -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,6 +47,7 @@ export interface IStatus {
|
||||||
featured?: boolean,
|
featured?: boolean,
|
||||||
hideActionBar?: boolean,
|
hideActionBar?: boolean,
|
||||||
hoverable?: boolean,
|
hoverable?: boolean,
|
||||||
|
variant?: 'default' | 'rounded',
|
||||||
}
|
}
|
||||||
|
|
||||||
const Status: React.FC<IStatus> = (props) => {
|
const Status: React.FC<IStatus> = (props) => {
|
||||||
|
@ -63,6 +64,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
unread,
|
unread,
|
||||||
group,
|
group,
|
||||||
hideActionBar,
|
hideActionBar,
|
||||||
|
variant = 'rounded',
|
||||||
} = props;
|
} = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -318,7 +320,8 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
>
|
>
|
||||||
{prepend}
|
{prepend}
|
||||||
|
|
||||||
<div
|
<Card
|
||||||
|
variant={variant}
|
||||||
className={classNames('status__wrapper', `status-${actualStatus.visibility}`, {
|
className={classNames('status__wrapper', `status-${actualStatus.visibility}`, {
|
||||||
'status-reply': !!status.in_reply_to_id,
|
'status-reply': !!status.in_reply_to_id,
|
||||||
muted,
|
muted,
|
||||||
|
@ -378,7 +381,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,7 +28,7 @@ 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}
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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 { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import api from 'soapbox/api';
|
|
||||||
import { Modal, Stack, Text, Input } from 'soapbox/components/ui';
|
import { Modal, Stack, Text, Input } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
|
||||||
|
|
||||||
import type { RootState } from 'soapbox/store';
|
import type { Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const fetchEmbed = (url: string) => {
|
|
||||||
return (dispatch: any, getState: () => RootState) => {
|
|
||||||
return api(getState).get('/api/oembed', { params: { url } });
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IEmbedModal {
|
interface IEmbedModal {
|
||||||
url: string,
|
status: Status,
|
||||||
onError: (error: any) => void,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmbedModal: React.FC<IEmbedModal> = ({ url, onError }) => {
|
const EmbedModal: React.FC<IEmbedModal> = ({ status }) => {
|
||||||
const dispatch = useAppDispatch();
|
const url = `${location.origin}/embed/${status.id}`;
|
||||||
|
const embed = `<iframe src="${url}" width="100%" height="300" frameborder="0" />`;
|
||||||
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) => {
|
const handleInputClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
|
||||||
e.currentTarget.select();
|
e.currentTarget.select();
|
||||||
|
@ -63,18 +28,12 @@ const EmbedModal: React.FC<IEmbedModal> = ({ url, onError }) => {
|
||||||
<Input
|
<Input
|
||||||
type='text'
|
type='text'
|
||||||
readOnly
|
readOnly
|
||||||
value={oembed?.html || ''}
|
value={embed}
|
||||||
onClick={handleInputClick}
|
onClick={handleInputClick}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<iframe
|
<div dangerouslySetInnerHTML={{ __html: embed }} />
|
||||||
className='inline-flex rounded-xl overflow-hidden max-w-full'
|
|
||||||
frameBorder='0'
|
|
||||||
ref={iframe}
|
|
||||||
sandbox='allow-same-origin'
|
|
||||||
title='preview'
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -238,12 +238,6 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
emailList: features.includes('email_list'),
|
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.
|
* Ability to add emoji reactions to a status.
|
||||||
* @see PUT /api/v1/pleroma/statuses/:id/reactions/:emoji
|
* @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__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