1 import React
from 'react';
2 import ImmutablePropTypes
from 'react-immutable-proptypes';
3 import { connect
} from 'react-redux';
4 import PropTypes
from 'prop-types';
5 import IconButton
from './icon_button';
6 import DropdownMenuContainer
from '../containers/dropdown_menu_container';
7 import { defineMessages
, injectIntl
} from 'react-intl';
8 import ImmutablePureComponent
from 'react-immutable-pure-component';
9 import { me
} from '../initial_state';
10 import classNames
from 'classnames';
11 import { PERMISSION_MANAGE_USERS
} from 'mastodon/permissions';
13 const messages
= defineMessages({
14 delete: { id: 'status.delete', defaultMessage: 'Delete' },
15 redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
16 edit: { id: 'status.edit', defaultMessage: 'Edit' },
17 direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
18 mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
19 mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
20 block: { id: 'account.block', defaultMessage: 'Block @{name}' },
21 reply: { id: 'status.reply', defaultMessage: 'Reply' },
22 share: { id: 'status.share', defaultMessage: 'Share' },
23 more: { id: 'status.more', defaultMessage: 'More' },
24 replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
25 reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
26 reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
27 cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
28 cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
29 favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
30 bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
31 removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
32 open: { id: 'status.open', defaultMessage: 'Expand this status' },
33 report: { id: 'status.report', defaultMessage: 'Report @{name}' },
34 muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
35 unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
36 pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
37 unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
38 embed: { id: 'status.embed', defaultMessage: 'Embed' },
39 admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
40 admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
41 copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
42 hide: { id: 'status.hide', defaultMessage: 'Hide toot' },
43 blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
44 unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
45 unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
46 unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
47 filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
50 const mapStateToProps
= (state
, { status
}) => ({
51 relationship: state
.getIn(['relationships', status
.getIn(['account', 'id'])]),
54 export default @connect(mapStateToProps
)
56 class StatusActionBar
extends ImmutablePureComponent
{
58 static contextTypes
= {
59 router: PropTypes
.object
,
60 identity: PropTypes
.object
,
64 status: ImmutablePropTypes
.map
.isRequired
,
65 relationship: ImmutablePropTypes
.map
,
66 onReply: PropTypes
.func
,
67 onFavourite: PropTypes
.func
,
68 onReblog: PropTypes
.func
,
69 onDelete: PropTypes
.func
,
70 onDirect: PropTypes
.func
,
71 onMention: PropTypes
.func
,
72 onMute: PropTypes
.func
,
73 onUnmute: PropTypes
.func
,
74 onBlock: PropTypes
.func
,
75 onUnblock: PropTypes
.func
,
76 onBlockDomain: PropTypes
.func
,
77 onUnblockDomain: PropTypes
.func
,
78 onReport: PropTypes
.func
,
79 onEmbed: PropTypes
.func
,
80 onMuteConversation: PropTypes
.func
,
81 onPin: PropTypes
.func
,
82 onBookmark: PropTypes
.func
,
83 onFilter: PropTypes
.func
,
84 onAddFilter: PropTypes
.func
,
85 withDismiss: PropTypes
.bool
,
86 withCounters: PropTypes
.bool
,
87 scrollKey: PropTypes
.string
,
88 intl: PropTypes
.object
.isRequired
,
91 // Avoid checking props that are functions (and whose equality will always
92 // evaluate to false. See react-immutable-pure-component for usage.
99 handleReplyClick
= () => {
101 this.props
.onReply(this.props
.status
, this.context
.router
.history
);
103 this._openInteractionDialog('reply');
107 handleShareClick
= () => {
109 text: this.props
.status
.get('search_index'),
110 url: this.props
.status
.get('url'),
112 if (e
.name
!== 'AbortError') console
.error(e
);
116 handleFavouriteClick
= () => {
118 this.props
.onFavourite(this.props
.status
);
120 this._openInteractionDialog('favourite');
124 handleReblogClick
= e
=> {
126 this.props
.onReblog(this.props
.status
, e
);
128 this._openInteractionDialog('reblog');
132 _openInteractionDialog
= type
=> {
133 window
.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
136 handleBookmarkClick
= () => {
137 this.props
.onBookmark(this.props
.status
);
140 handleDeleteClick
= () => {
141 this.props
.onDelete(this.props
.status
, this.context
.router
.history
);
144 handleRedraftClick
= () => {
145 this.props
.onDelete(this.props
.status
, this.context
.router
.history
, true);
148 handleEditClick
= () => {
149 this.props
.onEdit(this.props
.status
, this.context
.router
.history
);
152 handlePinClick
= () => {
153 this.props
.onPin(this.props
.status
);
156 handleMentionClick
= () => {
157 this.props
.onMention(this.props
.status
.get('account'), this.context
.router
.history
);
160 handleDirectClick
= () => {
161 this.props
.onDirect(this.props
.status
.get('account'), this.context
.router
.history
);
164 handleMuteClick
= () => {
165 const { status
, relationship
, onMute
, onUnmute
} = this.props
;
166 const account
= status
.get('account');
168 if (relationship
&& relationship
.get('muting')) {
175 handleBlockClick
= () => {
176 const { status
, relationship
, onBlock
, onUnblock
} = this.props
;
177 const account
= status
.get('account');
179 if (relationship
&& relationship
.get('blocking')) {
186 handleBlockDomain
= () => {
187 const { status
, onBlockDomain
} = this.props
;
188 const account
= status
.get('account');
190 onBlockDomain(account
.get('acct').split('@')[1]);
193 handleUnblockDomain
= () => {
194 const { status
, onUnblockDomain
} = this.props
;
195 const account
= status
.get('account');
197 onUnblockDomain(account
.get('acct').split('@')[1]);
201 this.context
.router
.history
.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
204 handleEmbed
= () => {
205 this.props
.onEmbed(this.props
.status
);
208 handleReport
= () => {
209 this.props
.onReport(this.props
.status
);
212 handleConversationMuteClick
= () => {
213 this.props
.onMuteConversation(this.props
.status
);
216 handleFilterClick
= () => {
217 this.props
.onAddFilter(this.props
.status
);
221 const url
= this.props
.status
.get('url');
222 const textarea
= document
.createElement('textarea');
224 textarea
.textContent
= url
;
225 textarea
.style
.position
= 'fixed';
227 document
.body
.appendChild(textarea
);
231 document
.execCommand('copy');
235 document
.body
.removeChild(textarea
);
240 handleHideClick
= () => {
241 this.props
.onFilter();
245 const { status
, relationship
, intl
, withDismiss
, withCounters
, scrollKey
} = this.props
;
247 const anonymousAccess
= !me
;
248 const publicStatus
= ['public', 'unlisted'].includes(status
.get('visibility'));
249 const pinnableStatus
= ['public', 'unlisted', 'private'].includes(status
.get('visibility'));
250 const mutingConversation
= status
.get('muted');
251 const account
= status
.get('account');
252 const writtenByMe
= status
.getIn(['account', 'id']) === me
;
256 menu
.push({ text: intl
.formatMessage(messages
.open
), action: this.handleOpen
});
259 menu
.push({ text: intl
.formatMessage(messages
.copy
), action: this.handleCopy
});
260 menu
.push({ text: intl
.formatMessage(messages
.embed
), action: this.handleEmbed
});
265 menu
.push({ text: intl
.formatMessage(status
.get('bookmarked') ? messages
.removeBookmark : messages
.bookmark
), action: this.handleBookmarkClick
});
267 if (writtenByMe
&& pinnableStatus
) {
268 menu
.push({ text: intl
.formatMessage(status
.get('pinned') ? messages
.unpin : messages
.pin
), action: this.handlePinClick
});
273 if (writtenByMe
|| withDismiss
) {
274 menu
.push({ text: intl
.formatMessage(mutingConversation
? messages
.unmuteConversation : messages
.muteConversation
), action: this.handleConversationMuteClick
});
279 // menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
280 menu
.push({ text: intl
.formatMessage(messages
.delete), action: this.handleDeleteClick
});
281 menu
.push({ text: intl
.formatMessage(messages
.redraft
), action: this.handleRedraftClick
});
283 menu
.push({ text: intl
.formatMessage(messages
.mention
, { name: account
.get('username') }), action: this.handleMentionClick
});
284 menu
.push({ text: intl
.formatMessage(messages
.direct
, { name: account
.get('username') }), action: this.handleDirectClick
});
287 if (relationship
&& relationship
.get('muting')) {
288 menu
.push({ text: intl
.formatMessage(messages
.unmute
, { name: account
.get('username') }), action: this.handleMuteClick
});
290 menu
.push({ text: intl
.formatMessage(messages
.mute
, { name: account
.get('username') }), action: this.handleMuteClick
});
293 if (relationship
&& relationship
.get('blocking')) {
294 menu
.push({ text: intl
.formatMessage(messages
.unblock
, { name: account
.get('username') }), action: this.handleBlockClick
});
296 menu
.push({ text: intl
.formatMessage(messages
.block
, { name: account
.get('username') }), action: this.handleBlockClick
});
299 if (!this.props
.onFilter
) {
301 menu
.push({ text: intl
.formatMessage(messages
.filter
), action: this.handleFilterClick
});
305 menu
.push({ text: intl
.formatMessage(messages
.report
, { name: account
.get('username') }), action: this.handleReport
});
307 if (account
.get('acct') !== account
.get('username')) {
308 const domain
= account
.get('acct').split('@')[1];
312 if (relationship
&& relationship
.get('domain_blocking')) {
313 menu
.push({ text: intl
.formatMessage(messages
.unblockDomain
, { domain
}), action: this.handleUnblockDomain
});
315 menu
.push({ text: intl
.formatMessage(messages
.blockDomain
, { domain
}), action: this.handleBlockDomain
});
319 if ((this.context
.identity
.permissions
& PERMISSION_MANAGE_USERS
) === PERMISSION_MANAGE_USERS
) {
321 menu
.push({ text: intl
.formatMessage(messages
.admin_account
, { name: account
.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
322 menu
.push({ text: intl
.formatMessage(messages
.admin_status
), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` });
328 if (status
.get('in_reply_to_id', null) === null) {
330 replyTitle
= intl
.formatMessage(messages
.reply
);
332 replyIcon
= 'reply-all';
333 replyTitle
= intl
.formatMessage(messages
.replyAll
);
336 const reblogPrivate
= status
.getIn(['account', 'id']) === me
&& status
.get('visibility') === 'private';
338 let reblogTitle
= '';
339 if (status
.get('reblogged')) {
340 reblogTitle
= intl
.formatMessage(messages
.cancel_reblog_private
);
341 } else if (publicStatus
) {
342 reblogTitle
= intl
.formatMessage(messages
.reblog
);
343 } else if (reblogPrivate
) {
344 reblogTitle
= intl
.formatMessage(messages
.reblog_private
);
346 reblogTitle
= intl
.formatMessage(messages
.cannot_reblog
);
349 const shareButton
= ('share' in navigator
) && publicStatus
&& (
350 <IconButton className
='status__action-bar-button' title
={intl
.formatMessage(messages
.share
)} icon
='share-alt' onClick
={this.handleShareClick
} />
353 const filterButton
= this.props
.onFilter
&& (
354 <IconButton className
='status__action-bar-button' title
={intl
.formatMessage(messages
.hide
)} icon
='eye' onClick
={this.handleHideClick
} />
358 <div className
='status__action-bar'>
359 <IconButton className
='status__action-bar-button' title
={replyTitle
} icon
={status
.get('in_reply_to_account_id') === status
.getIn(['account', 'id']) ? 'reply' : replyIcon
} onClick
={this.handleReplyClick
} counter
={status
.get('replies_count')} obfuscateCount
/>
360 <IconButton className
={classNames('status__action-bar-button', { reblogPrivate
})} disabled
={!publicStatus
&& !reblogPrivate
} active
={status
.get('reblogged')} pressed
={status
.get('reblogged')} title
={reblogTitle
} icon
='retweet' onClick
={this.handleReblogClick
} counter
={withCounters
? status
.get('reblogs_count') : undefined} />
361 <IconButton className
='status__action-bar-button star-icon' animate active
={status
.get('favourited')} pressed
={status
.get('favourited')} title
={intl
.formatMessage(messages
.favourite
)} icon
='star' onClick
={this.handleFavouriteClick
} counter
={withCounters
? status
.get('favourites_count') : undefined} />
367 <div className
='status__action-bar-dropdown'>
368 <DropdownMenuContainer
369 scrollKey
={scrollKey
}
370 disabled
={anonymousAccess
}
376 title
={intl
.formatMessage(messages
.more
)}