939 lines
28 KiB
JavaScript
Executable file
939 lines
28 KiB
JavaScript
Executable file
import React from 'react';
|
|
import ReactDOM from 'react-dom';
|
|
import classNames from 'classnames';
|
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
|
import ChannelTextArea from './ChannelTextArea';
|
|
import {copy, SUPPORTS_COPY} from '../utils/ClipboardUtils';
|
|
import {toReactionEmoji} from '../utils/ReactionUtils';
|
|
import Popout from './common/Popout';
|
|
import Avatar from './common/Avatar';
|
|
import BotTag from './common/BotTag';
|
|
import Attachment from './Attachment';
|
|
import Embed from './Embed';
|
|
import InviteButton from './InviteButton';
|
|
import Tooltip from './common/Tooltip';
|
|
import SystemMessage from './SystemMessage';
|
|
import MessageReactions from './MessageReactions';
|
|
import EditMessageStore from '../stores/EditMessageStore';
|
|
import ReadStateActionCreators from '../actions/ReadStateActionCreators';
|
|
import MessageActionCreators from '../actions/MessageActionCreators';
|
|
import ChannelPinActionCreators from '../actions/ChannelPinActionCreators';
|
|
import {addReaction} from '../actions/ReactionActionCreators';
|
|
import ChatRestrictionMixin from '../mixins/web/ChatRestrictionMixin';
|
|
import MessageUtils from '../utils/MessageUtils';
|
|
import ReportUtils from '../utils/ReportUtils';
|
|
import UserStore from '../stores/UserStore';
|
|
import i18n from '../i18n';
|
|
import NativeUtils from '../utils/NativeUtils';
|
|
import ContextMenu from './common/ContextMenu';
|
|
import UserContextMenu from './contextmenus/UserContextMenu';
|
|
import MessageContextMenu from './contextmenus/MessageContextMenu';
|
|
import MessageAlerts from './MessageAlerts';
|
|
import PermissionMixin from '../mixins/PermissionMixin';
|
|
import Flux from '../lib/flux';
|
|
import shallowEqual from '../../discord_common/js/lib/shallowEqual';
|
|
import BackgroundFlash from './common/animated/BackgroundFlash';
|
|
import ModalActionCreators from '../actions/ModalActionCreators';
|
|
import MessageReactionsModal from './MessageReactionsModal';
|
|
import ReportModal from './ReportModal';
|
|
import {hasAnimatedAvatar} from '../utils/AvatarUtils';
|
|
import {EmojiPicker} from './EmojiPicker';
|
|
import {
|
|
MessageTypesDeletable,
|
|
TextareaTypes,
|
|
MessageStates,
|
|
ContextMenuTypes,
|
|
MessageTypes,
|
|
ComponentActions,
|
|
} from '../Constants';
|
|
import UserPopout from './UserPopout';
|
|
import '../styles/confirm_message_modal.styl';
|
|
|
|
const EDIT_MESSAGE_REF = 'EDIT_MESSAGE_REF';
|
|
|
|
const OptionPopout = React.createClass({
|
|
mixins: [PureRenderMixin, PermissionMixin],
|
|
|
|
propTypes: {
|
|
channel: React.PropTypes.object.isRequired,
|
|
message: React.PropTypes.object.isRequired,
|
|
canPin: React.PropTypes.bool,
|
|
canEdit: React.PropTypes.bool,
|
|
developerMode: React.PropTypes.bool,
|
|
canDelete: React.PropTypes.bool,
|
|
},
|
|
|
|
handleCopyId() {
|
|
copy(this.props.message.id);
|
|
this.props.onClose();
|
|
},
|
|
|
|
handleEdit() {
|
|
MessageActionCreators.startEditMessage(this.props.channel.id, this.props.message.id, this.props.message.content);
|
|
},
|
|
|
|
handleRetry() {
|
|
const {channel, message} = this.props;
|
|
MessageActionCreators.retrySendMessage(channel, message.id, message.content, message.tts);
|
|
},
|
|
|
|
handleDelete() {
|
|
if (this.props.message.state === MessageStates.SEND_FAILED) {
|
|
MessageActionCreators.deleteMessage(this.props.channel.id, this.props.message.id, true);
|
|
} else {
|
|
MessageAlerts.confirmDelete(this.props.channel, this.props.message);
|
|
}
|
|
},
|
|
|
|
handleReactions() {
|
|
ModalActionCreators.push(MessageReactionsModal, {message: this.props.message});
|
|
},
|
|
|
|
handlePin() {
|
|
MessageAlerts.confirmPin(this.props.channel, this.props.message);
|
|
this.props.onClose();
|
|
},
|
|
|
|
handleReport() {
|
|
ModalActionCreators.push(ReportModal, {
|
|
message: this.props.message,
|
|
});
|
|
this.props.onClose();
|
|
},
|
|
|
|
handleUnpin(e) {
|
|
if (e.shiftKey) {
|
|
ChannelPinActionCreators.unpinMessage(this.props.channel, this.props.message.id);
|
|
} else {
|
|
MessageAlerts.confirmUnpin(this.props.channel, this.props.message);
|
|
}
|
|
|
|
this.props.onClose();
|
|
},
|
|
|
|
render() {
|
|
const {message} = this.props;
|
|
let retryButton;
|
|
let editButton;
|
|
let pinButton;
|
|
let copyIdButton;
|
|
let deleteButton;
|
|
let reactionsButton;
|
|
let reportButton;
|
|
|
|
if (message.state === MessageStates.SEND_FAILED) {
|
|
retryButton = <div className="btn-item" onClick={this.handleRetry}>{i18n.Messages.RETRY}</div>;
|
|
} else {
|
|
const currentUser = UserStore.getCurrentUser();
|
|
if (this.props.canEdit && message.author.id === currentUser.id) {
|
|
editButton = <div className="btn-item" onClick={this.handleEdit}>{i18n.Messages.EDIT}</div>;
|
|
}
|
|
|
|
if (this.props.canPin) {
|
|
if (message.pinned === false) {
|
|
pinButton = <div className="btn-item" onClick={this.handlePin}>{i18n.Messages.PIN}</div>;
|
|
} else {
|
|
pinButton = <div className="btn-item" onClick={this.handleUnpin}>{i18n.Messages.UNPIN}</div>;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (SUPPORTS_COPY && this.props.developerMode) {
|
|
copyIdButton = <div className="btn-item" onClick={this.handleCopyId}>{i18n.Messages.COPY_ID}</div>;
|
|
}
|
|
|
|
if (this.props.canDelete) {
|
|
deleteButton = (
|
|
<div className="btn-item" onClick={this.handleDelete}>
|
|
{i18n.Messages.DELETE}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (message.reactions.length) {
|
|
reactionsButton = (
|
|
<div className="btn-item" onClick={this.handleReactions}>
|
|
{i18n.Messages.REACTIONS}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (this.props.canReport) {
|
|
reportButton = (
|
|
<div className="btn-item" onClick={this.handleReport}>
|
|
{i18n.Messages.REPORT}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="option-popout small-popout-box">
|
|
{pinButton}
|
|
{editButton}
|
|
{retryButton}
|
|
{copyIdButton}
|
|
{deleteButton}
|
|
{reactionsButton}
|
|
{reportButton}
|
|
</div>
|
|
);
|
|
},
|
|
});
|
|
|
|
const Message = React.createClass({
|
|
mixins: [Flux.StoreListenerMixin(EditMessageStore), PureRenderMixin, ChatRestrictionMixin],
|
|
|
|
propTypes: {
|
|
first: React.PropTypes.bool.isRequired,
|
|
compact: React.PropTypes.bool.isRequired,
|
|
canDelete: React.PropTypes.bool,
|
|
canPin: React.PropTypes.bool,
|
|
canAddReactions: React.PropTypes.bool,
|
|
canAddNewReactions: React.PropTypes.bool,
|
|
canFlash: React.PropTypes.bool,
|
|
canReport: React.PropTypes.bool,
|
|
channel: React.PropTypes.object.isRequired,
|
|
message: React.PropTypes.object.isRequired,
|
|
onUpdate: React.PropTypes.func,
|
|
onEdit: React.PropTypes.func,
|
|
onToggleEditing: React.PropTypes.func,
|
|
inlineAttachmentMedia: React.PropTypes.bool.isRequired,
|
|
inlineEmbedMedia: React.PropTypes.bool.isRequired,
|
|
renderEmbeds: React.PropTypes.bool.isRequired,
|
|
renderReactions: React.PropTypes.bool.isRequired,
|
|
avatarSize: React.PropTypes.oneOf(Avatar.SIZES),
|
|
popoutPosition: React.PropTypes.oneOf(Popout.POSITIONS),
|
|
option: React.PropTypes.bool,
|
|
canEdit: React.PropTypes.bool,
|
|
jumpSequenceId: React.PropTypes.number,
|
|
developerMode: React.PropTypes.bool,
|
|
renderMentioned: React.PropTypes.bool,
|
|
},
|
|
|
|
getStateFromStores() {
|
|
return {
|
|
isEditing: this.props.canEdit && EditMessageStore.isEditing(this.props.channel.id, this.props.message.id),
|
|
};
|
|
},
|
|
|
|
componentWillMount() {
|
|
this._mounted = false;
|
|
},
|
|
|
|
componentDidMount() {
|
|
this._mounted = true;
|
|
},
|
|
|
|
componentDidUpdate(prevProps, prevState) {
|
|
const {message} = this.props;
|
|
if (message.embeds !== prevProps.message.embeds || message.reactions !== prevProps.message.reactions) {
|
|
this.props.onUpdate && this.props.onUpdate(ReactDOM.findDOMNode(this));
|
|
}
|
|
if (this.state.isEditing !== prevState.isEditing) {
|
|
this.props.onToggleEditing && this.props.onToggleEditing(this.state.isEditing);
|
|
if (this.state.isEditing) {
|
|
this.props.onEdit && this.props.onEdit(ReactDOM.findDOMNode(this));
|
|
}
|
|
}
|
|
},
|
|
|
|
handleCancelEditing() {
|
|
MessageActionCreators.endEditMessage();
|
|
},
|
|
|
|
handleSaveClick() {
|
|
this.handleSaveEditing(ReactDOM.findDOMNode(this.refs[EDIT_MESSAGE_REF].refs['textarea']).value);
|
|
},
|
|
|
|
handleSaveEditing(value) {
|
|
if (value.length == 0) {
|
|
MessageAlerts.confirmDelete(this.props.channel, this.props.message);
|
|
} else if (this.applyChatRestrictions(value)) {
|
|
// don't cancel editing just bail
|
|
return;
|
|
} else if (value !== EditMessageStore.getEditingContent()) {
|
|
const result = MessageUtils.parse(this.props.channel, value, true);
|
|
MessageActionCreators.editMessage(this.props.channel.id, this.props.message.id, result);
|
|
}
|
|
this.handleCancelEditing();
|
|
},
|
|
|
|
handleKeyDown(e) {
|
|
if (e.which === 27 /* ESCAPE */ && !e.shiftKey) {
|
|
e.preventDefault();
|
|
this.handleCancelEditing();
|
|
}
|
|
},
|
|
|
|
handleTextareaResize() {
|
|
this.props.onEdit && this.props.onEdit(ReactDOM.findDOMNode(this));
|
|
},
|
|
|
|
handleManualAck(e) {
|
|
if (e.altKey) {
|
|
e.preventDefault();
|
|
ReadStateActionCreators.manualAck(this.props.channel.id, this.props.message.id);
|
|
}
|
|
},
|
|
|
|
renderOptionPopout(props) {
|
|
return (
|
|
<OptionPopout
|
|
{...props}
|
|
channel={this.props.channel}
|
|
message={this.props.message}
|
|
developerMode={this.props.developerMode}
|
|
canEdit={this.props.canEdit}
|
|
canPin={this.props.canPin}
|
|
canDelete={this.props.canDelete}
|
|
canReport={this.props.canReport}
|
|
/>
|
|
);
|
|
},
|
|
|
|
renderUserPopout(props) {
|
|
const {channel, message} = this.props;
|
|
return <UserPopout {...props} user={message.author} guildId={channel.getGuildId()} channelId={channel.id} />;
|
|
},
|
|
|
|
renderUsername(message) {
|
|
const style = message.colorString ? {color: message.colorString} : null;
|
|
|
|
if (this.props.compact) {
|
|
return (
|
|
<Popout closeOnScroll={false} render={this.renderUserPopout} position={this.props.popoutPosition}>
|
|
<span className="username-wrapper" onContextMenu={this.props.onContextMenu}>
|
|
{message.author.bot ? <BotTag /> : null}
|
|
<strong className="user-name" style={style}>
|
|
{message.nick || message.author.toString()}
|
|
</strong>
|
|
<i className="highlight-separator right-pad">: </i>
|
|
</span>
|
|
</Popout>
|
|
);
|
|
} else {
|
|
return (
|
|
<Popout closeOnScroll={false} render={this.renderUserPopout} position={this.props.popoutPosition}>
|
|
<span className="username-wrapper" onContextMenu={this.props.onContextMenu}>
|
|
<strong className="user-name" style={style}>
|
|
{message.nick || message.author.toString()}
|
|
</strong>
|
|
{message.author.bot ? <BotTag /> : null}
|
|
</span>
|
|
</Popout>
|
|
);
|
|
}
|
|
},
|
|
|
|
renderAttachments(message) {
|
|
if (message.attachments.length) {
|
|
return message.attachments.map(attachment => {
|
|
return (
|
|
<Attachment
|
|
key={attachment.id}
|
|
attachment={attachment}
|
|
inlineMedia={this.props.inlineAttachmentMedia}
|
|
onContextMenu={e => this.handleMessageContextMenu(e, {attachment})}
|
|
/>
|
|
);
|
|
});
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
renderEmbeds(message) {
|
|
const {renderEmbeds, inlineEmbedMedia, channel} = this.props;
|
|
if (message.embeds.length && renderEmbeds) {
|
|
return message.embeds.map((embed, i) =>
|
|
<Embed key={i} {...embed} message={message} inlineMedia={inlineEmbedMedia} channelId={channel.id} />
|
|
);
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
renderOptionButton(message, isEditing, isSending) {
|
|
if (this.props.option === false) {
|
|
return null;
|
|
}
|
|
|
|
// We cannot actually modify local bot messages, so skip rendering the button
|
|
if (message.author.isLocalBot()) return;
|
|
|
|
const currentUser = UserStore.getCurrentUser();
|
|
const showOptionsPopout =
|
|
!isEditing &&
|
|
!isSending &&
|
|
(message.author.id === currentUser.id ||
|
|
this.props.canDelete ||
|
|
this.props.canPin ||
|
|
this.props.canReport ||
|
|
(SUPPORTS_COPY && this.props.developerMode));
|
|
if (showOptionsPopout) {
|
|
return (
|
|
<Popout closeOnScroll={false} render={this.renderOptionPopout} position={Popout.BOTTOM} offsetY={-6}>
|
|
<div className="btn-option" />
|
|
</Popout>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
renderReactionPopout(props) {
|
|
const {channel} = this.props;
|
|
return <EmojiPicker {...props} channel={channel} onSelectEmoji={this.handleAddReaction} />;
|
|
},
|
|
|
|
handleAddReaction(emoji) {
|
|
// Emoji equaling null means that the emoji picker was closed, and we shouldn't do anything.
|
|
if (emoji == null) return;
|
|
|
|
const {channel, message} = this.props;
|
|
addReaction(channel.id, message.id, toReactionEmoji(emoji));
|
|
},
|
|
|
|
renderReactionButton(message, left = true) {
|
|
if (
|
|
this.props.option === false ||
|
|
this.state.isEditing ||
|
|
message.state === MessageStates.SENDING ||
|
|
!this.props.canAddNewReactions ||
|
|
!this.props.renderReactions ||
|
|
message.author.isLocalBot()
|
|
)
|
|
return;
|
|
return (
|
|
<Popout
|
|
closeOnScroll={false}
|
|
render={this.renderReactionPopout}
|
|
position={left ? Popout.LEFT : Popout.RIGHT}
|
|
offsetX={left ? -10 : 0}
|
|
tooltip={i18n.Messages.ADD_REACTION}
|
|
subscribeTo={left ? ComponentActions.TOGGLE_REACTION_POPOUT(message.id) : null}>
|
|
<div className="btn-reaction" />
|
|
</Popout>
|
|
);
|
|
},
|
|
|
|
renderInvites(message) {
|
|
if (message.invites.length) {
|
|
return message.invites.map(code => <InviteButton key={code} code={code} />);
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
renderReactions(message) {
|
|
if (this.props.renderReactions && message.reactions.length) {
|
|
return (
|
|
<MessageReactions message={message} transitionAppear={this._mounted} readOnly={!this.props.canAddReactions}>
|
|
{this.renderReactionButton(message, false)}
|
|
</MessageReactions>
|
|
);
|
|
}
|
|
return null;
|
|
},
|
|
|
|
renderContent(message) {
|
|
let edited;
|
|
if (message.isEdited()) {
|
|
edited = (
|
|
<Tooltip text={message.editedTimestamp.calendar()}>
|
|
<span className="edited">({i18n.Messages.MESSAGE_EDITED})</span>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
if (this.props.compact) {
|
|
return (
|
|
<div className="markup">
|
|
{this.renderTimestamp(message)}
|
|
{this.renderUsername(message)}
|
|
<span className="message-content">
|
|
{message.contentParsed}
|
|
{edited}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="markup">
|
|
{message.contentParsed}
|
|
{edited}
|
|
</div>
|
|
);
|
|
},
|
|
|
|
renderTimestamp(message) {
|
|
if (this.props.compact) {
|
|
return (
|
|
<span className="timestamp">
|
|
<i className="highlight-separator left-pad">[</i>
|
|
{message.timestamp.format('LT')}
|
|
<i className="highlight-separator right-pad">] </i>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return <span className="timestamp">{message.timestamp.calendar()}</span>;
|
|
},
|
|
|
|
renderAccessories(message) {
|
|
return (
|
|
<div className="accessory">
|
|
{this.renderInvites(message)}
|
|
{this.renderAttachments(message)}
|
|
{this.renderEmbeds(message)}
|
|
{this.renderReactions(message)}
|
|
</div>
|
|
);
|
|
},
|
|
|
|
renderHeader(message, isEditing) {
|
|
if (!this.props.compact && (this.props.first || isEditing)) {
|
|
return (
|
|
<h2>
|
|
{this.renderUsername(message)}
|
|
<span className="highlight-separator"> - </span>
|
|
{this.renderTimestamp(message)}
|
|
</h2>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
handleSystemMessageContextMenu(event) {
|
|
if (!NativeUtils.embedded) return;
|
|
|
|
const {target} = event;
|
|
|
|
ContextMenu.openContextMenu(event, props =>
|
|
<MessageContextMenu
|
|
{...props}
|
|
type={ContextMenuTypes.MESSAGE_SYSTEM}
|
|
message={this.props.message}
|
|
channel={this.props.channel}
|
|
target={target}
|
|
/>
|
|
);
|
|
},
|
|
|
|
handleMessageContextMenu(event, extraProps) {
|
|
if (!NativeUtils.embedded) return;
|
|
|
|
// event is of type SyntheticEvent and is pooled, so we can't use event.target later in the callback
|
|
// see https://fb.me/react-event-pooling for more info.
|
|
const {target} = event;
|
|
|
|
ContextMenu.openContextMenu(event, props =>
|
|
<MessageContextMenu
|
|
{...props}
|
|
{...extraProps}
|
|
type={ContextMenuTypes.MESSAGE_MAIN}
|
|
message={this.props.message}
|
|
channel={this.props.channel}
|
|
target={target}
|
|
/>
|
|
);
|
|
},
|
|
|
|
render() {
|
|
const {message} = this.props;
|
|
const isEditing = this.state.isEditing;
|
|
const isSending = message.state === MessageStates.SENDING;
|
|
|
|
const header = this.renderHeader(message, isEditing);
|
|
const option = this.renderOptionButton(message, isEditing, isSending);
|
|
const reactionButton = this.renderReactionButton(message);
|
|
|
|
if (message.type > MessageTypes.DEFAULT) {
|
|
return (
|
|
<div
|
|
className={classNames('message', classes)}
|
|
onContextMenu={MessageTypesDeletable[message.type] && this.handleSystemMessageContextMenu}>
|
|
<SystemMessage message={message} option={option} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
let classes;
|
|
if (isEditing) {
|
|
const defaultValue = EditMessageStore.getEditingContent();
|
|
const editTextarea = (
|
|
<ChannelTextArea
|
|
ref={EDIT_MESSAGE_REF}
|
|
key={message.id}
|
|
channel={this.props.channel}
|
|
type={TextareaTypes.EDIT}
|
|
onSubmit={this.handleSaveEditing}
|
|
onKeyDown={this.handleKeyDown}
|
|
onResize={this.handleTextareaResize}
|
|
defaultValue={defaultValue}
|
|
/>
|
|
);
|
|
|
|
const editOperation = (
|
|
<div className="edit-operation">
|
|
{i18n.Messages.EDIT_TEXTAREA_HELP.format({
|
|
onCancel: this.handleCancelEditing,
|
|
onSave: this.handleSaveClick,
|
|
})}
|
|
</div>
|
|
);
|
|
|
|
classes = {
|
|
'edit-message': true,
|
|
'edit-first-message': this.props.first,
|
|
};
|
|
|
|
if (this.props.compact) {
|
|
return (
|
|
<div>
|
|
<div className={classNames(classes)}>
|
|
<div className="edit-container-outer">
|
|
{this.renderTimestamp(message)}
|
|
<div className="edit-container-inner">
|
|
{editTextarea}
|
|
{editOperation}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{this.renderAccessories(message)}
|
|
</div>
|
|
);
|
|
} else {
|
|
return (
|
|
<div>
|
|
<div className={classNames(classes)}>
|
|
<div className="edit-container-outer">
|
|
<Avatar user={message.author} size={this.props.avatarSize} animate />
|
|
<div className="edit-container-inner">
|
|
{header}
|
|
{editTextarea}
|
|
{editOperation}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{this.renderAccessories(message)}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
const content = this.renderContent(message);
|
|
|
|
classes = {
|
|
'message-sending': isSending,
|
|
'message-send-failed': message.state === MessageStates.SEND_FAILED,
|
|
mentioned: this.props.renderMentioned && message.mentioned,
|
|
first: this.props.first,
|
|
};
|
|
|
|
let body = null;
|
|
if (this.props.compact) {
|
|
body = (
|
|
<div className={classNames('message', classes)} onClick={this.handleManualAck}>
|
|
<div className="message-text" onContextMenu={this.handleMessageContextMenu}>
|
|
{option}
|
|
{reactionButton}
|
|
{content}
|
|
</div>
|
|
{this.renderAccessories(message)}
|
|
</div>
|
|
);
|
|
} else {
|
|
body = (
|
|
<div className={classNames('message', classes)} onClick={this.handleManualAck}>
|
|
<div className="body">
|
|
{header}
|
|
<div className="message-text" onContextMenu={this.handleMessageContextMenu}>
|
|
{option}
|
|
{reactionButton}
|
|
{content}
|
|
</div>
|
|
</div>
|
|
{this.renderAccessories(message)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (this.props.jumpSequenceId && this.props.canFlash) {
|
|
return <BackgroundFlash sequenceId={this.props.jumpSequenceId}>{body}</BackgroundFlash>;
|
|
}
|
|
|
|
return body;
|
|
},
|
|
});
|
|
|
|
const MessageGroup = React.createClass({
|
|
propTypes: {
|
|
hasDivider: React.PropTypes.bool,
|
|
canManageMessages: React.PropTypes.bool,
|
|
channel: React.PropTypes.object.isRequired,
|
|
messages: React.PropTypes.array.isRequired,
|
|
onUpdate: React.PropTypes.func,
|
|
onEdit: React.PropTypes.func,
|
|
inlineAttachmentMedia: React.PropTypes.bool,
|
|
inlineEmbedMedia: React.PropTypes.bool,
|
|
renderEmbeds: React.PropTypes.bool,
|
|
renderReactions: React.PropTypes.bool,
|
|
avatarSize: React.PropTypes.oneOf(Avatar.SIZES),
|
|
popoutPosition: React.PropTypes.oneOf(Popout.POSITIONS),
|
|
compact: React.PropTypes.bool,
|
|
groupOption: React.PropTypes.func,
|
|
messageOption: React.PropTypes.bool,
|
|
jumpTargetIndex: React.PropTypes.number,
|
|
jumpSequenceId: React.PropTypes.number,
|
|
canFlash: React.PropTypes.bool,
|
|
enableInteraction: React.PropTypes.bool,
|
|
developerMode: React.PropTypes.bool,
|
|
renderMentioned: React.PropTypes.bool,
|
|
canAddReactions: React.PropTypes.bool,
|
|
canChat: React.PropTypes.bool,
|
|
canReport: React.PropTypes.bool,
|
|
},
|
|
|
|
getDefaultProps() {
|
|
return {
|
|
canManageMessages: false,
|
|
hasDivider: false,
|
|
popoutPosition: Popout.RIGHT,
|
|
avatarSize: 'large',
|
|
compact: false,
|
|
messageOption: true,
|
|
canEdit: true,
|
|
jumpTargetIndex: -1,
|
|
enableInteraction: true,
|
|
developerMode: false,
|
|
canFlash: true,
|
|
renderMentioned: true,
|
|
inlineAttachmentMedia: false,
|
|
inlineEmbedMedia: false,
|
|
renderEmbeds: false,
|
|
renderReactions: true,
|
|
canAddReactions: false,
|
|
canChat: false,
|
|
canReport: false,
|
|
};
|
|
},
|
|
|
|
getInitialState() {
|
|
const {messages: [message]} = this.props;
|
|
return {
|
|
animatedAvatar: hasAnimatedAvatar(message.author),
|
|
animate: false,
|
|
isEditing: false,
|
|
};
|
|
},
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
const {messages: [message]} = nextProps;
|
|
const animatedAvatar = hasAnimatedAvatar(message.author);
|
|
if (animatedAvatar !== this.state.animatedAvatar) {
|
|
this.setState({animatedAvatar});
|
|
}
|
|
},
|
|
|
|
shouldComponentUpdate(nextProps, nextState) {
|
|
if (!shallowEqual(this.props, nextProps, ['messages']) || !shallowEqual(this.state, nextState)) {
|
|
return true;
|
|
}
|
|
|
|
const {messages} = this.props;
|
|
if (messages.length !== nextProps.messages.length) {
|
|
return true;
|
|
}
|
|
|
|
for (let i = 0; i < messages.length; i++) {
|
|
if (messages[i] !== nextProps.messages[i]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
getMessage(index) {
|
|
return this.refs[index];
|
|
},
|
|
|
|
onToggleEditing(isEditing) {
|
|
this.setState({isEditing});
|
|
},
|
|
|
|
renderUserPopout(props) {
|
|
const {channel, messages: [message]} = this.props;
|
|
return <UserPopout {...props} user={message.author} guildId={channel.getGuildId()} channelId={channel.id} />;
|
|
},
|
|
|
|
render() {
|
|
const {channel, compact} = this.props;
|
|
const {animatedAvatar, animate} = this.state;
|
|
const author = this.props.messages[0].author;
|
|
const isLocalBot = author.isLocalBot();
|
|
|
|
const className = classNames({
|
|
'message-group': true,
|
|
'has-divider': this.props.hasDivider,
|
|
'hide-overflow': !this.state.isEditing,
|
|
'is-local-bot-message': isLocalBot,
|
|
compact,
|
|
});
|
|
|
|
const isSystemMessage = this.props.messages[0].type > MessageTypes.DEFAULT;
|
|
const canDelete = this.props.canManageMessages || author.id === UserStore.getCurrentUser().id;
|
|
const canPin = !isSystemMessage && (this.props.canManageMessages || channel.isPrivate());
|
|
const canEdit = !isSystemMessage && this.props.canEdit;
|
|
const canAddNewReactions = !isSystemMessage && (this.props.canAddNewReactions || channel.isPrivate());
|
|
const canAddReactions = !isSystemMessage && (this.props.canChat || channel.isPrivate());
|
|
const canReport = this.props.canReport && ReportUtils.canReportUser(author);
|
|
|
|
const messages = this.props.messages.map((message, i) => {
|
|
return (
|
|
<Message
|
|
key={message.id}
|
|
ref={i}
|
|
first={i === 0}
|
|
renderMentioned={this.props.renderMentioned}
|
|
jumpSequenceId={this.props.jumpTargetIndex === i ? this.props.jumpSequenceId : null}
|
|
canFlash={this.props.canFlash}
|
|
compact={compact}
|
|
channel={channel}
|
|
message={message}
|
|
inlineAttachmentMedia={this.props.inlineAttachmentMedia}
|
|
inlineEmbedMedia={this.props.inlineEmbedMedia}
|
|
renderEmbeds={this.props.renderEmbeds}
|
|
renderReactions={this.props.renderReactions}
|
|
canPin={canPin}
|
|
canDelete={canDelete}
|
|
onEdit={this.props.onEdit}
|
|
onToggleEditing={this.onToggleEditing}
|
|
onUpdate={this.props.onUpdate}
|
|
avatarSize={this.props.avatarSize}
|
|
popoutPosition={this.props.popoutPosition}
|
|
onContextMenu={this.handleContextMenu}
|
|
option={this.props.messageOption}
|
|
developerMode={this.props.developerMode}
|
|
canAddReactions={canAddReactions}
|
|
canAddNewReactions={canAddNewReactions}
|
|
canEdit={canEdit}
|
|
canReport={canReport}
|
|
/>
|
|
);
|
|
});
|
|
|
|
let avatar;
|
|
if (!compact) {
|
|
avatar = (
|
|
<Popout closeOnScroll={false} render={this.renderUserPopout} position={Popout.RIGHT}>
|
|
<Avatar user={author} onContextMenu={this.handleContextMenu} size={this.props.avatarSize} animate={animate} />
|
|
</Popout>
|
|
);
|
|
}
|
|
|
|
let clickWrapper;
|
|
if (this.props.onClickAnywhere || !this.props.enableInteraction) {
|
|
clickWrapper = (
|
|
<div
|
|
className={classNames('sink-interactions', {clickable: this.props.onClickAnywhere})}
|
|
onClick={this.props.onClickAnywhere}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (isSystemMessage) {
|
|
return (
|
|
<div className={className}>
|
|
<div className="comment">{messages}</div>
|
|
{clickWrapper}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
let localBotMessage;
|
|
if (isLocalBot) {
|
|
localBotMessage = (
|
|
<div className="local-bot-message">
|
|
{i18n.Messages.ONLY_YOU_CAN_SEE_AND_DELETE_THESE.format({
|
|
count: messages.length,
|
|
handleDelete: this.handleDeleteMessageGroup,
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={className}
|
|
onMouseEnter={animatedAvatar && this.onMouseEnter}
|
|
onMouseLeave={animatedAvatar && this.onMouseLeave}>
|
|
{avatar}
|
|
<div className="comment">{messages}{localBotMessage}</div>
|
|
{clickWrapper}
|
|
{this.props.groupOption && this.props.groupOption(this.props.messages)}
|
|
</div>
|
|
);
|
|
},
|
|
|
|
onMouseEnter() {
|
|
this.setState({animate: true});
|
|
},
|
|
|
|
onMouseLeave() {
|
|
this.setState({animate: false});
|
|
},
|
|
|
|
handleContextMenu(event) {
|
|
const {channel, messages: [nearestMessage]} = this.props;
|
|
const {author: user} = nearestMessage;
|
|
|
|
if (channel.isGroupDM()) {
|
|
ContextMenu.openContextMenu(event, props =>
|
|
<UserContextMenu
|
|
{...props}
|
|
type={ContextMenuTypes.USER_GROUP_DM}
|
|
user={user}
|
|
nearestMessage={nearestMessage}
|
|
channelId={channel.id}
|
|
/>
|
|
);
|
|
} else if (channel.isDM()) {
|
|
ContextMenu.openContextMenu(event, props =>
|
|
<UserContextMenu
|
|
{...props}
|
|
type={ContextMenuTypes.USER_PRIVATE_CHANNELS_MESSAGE}
|
|
user={user}
|
|
channelId={channel.id}
|
|
nearestMessage={nearestMessage}
|
|
selected={true}
|
|
/>
|
|
);
|
|
} else {
|
|
ContextMenu.openContextMenu(event, props =>
|
|
<UserContextMenu
|
|
{...props}
|
|
type={ContextMenuTypes.USER_CHANNEL_MESSAGE}
|
|
user={user}
|
|
channelId={channel.id}
|
|
nearestMessage={nearestMessage}
|
|
guildId={channel['guild_id']}
|
|
/>
|
|
);
|
|
}
|
|
},
|
|
handleDeleteMessageGroup() {
|
|
const {messages, channel} = this.props;
|
|
messages.forEach(message => MessageActionCreators.deleteMessage(channel.id, message.id, true));
|
|
},
|
|
});
|
|
|
|
export default MessageGroup;
|
|
|
|
|
|
|
|
// WEBPACK FOOTER //
|
|
// ./discord_app/components/MessageGroup.js
|