Progress
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user