[Glitch] Add option to open original page in dropdowns of remote content in web UI

Port ef582dc4f2 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Eugen Rochko 2022-11-10 08:49:35 +01:00 committed by Claire
parent 099b3011aa
commit c4d2c72924
7 changed files with 110 additions and 39 deletions

View file

@ -42,6 +42,7 @@ const messages = defineMessages({
hide: { id: 'status.hide', defaultMessage: 'Hide toot' }, hide: { id: 'status.hide', defaultMessage: 'Hide toot' },
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
filter: { id: 'status.filter', defaultMessage: 'Filter this post' }, filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
}); });
export default @injectIntl export default @injectIntl
@ -182,22 +183,8 @@ class StatusActionBar extends ImmutablePureComponent {
} }
handleCopy = () => { handleCopy = () => {
const url = this.props.status.get('url'); const url = this.props.status.get('url');
const textarea = document.createElement('textarea'); navigator.clipboard.writeText(url);
textarea.textContent = url;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
try {
textarea.select();
document.execCommand('copy');
} catch (e) {
} finally {
document.body.removeChild(textarea);
}
} }
handleHideClick = () => { handleHideClick = () => {
@ -216,6 +203,7 @@ class StatusActionBar extends ImmutablePureComponent {
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
const writtenByMe = status.getIn(['account', 'id']) === me; const writtenByMe = status.getIn(['account', 'id']) === me;
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
let menu = []; let menu = [];
let reblogIcon = 'retweet'; let reblogIcon = 'retweet';
@ -225,6 +213,9 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
if (publicStatus) { if (publicStatus) {
if (isRemote) {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
}
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
} }

View file

@ -53,6 +53,7 @@ const messages = defineMessages({
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' }, add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' },
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
}); });
const titleFromAccount = account => { const titleFromAccount = account => {
@ -97,6 +98,7 @@ class Header extends ImmutablePureComponent {
onEditAccountNote: PropTypes.func.isRequired, onEditAccountNote: PropTypes.func.isRequired,
onChangeLanguages: PropTypes.func.isRequired, onChangeLanguages: PropTypes.func.isRequired,
onInteractionModal: PropTypes.func.isRequired, onInteractionModal: PropTypes.func.isRequired,
onOpenAvatar: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
hidden: PropTypes.bool, hidden: PropTypes.bool,
@ -132,6 +134,13 @@ class Header extends ImmutablePureComponent {
} }
} }
handleAvatarClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.props.onOpenAvatar();
}
}
render () { render () {
const { account, hidden, intl, domain } = this.props; const { account, hidden, intl, domain } = this.props;
const { signedIn } = this.context.identity; const { signedIn } = this.context.identity;
@ -142,7 +151,9 @@ class Header extends ImmutablePureComponent {
const accountNote = account.getIn(['relationship', 'note']); const accountNote = account.getIn(['relationship', 'note']);
const suspended = account.get('suspended'); const suspended = account.get('suspended');
const isRemote = account.get('acct') !== account.get('username');
const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null;
let info = []; let info = [];
let actionBtn = ''; let actionBtn = '';
@ -199,6 +210,11 @@ class Header extends ImmutablePureComponent {
menu.push(null); menu.push(null);
} }
if (isRemote) {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
menu.push(null);
}
if ('share' in navigator && !suspended) { if ('share' in navigator && !suspended) {
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
menu.push(null); menu.push(null);
@ -253,15 +269,13 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
} }
if (signedIn && account.get('acct') !== account.get('username')) { if (signedIn && isRemote) {
const domain = account.get('acct').split('@')[1];
menu.push(null); menu.push(null);
if (account.getIn(['relationship', 'domain_blocking'])) { if (account.getIn(['relationship', 'domain_blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain }); menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain });
} else { } else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain }); menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain });
} }
} }
@ -299,7 +313,7 @@ class Header extends ImmutablePureComponent {
<div className='account__header__bar'> <div className='account__header__bar'>
<div className='account__header__tabs'> <div className='account__header__tabs'>
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'> <a className='avatar' href={account.get('avatar')} rel='noopener noreferrer' target='_blank' onClick={this.handleAvatarClick}>
<Avatar account={suspended || hidden ? undefined : account} size={90} /> <Avatar account={suspended || hidden ? undefined : account} size={90} />
</a> </a>

View file

@ -25,6 +25,7 @@ export default class Header extends ImmutablePureComponent {
onAddToList: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired,
onChangeLanguages: PropTypes.func.isRequired, onChangeLanguages: PropTypes.func.isRequired,
onInteractionModal: PropTypes.func.isRequired, onInteractionModal: PropTypes.func.isRequired,
onOpenAvatar: PropTypes.func.isRequired,
hideTabs: PropTypes.bool, hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
hidden: PropTypes.bool, hidden: PropTypes.bool,
@ -102,6 +103,10 @@ export default class Header extends ImmutablePureComponent {
this.props.onInteractionModal(this.props.account); this.props.onInteractionModal(this.props.account);
} }
handleOpenAvatar = () => {
this.props.onOpenAvatar(this.props.account);
}
render () { render () {
const { account, hidden, hideTabs } = this.props; const { account, hidden, hideTabs } = this.props;
@ -130,6 +135,7 @@ export default class Header extends ImmutablePureComponent {
onEditAccountNote={this.handleEditAccountNote} onEditAccountNote={this.handleEditAccountNote}
onChangeLanguages={this.handleChangeLanguages} onChangeLanguages={this.handleChangeLanguages}
onInteractionModal={this.handleInteractionModal} onInteractionModal={this.handleInteractionModal}
onOpenAvatar={this.handleOpenAvatar}
domain={this.props.domain} domain={this.props.domain}
hidden={hidden} hidden={hidden}
/> />

View file

@ -161,6 +161,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
})); }));
}, },
onOpenAvatar (account) {
dispatch(openModal('IMAGE', {
src: account.get('avatar'),
alt: account.get('acct'),
}));
},
}); });
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));

