1
0

[Glitch] Change design of embed modal in web UI

Port 24ef8255b3 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Eugen Rochko 2024-09-12 14:54:16 +02:00 committed by Claire
parent e705ec13db
commit bd68d2ab21
8 changed files with 254 additions and 234 deletions

View File

@ -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 { Icon } from 'flavours/glitch/components/icon';
import { useTimeout } from 'flavours/glitch/hooks/useTimeout';
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>
);
};

View File

@ -56,7 +56,7 @@ const messages = defineMessages({
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' },
embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },

View File

@ -34,8 +34,6 @@ import Status from 'flavours/glitch/components/status';
import { deleteModal } from 'flavours/glitch/initial_state';
import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
import { showAlertForError } from '../actions/alerts';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
@ -111,10 +109,7 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
onEmbed (status) {
dispatch(openModal({
modalType: 'EMBED',
modalProps: {
id: status.get('id'),
onError: error => dispatch(showAlertForError(error)),
},
modalProps: { id: status.get('id') },
}));
},

View File

@ -10,8 +10,8 @@ import { Link } from 'react-router-dom';
import SwipeableViews from 'react-swipeable-views';
import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
import { CopyPasteText } from 'flavours/glitch/components/copy_paste_text';
import { Icon } from 'flavours/glitch/components/icon';
import { me, domain } from 'flavours/glitch/initial_state';
import { useAppSelector } from 'flavours/glitch/store';
@ -20,67 +20,6 @@ const messages = defineMessages({
shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
});
class CopyPasteText extends PureComponent {
static propTypes = {
value: PropTypes.string,
};
state = {
copied: false,
focused: false,
};
setRef = c => {
this.input = c;
};
handleInputClick = () => {
this.setState({ copied: false });
this.input.focus();
this.input.select();
this.input.setSelectionRange(0, this.props.value.length);
};
handleButtonClick = e => {
e.stopPropagation();
const { value } = this.props;
navigator.clipboard.writeText(value);
this.input.blur();
this.setState({ copied: true });
this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
};
handleFocus = () => {
this.setState({ focused: true });
};
handleBlur = () => {
this.setState({ focused: false });
};
componentWillUnmount () {
if (this.timeout) clearTimeout(this.timeout);
}
render () {
const { value } = this.props;
const { copied, focused } = this.state;
return (
<div className={classNames('copy-paste-text', { copied, focused })} tabIndex='0' role='button' onClick={this.handleInputClick}>
<textarea readOnly value={value} ref={this.setRef} onClick={this.handleInputClick} onFocus={this.handleFocus} onBlur={this.handleBlur} />
<button className='button' onClick={this.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>
);
}
}
class TipCarousel extends PureComponent {
static propTypes = {

View File

@ -49,7 +49,7 @@ const messages = defineMessages({
share: { id: 'status.share', defaultMessage: 'Share' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' },
embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },

View File

@ -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 'flavours/glitch/api';
import { IconButton } from 'flavours/glitch/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);

View File

@ -0,0 +1,116 @@
import { useRef, useState, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { showAlertForError } from 'flavours/glitch/actions/alerts';
import api from 'flavours/glitch/api';
import { Button } from 'flavours/glitch/components/button';
import { CopyPasteText } from 'flavours/glitch/components/copy_paste_text';
import { useAppDispatch } from 'flavours/glitch/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;

View File

@ -6741,6 +6741,50 @@ a.status-card {
}
}
.dialog-modal {
width: 588px;
max-height: 80vh;
flex-direction: column;
background: var(--modal-background-color);
backdrop-filter: var(--background-filter);
border: 1px solid var(--modal-border-color);
border-radius: 16px;
&__header {
border-bottom: 1px solid var(--modal-border-color);
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: row-reverse;
padding: 12px 24px;
&__title {
font-size: 16px;
line-height: 24px;
font-weight: 500;
letter-spacing: 0.15px;
}
}
&__content {
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
overflow-y: auto;
&__form {
display: flex;
flex-direction: column;
gap: 16px;
padding: 24px;
}
}
.copy-paste-text {
margin-bottom: 0;
}
}
.hotkey-combination {
display: inline-flex;
align-items: center;
@ -8273,69 +8317,6 @@ noscript {
}
}
.embed-modal {
width: auto;
max-width: 80vw;
max-height: 80vh;
h4 {
padding: 30px;
font-weight: 500;
font-size: 16px;
text-align: center;
}
.embed-modal__container {
padding: 10px;
.hint {
margin-bottom: 15px;
}
.embed-modal__html {
outline: 0;
box-sizing: border-box;
display: block;
width: 100%;
border: 0;
padding: 10px;
font-family: $font-monospace, monospace;
background: $ui-base-color;
color: $primary-text-color;
font-size: 14px;
margin: 0;
margin-bottom: 15px;
border-radius: 4px;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
@media screen and (width <= 600px) {
font-size: 16px;
}
}
.embed-modal__iframe {
width: 400px;
max-width: 100%;
overflow: hidden;
border: 0;
border-radius: 4px;
}
}
}
.moved-account-banner,
.follow-request-banner,
.account-memorial-banner {