2017-06-08_509bba0/509bba0_unpacked_with_node_modules/discord_app/components/Channel.js
2022-07-26 10:06:20 -07:00

589 lines
18 KiB
JavaScript
Executable file

import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import classNames from 'classnames';
import Helmet from 'react-helmet';
import ChannelMembers from './ChannelMembers';
import Messages from './Messages';
import TypingUsers from './TypingUsers';
import Status from './Status';
import ChannelContextMenu from './contextmenus/ChannelContextMenu';
import UserContextMenu from './contextmenus/UserContextMenu';
import ContextMenu from './common/ContextMenu';
import InputAutosize from './common/InputAutosize';
import DMVerification from './DMVerification';
import HeaderToolbar from './HeaderToolbar';
import GuildVerification from './GuildVerification';
import ChannelMuteButton from './ChannelMuteButton';
import PrivateChannelRecipientsInviteButton from './PrivateChannelRecipientsInvite';
import PrivateChannelRecipients from './PrivateChannelRecipients';
import PrivateChannelCall from './calls/PrivateChannelCall';
import PrivateChannelCallButton from './calls/PrivateChannelCallButton';
import SearchResults from './SearchResults';
import ChannelMembersButton from './ChannelMembersButton';
import ChannelPinsButton from './ChannelPinsButton';
import ChannelTextArea from './ChannelTextArea';
import ChannelRecord from '../records/ChannelRecord';
import ChannelStore from '../stores/ChannelStore';
import SelectedChannelStore from '../stores/SelectedChannelStore';
import SelectedGuildStore from '../stores/SelectedGuildStore';
import GuildStore from '../stores/GuildStore';
import EditMessageStore from '../stores/EditMessageStore';
import ChannelSectionStore from '../stores/ChannelSectionStore';
import GuildMemberStore from '../stores/GuildMemberStore';
import MessageStore from '../stores/MessageStore';
import ModalStore from '../stores/ModalStore';
import UserStore from '../stores/UserStore';
import InProgressTextStore from '../stores/InProgressTextStore';
import SearchStore from '../stores/SearchStore';
import VideoThemeStore from '../stores/VideoThemeStore';
import PrivateChannelCallStore from '../stores/views/PrivateChannelCallStore';
import Flux from '../lib/flux';
import MarkupUtils from '../utils/MarkupUtils';
import TypingUtils from '../utils/TypingUtils';
import i18n from '../i18n';
import MessageUtils from '../utils/MessageUtils';
import ComponentDispatchMixin from '../mixins/ComponentDispatchMixin';
import ChannelActionCreators from '../actions/ChannelActionCreators';
import UserProfileModalActionCreators from '../actions/UserProfileModalActionCreators';
import ModalActionCreators from '../actions/ModalActionCreators';
import MessageActionCreators from '../actions/MessageActionCreators';
import PermissionMixin from '../mixins/PermissionMixin';
import TutorialIndicator from './common/TutorialIndicator';
import MarkdownModal from './MarkdownModal';
import InProgressTextCreators from '../actions/InProgressTextCreators';
import ChatRestrictionMixin from '../mixins/web/ChatRestrictionMixin';
import Spinner from './common/Spinner';
import GuildNSFW from './warnings/GuildNSFW';
import GuildNSFWAgreeStore from '../stores/GuildNSFWAgreeStore';
import {
ChannelSections,
ContextMenuTypes,
ChannelTypes,
ComponentActions,
MouseButtons,
ThemeTypes,
} from '../Constants';
const ChannelName = React.createClass({
handleBlur(e) {
if (this.props.channel.toString() !== e.currentTarget.value) {
ChannelActionCreators.setName(this.props.channel.id, e.currentTarget.value);
}
},
render() {
return (
<InputAutosize
minLen={1}
maxLen={100}
className="channel-name"
value={this.props.channel.toString()}
onBlur={this.handleBlur}
/>
);
},
});
const ChannelTopicModal = React.createClass({
renderHeader() {
return `#${this.props.channel.toString()}`;
},
render() {
const {channel} = this.props;
return (
<MarkdownModal onClose={this.props.onClose} renderHeader={this.renderHeader} selectable>
{MarkupUtils.parseTopic(channel.topic, true, {channelId: channel.id})}
</MarkdownModal>
);
},
});
const ChannelTopic = React.createClass({
handleOpenTopic(e) {
// Don't open when clicking a link.
const shouldOpen = !(
e.target.matches('a') ||
e.target.parentNode.matches('a') ||
e.target.className == 'highlight' ||
e.target.parentNode.className == 'highlight'
);
if (shouldOpen) {
ModalActionCreators.push(ChannelTopicModal, this.props);
}
},
render() {
const {channel} = this.props;
if (channel.topic == null || channel.topic.length == 0) {
return <div className="no-topic" />;
}
if (channel.type === ChannelTypes.GROUP_DM) return null;
return (
<div
className="topic topic-expandable"
onMouseDown={this.onMouseDown}
onMouseMove={this.onMouseMove}
onMouseUp={this.onMouseUp}
onContextMenu={e => this.props.onContextMenu(e, ContextMenuTypes.CHANNEL_TOPIC)}>
{MarkupUtils.parseTopic(channel.topic, true, {channelId: channel.id})}
</div>
);
},
// Basically these methods are used to detect whether the user is dragging
// the window or not, to only show the title if they aren't dragging
onMouseDown() {
this._mouseDown = true;
},
onMouseMove() {
if (this._mouseDown) {
this._mouseDown = false;
}
},
onMouseUp(e) {
if (this._mouseDown && e.button !== MouseButtons.SECONDARY) {
this.handleOpenTopic(e);
}
this._mouseDown = false;
},
});
const ChannelTextAreaForm = React.createClass({
mixins: [ChatRestrictionMixin, PureRenderMixin],
componentWillMount() {
InProgressTextStore.addChangeListener(this.inProgressTextDidChange);
},
componentWillUnmount() {
InProgressTextStore.removeChangeListener(this.inProgressTextDidChange);
},
getInitialState() {
return {
textValue: InProgressTextStore.getSavedText(this.props.channel.id),
};
},
componentWillReceiveProps(nextProps) {
if (nextProps.channel.id !== this.props.channel.id) {
this.inProgressTextDidChange(nextProps);
}
},
inProgressTextDidChange(props = this.props) {
const textValue = InProgressTextStore.getSavedText(props.channel.id);
if (this.state.textValue === textValue && (!this.refs.input || textValue === this.refs.input.getValue())) return;
this.setState({textValue}, () => {
if (this.refs.input && textValue !== this.refs.input.getValue()) {
this.refs.input.setValue(textValue);
this.focus();
}
});
},
render() {
const {channel, onResize} = this.props;
const placeholder = i18n.Messages.TEXTAREA_PLACEHOLDER.format({channel: channel.toString(true)});
const Verification = channel.isPrivate() ? DMVerification : GuildVerification;
return (
<form>
<Verification key={channel.id} channel={channel}>
<TutorialIndicator tutorialId="writing-messages" position={TutorialIndicator.LEFT} offsetY={-50} offsetX={90}>
<ChannelTextArea
ref="input"
channel={channel}
placeholder={placeholder}
onChange={this.handleTextareaChange}
onSubmit={this.handleSendMessage}
onResize={onResize}
onKeyDown={this.handleKeyDown}
defaultValue={this.state.textValue}
/>
</TutorialIndicator>
</Verification>
<TypingUsers channel={channel} />
</form>
);
},
handleTextareaChange() {
InProgressTextCreators.changeCurrentText(this.props.channel.id, this.refs.input.getValue());
TypingUtils.typing(this.props.channel.id);
},
handleKeyDown(e, value) {
// Ensure no modifier keys are also pressed - since those usually coincide
// with keyboard shortcuts
if (e.which === 38 /* UP */ && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) {
if (value.length === 0) {
e.preventDefault();
const channel = this.props.channel;
const lastEditableMessage = MessageStore.getLastEditableMessage(channel.id);
if (lastEditableMessage) {
MessageActionCreators.startEditMessage(channel.id, lastEditableMessage.id, lastEditableMessage.content);
}
}
}
},
handleSendMessage(content) {
if (content.length === 0) return false;
if (this.applyChatRestrictions(content)) {
return;
}
const {channel} = this.props;
TypingUtils.clear(channel.id);
this.props.handleJumpToChat && this.props.handleJumpToChat();
MessageActionCreators.sendMessage(channel.id, MessageUtils.parse(channel, content));
return true;
},
focus() {
const input = this.refs['input'];
if (input != null) {
input.focus();
}
},
});
const AKA = React.createClass({
mixins: [Flux.StoreListenerMixin(GuildMemberStore), PureRenderMixin],
getStateFromStores() {
return {
nicknames: GuildMemberStore.getNicknames(this.props.channel.getRecipientId()).join(', '),
};
},
render() {
if (this.state.nicknames.length === 0) {
return <div className="no-topic" />;
}
return (
<div className="topic">
<span className="aka">AKA</span>
{this.state.nicknames}
</div>
);
},
});
const Channel = React.createClass({
mixins: [
Flux.StoreListenerMixin(
ChannelStore,
GuildStore,
SelectedChannelStore,
SelectedGuildStore,
EditMessageStore,
ModalStore,
ChannelSectionStore,
SearchStore,
PrivateChannelCallStore,
GuildNSFWAgreeStore,
VideoThemeStore
),
PermissionMixin,
PureRenderMixin,
ComponentDispatchMixin,
],
propTypes: {
channel: React.PropTypes.instanceOf(ChannelRecord),
},
getStateFromStores() {
const selectedChannelId = SelectedChannelStore.getChannelId();
const editingMessageId = EditMessageStore.getEditingMessageId();
const selectedGuildId = SelectedGuildStore.getGuildId();
const participants = PrivateChannelCallStore.getVisibleCallParticipants();
return {
channel: ChannelStore.getChannel(selectedChannelId),
isEditing: editingMessageId != null,
hasModalOpen: ModalStore.hasModalOpen(),
section: ChannelSectionStore.getSection(),
guild: GuildStore.getGuild(selectedGuildId),
searchId: SearchStore.getCurrentSearchId(),
hasCall: participants && participants.length > 0,
nsfwAgree: GuildNSFWAgreeStore.didAgree(selectedGuildId),
theme: VideoThemeStore.theme,
};
},
getSubscriptions() {
return {
[ComponentActions.TEXTAREA_FOCUS]: this.handleInputFocus,
};
},
componentDidUpdate(prevProps, prevState) {
if (
(this.state.isEditing !== prevState.isEditing || this.state.hasModalOpen !== prevState.hasModalOpen) &&
(this.state.isEditing == false && this.state.hasModalOpen == false)
) {
this.handleInputFocus();
}
if (
this.state.channel &&
prevState.channel &&
this.state.channel.id !== prevState.channel.id &&
this.state.topicExpanded
) {
this.setState({topicExpanded: false});
}
if (this.state.section !== prevState.section) {
this.handleResize();
}
},
handleInputFocus() {
const input = this.refs['input'];
if (input != null) {
input.focus();
}
},
handleOpenProfile() {
UserProfileModalActionCreators.open(this.state.channel.getRecipientId());
},
handleChannelContextMenu(event, type) {
const {guild, channel} = this.state;
ContextMenu.openContextMenu(event, props =>
<ChannelContextMenu {...props} type={type} channel={channel} guild={guild} />
);
},
handleUserContextMenu(event) {
const {channel} = this.state;
ContextMenu.openContextMenu(event, props =>
<UserContextMenu
{...props}
type={ContextMenuTypes.USER_CHANNEL_TITLE}
user={UserStore.getUser(channel.getRecipientId())}
selected={true}
channelId={channel.id}
/>
);
},
renderTitle() {
const {channel} = this.state;
switch (channel.type) {
case ChannelTypes.DM:
const userId = channel.getRecipientId();
return (
<div className="title" onContextMenu={this.handleUserContextMenu}>
<a className="channel-name channel-private" onClick={this.handleOpenProfile}>{channel.toString()}</a>
<Status userId={userId} />
</div>
);
case ChannelTypes.GROUP_DM:
if (channel.isManaged()) {
return (
<div className="title channel-group-dm-managed">
<a className="channel-name channel-private">{channel.toString()}</a>
</div>
);
} else {
return (
<div className="title channel-group-dm">
<ChannelName key={`channel-${channel.id}`} channel={channel} />
</div>
);
}
case ChannelTypes.GUILD_TEXT:
return (
<div className="title" onContextMenu={e => this.handleChannelContextMenu(e, ContextMenuTypes.CHANNEL_TITLE)}>
<span className="channel-name">{channel.toString()}</span>
</div>
);
default:
return null;
}
},
renderTopic() {
const {channel, guild} = this.state;
switch (channel.type) {
case ChannelTypes.DM:
return <AKA key={channel.id} channel={channel} />;
case ChannelTypes.GUILD_TEXT:
return (
<ChannelTopic
key={channel.id}
channel={channel}
guild={guild}
onContextMenu={this.handleChannelContextMenu}
/>
);
default:
return null;
}
},
renderToolbar() {
const {channel} = this.state;
let callButton;
let muteButton;
let membersButton;
let recipientsButton;
switch (channel.type) {
case ChannelTypes.DM:
callButton = <PrivateChannelCallButton channel={channel} clickRecipientButton={this.clickRecipientButton} />;
recipientsButton = (
<PrivateChannelRecipientsInviteButton
channel={channel}
ref={this.getRecipientButtonRef}
tooltip={i18n.Messages.GROUP_DM_ADD_FRIENDS}
/>
);
callButton = <PrivateChannelCallButton channel={channel} clickRecipientButton={this.clickRecipientButton} />;
break;
case ChannelTypes.GROUP_DM:
membersButton = <ChannelMembersButton />;
if (!channel.isManaged()) {
recipientsButton = (
<PrivateChannelRecipientsInviteButton channel={channel} tooltip={i18n.Messages.GROUP_DM_ADD_FRIENDS} />
);
}
callButton = <PrivateChannelCallButton channel={channel} />;
break;
case ChannelTypes.GUILD_TEXT:
membersButton = <ChannelMembersButton />;
muteButton = <ChannelMuteButton channel={channel} />;
break;
}
return (
<HeaderToolbar>
{callButton}
{muteButton}
<ChannelPinsButton />
{membersButton}
{recipientsButton}
</HeaderToolbar>
);
},
renderCall() {
const {channel} = this.state;
switch (channel.type) {
case ChannelTypes.DM:
return <PrivateChannelCall key={channel.id} channel={channel} onResize={this.handleResize} />;
case ChannelTypes.GROUP_DM:
return <PrivateChannelCall key={channel.id} channel={channel} onResize={this.handleResize} />;
default:
return null;
}
},
renderChat() {
const {channel, nsfwAgree, guild} = this.state;
if (!nsfwAgree && channel.isNSFW()) {
return <GuildNSFW guild={guild} />;
}
return (
<div className="flex-spacer flex-vertical" style={{position: 'relative'}}>
<Messages key={channel.id} ref="messages" channel={channel} />
<ChannelTextAreaForm
ref="input"
channel={channel}
onResize={this.handleResize}
handleJumpToPresent={this.handleJumpToPresent}
/>
</div>
);
},
renderSidebar() {
const {searchId, channel, section} = this.state;
if (section === ChannelSections.MEMBERS) {
switch (channel.type) {
case ChannelTypes.GROUP_DM:
return <PrivateChannelRecipients channel={channel} />;
case ChannelTypes.GUILD_TEXT:
return <ChannelMembers channel={channel} />;
}
} else if (section === ChannelSections.SEARCH) {
return <SearchResults searchId={searchId} />;
}
return null;
},
renderEmptyChannel() {
return (
<div className="chat flex-vertical flex-spacer empty">
<Spinner />
</div>
);
},
render() {
const {channel, hasCall, theme} = this.state;
if (!channel) {
return this.renderEmptyChannel();
}
return (
<div className={classNames('chat flex-vertical flex-spacer', {private: channel.isPrivate()})}>
<div className={classNames('title-wrap', {call: hasCall, 'title-wrap-dark': theme === ThemeTypes.DARK})}>
<Helmet title={channel.toString(true)} />
{this.renderTitle()}
{this.renderTopic()}
{this.renderToolbar()}
</div>
{this.renderCall()}
<div className="content flex-spacer flex-horizontal">
{this.renderChat()}
{this.renderSidebar()}
</div>
</div>
);
},
handleJumpToPresent() {
if (MessageStore.getMessages(this.state.channel.id).hasMoreAfter) {
this.refs.messages.jumpToPresent();
}
},
handleResize() {
const messages = this.refs['messages'];
if (messages != null) {
messages.handleResize();
}
},
getRecipientButtonRef(button) {
this._recipientButton = button || null;
},
clickRecipientButton() {
this._recipientButton && this._recipientButton.openFromDisabledCall();
},
});
export default Channel;
// WEBPACK FOOTER //
// ./discord_app/components/Channel.js