View file

@ -35,6 +35,7 @@ const messages = defineMessages({
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
}); });
export default @injectIntl export default @injectIntl
@ -132,22 +133,8 @@ class ActionBar extends React.PureComponent {
} }
handleCopy = () => { handleCopy = () => {
const url = this.props.status.get('url'); const url = this.props.status.get('url');
const textarea = document.createElement('textarea'); navigator.clipboard.writeText(url);
textarea.textContent = url;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
try {
textarea.select();
document.execCommand('copy');
} catch (e) {
} finally {
document.body.removeChild(textarea);
}
} }
render () { render () {
@ -158,10 +145,15 @@ class ActionBar extends React.PureComponent {
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
const mutingConversation = status.get('muted'); const mutingConversation = status.get('muted');
const writtenByMe = status.getIn(['account', 'id']) === me; const writtenByMe = status.getIn(['account', 'id']) === me;
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
let menu = []; let menu = [];
if (publicStatus) { if (publicStatus) {
if (isRemote) {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
}
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
menu.push(null); menu.push(null);

View file

@ -0,0 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from 'flavours/glitch/components/icon_button';
import ImageLoader from './image_loader';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
export default @injectIntl
class ImageModal extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
state = {
navigationHidden: false,
};
toggleNavigation = () => {
this.setState(prevState => ({
navigationHidden: !prevState.navigationHidden,
}));
};
render () {
const { intl, src, alt, onClose } = this.props;
const { navigationHidden } = this.state;
const navigationClassName = classNames('media-modal__navigation', {
'media-modal__navigation--hidden': navigationHidden,
});
return (
<div className='modal-root__modal media-modal'>
<div className='media-modal__closer' role='presentation' onClick={onClose} >
<ImageLoader
src={src}
width={400}
height={400}
alt={alt}
onClick={this.toggleNavigation}
/>
</div>
<div className={navigationClassName}>
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} />
</div>
</div>
);
}
}

View file

@ -15,6 +15,7 @@ import DoodleModal from './doodle_modal';
import ConfirmationModal from './confirmation_modal'; import ConfirmationModal from './confirmation_modal';
import FocalPointModal from './focal_point_modal'; import FocalPointModal from './focal_point_modal';
import DeprecatedSettingsModal from './deprecated_settings_modal'; import DeprecatedSettingsModal from './deprecated_settings_modal';
import ImageModal from './image_modal';
import { import {
OnboardingModal, OnboardingModal,
MuteModal, MuteModal,
@ -38,6 +39,7 @@ const MODAL_COMPONENTS = {
'ONBOARDING': OnboardingModal, 'ONBOARDING': OnboardingModal,
'VIDEO': () => Promise.resolve({ default: VideoModal }), 'VIDEO': () => Promise.resolve({ default: VideoModal }),
'AUDIO': () => Promise.resolve({ default: AudioModal }), 'AUDIO': () => Promise.resolve({ default: AudioModal }),
'IMAGE': () => Promise.resolve({ default: ImageModal }),
'BOOST': () => Promise.resolve({ default: BoostModal }), 'BOOST': () => Promise.resolve({ default: BoostModal }),
'FAVOURITE': () => Promise.resolve({ default: FavouriteModal }), 'FAVOURITE': () => Promise.resolve({ default: FavouriteModal }),
'DOODLE': () => Promise.resolve({ default: DoodleModal }), 'DOODLE': () => Promise.resolve({ default: DoodleModal }),