mirror of https://github.com/mastodon/mastodon
Add option to open original page in dropdowns of remote content in web UI (#20299)
Change profile picture click to open profile picture in modal in web UI
This commit is contained in:
parent
e37e8deb0f
commit
ef582dc4f2
|
@ -45,6 +45,7 @@ const messages = defineMessages({
|
||||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
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' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, { status }) => ({
|
const mapStateToProps = (state, { status }) => ({
|
||||||
|
@ -221,25 +222,10 @@ 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 = () => {
|
||||||
this.props.onFilter();
|
this.props.onFilter();
|
||||||
}
|
}
|
||||||
|
@ -254,12 +240,17 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
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 = [];
|
||||||
|
|
||||||
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 });
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ const messages = defineMessages({
|
||||||
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
|
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
|
||||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface 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,
|
||||||
|
@ -140,6 +142,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;
|
||||||
|
@ -148,7 +157,9 @@ class Header extends ImmutablePureComponent {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = '';
|
||||||
|
@ -200,6 +211,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) {
|
if ('share' in navigator) {
|
||||||
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);
|
||||||
|
@ -250,15 +266,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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,7 +310,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>
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,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,
|
||||||
|
@ -101,6 +102,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;
|
||||||
|
|
||||||
|
@ -129,6 +134,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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -152,6 +152,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));
|
||||||
|
|
|
@ -39,6 +39,7 @@ const messages = defineMessages({
|
||||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
|
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, { status }) => ({
|
const mapStateToProps = (state, { status }) => ({
|
||||||
|
@ -174,22 +175,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 () {
|
||||||
|
@ -201,10 +188,15 @@ class ActionBar extends React.PureComponent {
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
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);
|
||||||
|
|
|
@ -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 'mastodon/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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import BoostModal from './boost_modal';
|
||||||
import AudioModal from './audio_modal';
|
import AudioModal from './audio_modal';
|
||||||
import ConfirmationModal from './confirmation_modal';
|
import ConfirmationModal from './confirmation_modal';
|
||||||
import FocalPointModal from './focal_point_modal';
|
import FocalPointModal from './focal_point_modal';
|
||||||
|
import ImageModal from './image_modal';
|
||||||
import {
|
import {
|
||||||
MuteModal,
|
MuteModal,
|
||||||
BlockModal,
|
BlockModal,
|
||||||
|
@ -31,6 +32,7 @@ const MODAL_COMPONENTS = {
|
||||||
'MEDIA': () => Promise.resolve({ default: MediaModal }),
|
'MEDIA': () => Promise.resolve({ default: MediaModal }),
|
||||||
'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 }),
|
||||||
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
|
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
|
||||||
'MUTE': MuteModal,
|
'MUTE': MuteModal,
|
||||||
|
|
Loading…
Reference in New Issue