mirror of https://github.com/mastodon/mastodon
Change design of embed modal in web UI (#31801)
parent
ab763c493f
commit
24ef8255b3
@ -0,0 +1,90 @@
|
||||
import { useRef, useState, useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
|
||||
import { useTimeout } from 'mastodon/../hooks/useTimeout';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [setAnimationTimeout] = useTimeout();
|
||||
|
||||
const handleInputClick = useCallback(() => {
|
||||
setCopied(false);
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
inputRef.current.setSelectionRange(0, value.length);
|
||||
}
|
||||
}, [setCopied, value]);
|
||||
|
||||
const handleButtonClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
void navigator.clipboard.writeText(value);
|
||||
inputRef.current?.blur();
|
||||
setCopied(true);
|
||||
setAnimationTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 700);
|
||||
},
|
||||
[setCopied, setAnimationTimeout, value],
|
||||
);
|
||||
|
||||
const handleKeyUp = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key !== ' ') return;
|
||||
void navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setAnimationTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 700);
|
||||
},
|
||||
[setCopied, setAnimationTimeout, value],
|
||||
);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
setFocused(true);
|
||||
}, [setFocused]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setFocused(false);
|
||||
}, [setFocused]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('copy-paste-text', { copied, focused })}
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
onClick={handleInputClick}
|
||||
onKeyUp={handleKeyUp}
|
||||
>
|
||||
<textarea
|
||||
readOnly
|
||||
value={value}
|
||||
ref={inputRef}
|
||||
onClick={handleInputClick}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
|
||||
<button className='button' onClick={handleButtonClick}>
|
||||
<Icon id='copy' icon={ContentCopyIcon} />{' '}
|
||||
{copied ? (
|
||||
<FormattedMessage id='copypaste.copied' defaultMessage='Copied' />
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='copypaste.copy_to_clipboard'
|
||||
defaultMessage='Copy to clipboard'
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,101 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import api from 'mastodon/api';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
class EmbedModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onError: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: false,
|
||||
oembed: null,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { id } = this.props;
|
||||
|
||||
this.setState({ loading: true });
|
||||
|
||||
api().get(`/api/web/embeds/${id}`).then(res => {
|
||||
this.setState({ loading: false, oembed: res.data });
|
||||
|
||||
const iframeDocument = this.iframe.contentWindow.document;
|
||||
|
||||
iframeDocument.open();
|
||||
iframeDocument.write(res.data.html);
|
||||
iframeDocument.close();
|
||||
|
||||
iframeDocument.body.style.margin = 0;
|
||||
this.iframe.width = iframeDocument.body.scrollWidth;
|
||||
this.iframe.height = iframeDocument.body.scrollHeight;
|
||||
}).catch(error => {
|
||||
this.props.onError(error);
|
||||
});
|
||||
}
|
||||
|
||||
setIframeRef = c => {
|
||||
this.iframe = c;
|
||||
};
|
||||
|
||||
handleTextareaClick = (e) => {
|
||||
e.target.select();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, onClose } = this.props;
|
||||
const { oembed } = this.state;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal report-modal embed-modal'>
|
||||
<div className='report-modal__target'>
|
||||
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={16} />
|
||||
<FormattedMessage id='status.embed' defaultMessage='Embed' />
|
||||
</div>
|
||||
|
||||
<div className='report-modal__container embed-modal__container' style={{ display: 'block' }}>
|
||||
<p className='hint'>
|
||||
<FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' />
|
||||
</p>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
className='embed-modal__html'
|
||||
readOnly
|
||||
value={oembed && oembed.html || ''}
|
||||
onClick={this.handleTextareaClick}
|
||||
/>
|
||||
|
||||
<p className='hint'>
|
||||
<FormattedMessage id='embed.preview' defaultMessage='Here is what it will look like:' />
|
||||
</p>
|
||||
|
||||
<iframe
|
||||
className='embed-modal__iframe'
|
||||
frameBorder='0'
|
||||
ref={this.setIframeRef}
|
||||
sandbox='allow-scripts allow-same-origin'
|
||||
title='preview'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(EmbedModal);
|
@ -0,0 +1,116 @@
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { showAlertForError } from 'mastodon/actions/alerts';
|
||||
import api from 'mastodon/api';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CopyPasteText } from 'mastodon/components/copy_paste_text';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
interface OEmbedResponse {
|
||||
html: string;
|
||||
}
|
||||
|
||||
const EmbedModal: React.FC<{
|
||||
id: string;
|
||||
onClose: () => void;
|
||||
}> = ({ id, onClose }) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval>>();
|
||||
const [oembed, setOembed] = useState<OEmbedResponse | null>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
api()
|
||||
.get(`/api/web/embeds/${id}`)
|
||||
.then((res) => {
|
||||
const data = res.data as OEmbedResponse;
|
||||
|
||||
setOembed(data);
|
||||
|
||||
const iframeDocument = iframeRef.current?.contentWindow?.document;
|
||||
|
||||
if (!iframeDocument) {
|
||||
return '';
|
||||
}
|
||||
|
||||
iframeDocument.open();
|
||||
iframeDocument.write(data.html);
|
||||
iframeDocument.close();
|
||||
|
||||
iframeDocument.body.style.margin = '0px';
|
||||
|
||||
// This is our best chance to ensure the parent iframe has the correct height...
|
||||
intervalRef.current = setInterval(
|
||||
() =>
|
||||
window.requestAnimationFrame(() => {
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.width = `${iframeDocument.body.scrollWidth}px`;
|
||||
iframeRef.current.height = `${iframeDocument.body.scrollHeight}px`;
|
||||
}
|
||||
}),
|
||||
100,
|
||||
);
|
||||
|
||||
return '';
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
dispatch(showAlertForError(error));
|
||||
});
|
||||
}, [dispatch, id, setOembed]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal dialog-modal'>
|
||||
<div className='dialog-modal__header'>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id='report.close' defaultMessage='Done' />
|
||||
</Button>
|
||||
<span className='dialog-modal__header__title'>
|
||||
<FormattedMessage id='status.embed' defaultMessage='Get embed code' />
|
||||
</span>
|
||||
<Button secondary onClick={onClose}>
|
||||
<FormattedMessage
|
||||
id='confirmation_modal.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='dialog-modal__content'>
|
||||
<div className='dialog-modal__content__form'>
|
||||
<FormattedMessage
|
||||
id='embed.instructions'
|
||||
defaultMessage='Embed this status on your website by copying the code below.'
|
||||
/>
|
||||
|
||||
<CopyPasteText value={oembed?.html ?? ''} />
|
||||
|
||||
<FormattedMessage
|
||||
id='embed.preview'
|
||||
defaultMessage='Here is what it will look like:'
|
||||
/>
|
||||
|
||||
<iframe
|
||||
frameBorder='0'
|
||||
ref={iframeRef}
|
||||
sandbox='allow-scripts allow-same-origin'
|
||||
title='Preview'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default EmbedModal;
|
Loading…
Reference in New Issue