mastodon/app/javascript/mastodon/features/direct_timeline/components/conversation.js

250 lines
7.5 KiB
JavaScript

import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import StatusContent from 'mastodon/components/status_content';
import AttachmentList from 'mastodon/components/attachment_list';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import AvatarComposite from 'mastodon/components/avatar_composite';
import Permalink from 'mastodon/components/permalink';
import IconButton from 'mastodon/components/icon_button';
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import { HotKeys } from 'react-hotkeys';
import { autoPlayGif } from 'mastodon/initial_state';
import classNames from 'classnames';
const messages = defineMessages({
more : { id: 'status.more', defaultMessage: 'More' },
open : { id: 'conversation.open', defaultMessage: 'View conversation' },
reply : { id: 'status.reply', defaultMessage: 'Reply' },
markAsRead : { id: 'conversation.mark_as_read', defaultMessage: 'Mark as read' },
delete : { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
muteConversation : { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
});
export default @injectIntl
class Conversation extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
conversationId: PropTypes.string.isRequired,
accounts : ImmutablePropTypes.list.isRequired,
lastStatus : ImmutablePropTypes.map,
unread : PropTypes.bool.isRequired,
onMoveUp : PropTypes.func,
onMoveDown : PropTypes.func,
markRead : PropTypes.func.isRequired,
delete : PropTypes.func.isRequired,
intl : PropTypes.object.isRequired,
};
_updateEmojis() {
const node = this.namesNode;
if (!node || autoPlayGif) {
return;
}
const emojis = node.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
if (emoji.classList.contains('status-emoji')) {
continue;
}
emoji.classList.add('status-emoji');
emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
}
}
componentDidMount() {
this._updateEmojis();
}
componentDidUpdate() {
this._updateEmojis();
}
handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original');
};
handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static');
};
handleClick = () => {
if (!this.context.router) {
return;
}
const { lastStatus, unread, markRead } = this.props;
if (unread) {
markRead();
}
this.context.router.history.push(`/statuses/${lastStatus.get('id')}`);
};
handleMarkAsRead = () => {
this.props.markRead();
};
handleReply = () => {
this.props.reply(this.props.lastStatus, this.context.router.history);
};
handleDelete = () => {
this.props.delete();
};
handleHotkeyMoveUp = () => {
this.props.onMoveUp(this.props.conversationId);
};
handleHotkeyMoveDown = () => {
this.props.onMoveDown(this.props.conversationId);
};
handleConversationMute = () => {
this.props.onMute(this.props.lastStatus);
};
handleShowMore = () => {
this.props.onToggleHidden(this.props.lastStatus);
};
setNamesRef = (c) => {
this.namesNode = c;
};
render() {
const { accounts, lastStatus, unread, intl } = this.props;
if (lastStatus === null) {
return null;
}
const menu = [
{ text: intl.formatMessage(messages.open), action: this.handleClick },
null,
];
menu.push({
text : intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation),
action: this.handleConversationMute,
});
if (unread) {
menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead });
menu.push(null);
}
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
const names = accounts.map(a => (<Permalink
to={`/accounts/${a.get('id')}`}
href={a.get('url')}
key={a.get('id')}
title={a.get('acct')}
>
<bdi ><strong
className='display-name__html'
dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
/></bdi >
</Permalink >)).reduce((prev, cur) => [prev, ', ', cur]);
const handlers = {
reply : this.handleReply,
open : this.handleClick,
moveUp : this.handleHotkeyMoveUp,
moveDown : this.handleHotkeyMoveDown,
toggleHidden: this.handleShowMore,
};
return (
<HotKeys handlers={handlers}>
<div
className={classNames('conversation focusable muted', { 'conversation--unread': unread })}
tabIndex='0'
>
<div className='conversation__avatar'>
<AvatarComposite
accounts={accounts}
size={55}
/>
</div >
<div className='conversation__content'>
<div className='conversation__content__info'>
<div className='conversation__content__relative-time'>
<span className='conversation_created-at'>
{lastStatus.get('created_at').substr(0, 10)}
</span >
{unread && <span className='conversation__unread' />}
<RelativeTimestamp timestamp={lastStatus.get('created_at')} />
</div >
<div
className='conversation__content__names'
ref={this.setNamesRef}
>
<FormattedMessage
id='conversation.with'
defaultMessage='With {names}'
values={{ names: <span >{names}</span > }}
/>
</div >
</div >
<StatusContent
status={lastStatus}
onClick={this.handleClick}
expanded={!lastStatus.get('hidden')}
onExpandedToggle={this.handleShowMore}
collapsable
/>
{lastStatus.get('media_attachments').size > 0 && (
<AttachmentList
compact
media={lastStatus.get('media_attachments')}
/>
)}
<div className='status__action-bar'>
<div className='status__action-bar-dropdown'>
<DropdownMenuContainer
status={lastStatus}
items={menu}
icon='ellipsis-h'
direction='right'
style={{ width: '15em' }}
size={18}
title={intl.formatMessage(messages.more)}
/>
</div >
<IconButton
className='status__action-bar-button conversation_reply'
title={intl.formatMessage(messages.reply)}
icon='reply'
size={40}
onClick={this.handleReply}
/>
</div >
</div >
</div >
</HotKeys >
);
}
}