This commit is contained in:
mgabdev
2020-03-25 23:11:32 -04:00
parent 0f01c1bc97
commit 3d0a85cde4
68 changed files with 1206 additions and 1275 deletions

View File

@@ -1,81 +1,149 @@
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { defineMessages, injectIntl } from 'react-intl'
import PopoverLayout from './popover_layout'
import List from '../list'
export default class StatusOptionsPopover extends PureComponent {
_makeMenu = (publicStatus) => {
// const { status, intl: { formatMessage }, withDismiss, withGroupAdmin } = this.props
// const mutingConversation = status.get('muted')
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
edit: { id: 'status.edit', defaultMessage: 'Edit' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
comment: { id: 'status.comment', defaultMessage: 'Comment' },
more: { id: 'status.more', defaultMessage: 'More' },
share: { id: 'status.share', defaultMessage: 'Share' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
repost: { id: 'repost', defaultMessage: 'Repost' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
repost_private: { id: 'status.repost_private', defaultMessage: 'Repost to original audience' },
cancel_repost_private: { id: 'status.cancel_repost_private', defaultMessage: 'Un-repost' },
cannot_repost: { id: 'status.cannot_repost', defaultMessage: 'This post cannot be reposted' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
like: { id: 'status.like', defaultMessage: 'Like' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
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' },
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' },
group_remove_account: { id: 'status.remove_account_from_group', defaultMessage: 'Remove account from group' },
group_remove_post: { id: 'status.remove_post_from_group', defaultMessage: 'Remove status from group' },
})
export default
@injectIntl
class StatusOptionsPopover extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
onOpenUnauthorizedModal: PropTypes.func.isRequired,
onOpenStatusSharePopover: PropTypes.func.isRequired,
onReply: PropTypes.func,
onQuote: PropTypes.func,
onFavorite: PropTypes.func,
onRepost: PropTypes.func,
onDelete: PropTypes.func,
onMention: PropTypes.func,
onMute: PropTypes.func,
onBlock: PropTypes.func,
onReport: PropTypes.func,
onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func,
onPin: PropTypes.func,
withDismiss: PropTypes.bool,
withGroupAdmin: PropTypes.bool,
intl: PropTypes.object.isRequired,
}
getItems = () => {
const { status, intl, withDismiss, withGroupAdmin } = this.props
const mutingConversation = status.get('muted')
let menu = [];
// menu.push({ text: formatMessage(messages.open), action: this.handleOpen });
menu.push({
icon: 'circle',
hideArrow: true,
title: formatMessage(messages.open),
onClick: this.handleOpen
});
// if (publicStatus) {
// menu.push({ text: formatMessage(messages.copy), action: this.handleCopy });
// menu.push({ text: formatMessage(messages.embed), action: this.handleEmbed });
// }
if (publicStatus) {
menu.push({
icon: 'circle',
hideArrow: true,
title: formatMessage(messages.copy),
onClick: this.handleCopy,
})
menu.push({
icon: 'circle',
hideArrow: true,
title: formatMessage(messages.embed),
onClick: this.handleEmbed,
})
}
// if (!me) return menu
if (!me) return menu
// menu.push(null);
if (status.getIn(['account', 'id']) === me || withDismiss) {
menu.push({
icon: 'circle',
hideArrow: true,
title: formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
onClick: this.handleConversationMuteClick,
})
}
// if (status.getIn(['account', 'id']) === me || withDismiss) {
// menu.push({ text: formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
// menu.push(null);
// }
if (status.getIn(['account', 'id']) === me) {
if (publicStatus) {
menu.push({
icon: 'circle',
hideArrow: true,
title: formatMessage(status.get('pinned') ? messages.unpin : messages.pin),
onClick: this.handlePinClick,
})
} else {
if (status.get('visibility') === 'private') {
menu.push({
title: formatMessage(status.get('reblogged') ? messages.cancel_repost_private : messages.repost_private),
onClick: this.handleRepostClick
})
}
}
menu.push({ text: formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: formatMessage(messages.edit), action: this.handleEditClick });
} else {
menu.push({ text: formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push({ text: formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
menu.push({ text: formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
menu.push({ text: formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
// if (status.getIn(['account', 'id']) === me) {
// if (publicStatus) {
// menu.push({ text: formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
// } else {
// if (status.get('visibility') === 'private') {
// menu.push({ text: formatMessage(status.get('reblogged') ? messages.cancel_repost_private : messages.repost_private), action: this.handleRepostClick });
// }
// }
// menu.push({ text: formatMessage(messages.delete), action: this.handleDeleteClick });
// menu.push({ text: formatMessage(messages.edit), action: this.handleEditClick });
// } else {
// menu.push({ text: formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
// menu.push(null);
// menu.push({ text: formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
// menu.push({ text: formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
// menu.push({ text: formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
if (isStaff) {
menu.push({ text: formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
}
// if (isStaff) {
// menu.push(null);
// menu.push({ text: formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
// menu.push({ text: formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
// }
// if (withGroupAdmin) {
// menu.push(null);
// menu.push({ text: formatMessage(messages.group_remove_account), action: this.handleGroupRemoveAccount });
// menu.push({ text: formatMessage(messages.group_remove_post), action: this.handleGroupRemovePost });
// }
// }
if (withGroupAdmin) {
menu.push({ text: formatMessage(messages.group_remove_account), action: this.handleGroupRemoveAccount });
menu.push({ text: formatMessage(messages.group_remove_post), action: this.handleGroupRemovePost });
}
}
return menu;
}
render() {
const items = this.getItems()
return (
<PopoverLayout className={_s.width240PX}>
<List
scrollKey='profile_options'
items={[
{
title: 'Help',
href: 'https://help.gab.com',
},
{
title: 'Settings',
href: '/settings',
},
{
title: 'Log Out',
href: '/auth/log_out',
},
]}
items={items}
small
/>
</PopoverLayout>

View File

@@ -0,0 +1,259 @@
import { injectIntl, defineMessages } from 'react-intl'
import spring from 'react-motion/lib/spring'
import detectPassiveEvents from 'detect-passive-events'
import classNames from 'classnames'
import Overlay from 'react-overlays/lib/Overlay'
import { changeComposeVisibility } from '../../actions/compose'
import { openModal, closeModal } from '../../actions/modal'
import { isUserTouching } from '../../utils/is_mobile'
import Motion from '../../features/ui/util/optional_motion'
import Icon from '../icon'
import ComposeExtraButton from '../../features/compose/components/compose_extra_button'
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
visibility: { id: 'privacy.visibility', defaultMessage: 'Visibility' },
})
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false
class PrivacyDropdownMenu extends PureComponent {
static propTypes = {
style: PropTypes.object,
items: PropTypes.array.isRequired,
value: PropTypes.string.isRequired,
placement: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
}
state = {
mounted: false,
}
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose()
}
}
handleKeyDown = e => {
const { items } = this.props
const value = e.currentTarget.getAttribute('data-index')
const index = items.findIndex(item => {
return (item.value === value)
})
let element
switch(e.key) {
case 'Escape':
this.props.onClose()
break
case 'Enter':
this.handleClick(e)
break
case 'ArrowDown':
element = this.node.childNodes[index + 1]
if (element) {
element.focus()
this.props.onChange(element.getAttribute('data-index'))
}
break
case 'ArrowUp':
element = this.node.childNodes[index - 1]
if (element) {
element.focus()
this.props.onChange(element.getAttribute('data-index'))
}
break
case 'Home':
element = this.node.firstChild
if (element) {
element.focus()
this.props.onChange(element.getAttribute('data-index'))
}
break
case 'End':
element = this.node.lastChild
if (element) {
element.focus()
this.props.onChange(element.getAttribute('data-index'))
}
break
}
}
handleClick = e => {
const value = e.currentTarget.getAttribute('data-index')
e.preventDefault()
this.props.onClose()
this.props.onChange(value)
}
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false)
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions)
if (this.focusedItem) this.focusedItem.focus()
this.setState({ mounted: true })
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false)
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions)
}
setRef = c => {
this.node = c
}
setFocusRef = c => {
this.focusedItem = c
}
render () {
const { mounted } = this.state
const { style, items, placement, value } = this.props
return (
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ opacity, scaleX, scaleY }) => (
// It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by
// react-overlays
<div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}>
{items.map(item => (
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
<div className='privacy-dropdown__option__icon'>
<Icon id={item.icon} fixedWidth />
</div>
<div className='privacy-dropdown__option__content'>
<strong>{item.text}</strong>
{item.meta}
</div>
</div>
))}
</div>
)}
</Motion>
)
}
}
const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS',
value: state.getIn(['compose', 'privacy']),
})
const mapDispatchToProps = dispatch => ({
onChange (value) {
dispatch(changeComposeVisibility(value))
},
isUserTouching,
onModalOpen: props => dispatch(openModal('ACTIONS', props)),
onModalClose: () => dispatch(closeModal()),
})
export default
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class PrivacyDropdown extends PureComponent {
static propTypes = {
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
}
state = {
open: false,
placement: 'bottom',
}
handleToggle = ({ target }) => {
if (this.props.isUserTouching()) {
if (this.state.open) {
this.props.onModalClose()
} else {
this.props.onModalOpen({
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
onClick: this.handleModalActionClick,
})
}
} else {
const { top } = target.getBoundingClientRect()
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' })
this.setState({ open: !this.state.open })
}
}
handleModalActionClick = (e) => {
e.preventDefault()
const { value } = this.options[e.currentTarget.getAttribute('data-index')]
this.props.onModalClose()
this.props.onChange(value)
}
handleKeyDown = e => {
switch(e.key) {
case 'Escape':
this.handleClose()
break
}
}
handleClose = () => {
this.setState({ open: false })
}
handleChange = value => {
this.props.onChange(value)
}
componentWillMount () {
const { intl: { formatMessage } } = this.props
this.options = [
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
]
}
render () {
const { value, intl } = this.props
const { open, placement } = this.state
const valueOption = this.options.find(item => item.value === value)
return (
<PrivacyDropdownMenu
items={this.options}
value={value}
onClose={this.handleClose}
onChange={this.handleChange}
placement={placement}
/>
)
}
}