404 lines
13 KiB
JavaScript
Executable file
404 lines
13 KiB
JavaScript
Executable file
/* @flow */
|
|
|
|
import React from 'react';
|
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
|
import ReactDOM from 'react-dom';
|
|
import lodash from 'lodash';
|
|
import Flux from '../lib/flux';
|
|
import classNames from 'classnames';
|
|
import Popout from './common/Popout';
|
|
import SearchBar from './common/SearchBar';
|
|
import Avatar from './common/Avatar';
|
|
import LazyScroller, {Themes} from './common/LazyScroller';
|
|
import DiscordTag from './common/DiscordTag';
|
|
import Checkbox from './common/Checkbox';
|
|
import PrivateChannelRecipientsInviteStore from '../stores/PrivateChannelRecipientsInviteStore';
|
|
import UserSettingsStore from '../stores/UserSettingsStore';
|
|
import PresenceStore from '../stores/PresenceStore';
|
|
import RelationshipStore from '../stores/RelationshipStore';
|
|
import UserStore from '../stores/UserStore';
|
|
import CallStore from '../stores/CallStore';
|
|
import PopoutStore from '../stores/PopoutStore';
|
|
import PrivateChannelRecipientsInviteActionCreators from '../actions/PrivateChannelRecipientsInviteActionCreators';
|
|
import ChannelActionCreators from '../actions/ChannelActionCreators';
|
|
import RelationshipActionCreators from '../actions/RelationshipActionCreators';
|
|
import CallActionCreators from '../actions/CallActionCreators';
|
|
import {HeaderToolbarButton} from './HeaderToolbar';
|
|
import {
|
|
ChannelTypes,
|
|
ThemeTypes,
|
|
Colors,
|
|
MAX_GROUP_DM_PARTICIPANTS,
|
|
PopoutCallers,
|
|
ComponentActions,
|
|
} from '../Constants';
|
|
import i18n from '../i18n';
|
|
import type ChannelRecord from '../records/ChannelRecord';
|
|
import AnalyticsUtils from '../utils/AnalyticsUtils';
|
|
import '../styles/private_channel_recipients_invite.styl';
|
|
|
|
type PrivateChannelRecipientsInviteButtonProps = {
|
|
channel: ?ChannelRecord,
|
|
tooltip?: string,
|
|
};
|
|
|
|
const POPOUT_ID = 'PrivateChannelRecipientsInvitePopout';
|
|
const ROW_HEIGHT = 38;
|
|
const BUTTON_REF = 'BUTTON_REF';
|
|
const SEARCH_REF = 'SEARCH_REF';
|
|
|
|
const PrivateChannelRecipientsInvitePopout = React.createClass({
|
|
mixins: [Flux.LazyStoreListenerMixin(PrivateChannelRecipientsInviteStore), PureRenderMixin],
|
|
|
|
getInitialState() {
|
|
return this.getStateFromStores();
|
|
},
|
|
|
|
getStateFromStores() {
|
|
return PrivateChannelRecipientsInviteStore.getState();
|
|
},
|
|
|
|
componentDidMount() {
|
|
const {channel} = this.props;
|
|
if (channel != null && channel.isDM() && !RelationshipStore.isFriend(channel.getRecipientId())) {
|
|
AnalyticsUtils.track('open_popout', {
|
|
type: 'Not Friend',
|
|
source: this.props.caller === PopoutCallers.GROUP_DM ? 'Group DM' : 'Call',
|
|
});
|
|
}
|
|
},
|
|
|
|
render() {
|
|
const {hasFriends, results, selectedRow, selectedUsers} = this.state;
|
|
const {channel} = this.props;
|
|
|
|
const theme = UserSettingsStore.theme;
|
|
let notFriends = false;
|
|
let isPartyFull = false;
|
|
if (channel != null) {
|
|
const {recipients} = channel;
|
|
notFriends = channel.isDM() && !RelationshipStore.isFriend(recipients[0]);
|
|
isPartyFull = recipients.length + 1 >= MAX_GROUP_DM_PARTICIPANTS;
|
|
}
|
|
|
|
const hideSearchBar = !hasFriends || notFriends;
|
|
|
|
let rows = [];
|
|
let footer;
|
|
let title = i18n.Messages.GROUP_DM_HEADER;
|
|
let subtitle;
|
|
|
|
// Are you friends with this user
|
|
if (notFriends) {
|
|
const user = UserStore.getUser(channel.recipients[0]);
|
|
|
|
let text;
|
|
if (this.props.caller === PopoutCallers.CALL) {
|
|
title = i18n.Messages.START_CALL;
|
|
text = i18n.Messages.CALL_INVITE_NOT_FRIENDS;
|
|
} else {
|
|
title = i18n.Messages.GROUP_DM_ADD_FRIENDS;
|
|
text = i18n.Messages.GROUP_DM_INVITE_NOT_FRIENDS;
|
|
}
|
|
|
|
rows = (
|
|
<div className="error-state not-friends">
|
|
<div className="icon" />
|
|
<div className="text">
|
|
{text.format({username: user.username})}
|
|
</div>
|
|
<div className="btn" onClick={() => this.handleAddFriend(user)}>
|
|
{i18n.Messages.ADD_FRIEND_BUTTON}
|
|
</div>
|
|
</div>
|
|
);
|
|
} else if (!hasFriends) {
|
|
// No friends added
|
|
rows = (
|
|
<div className="error-state no-friends">
|
|
<div className="icon" />
|
|
<div className="text">{i18n.Messages.GROUP_DM_INVITE_NO_FRIENDS}</div>
|
|
</div>
|
|
);
|
|
} else {
|
|
if (results == null) {
|
|
throw new Error('[App]PrivateChannelRecipientsInvitePopout.render(): assert failed: results != null');
|
|
}
|
|
if (results.length > 0) {
|
|
rows = results.map(this.renderRow);
|
|
}
|
|
|
|
// Party full
|
|
if (isPartyFull) {
|
|
rows = (
|
|
<div className="error-state party-full">
|
|
<div className="icon" />
|
|
<div className="text">{i18n.Messages.GROUP_DM_INVITE_FULL_MAIN}</div>
|
|
<div className="text">{i18n.Messages.GROUP_DM_INVITE_FULL_SUB}</div>
|
|
</div>
|
|
);
|
|
} else {
|
|
// No results or friends remaining
|
|
if (rows.length === 0) {
|
|
rows = (
|
|
<div className="error-state search-results">
|
|
<div className="icon" />
|
|
<div className="text">{i18n.Messages.GROUP_DM_INVITE_EMPTY}</div>
|
|
</div>
|
|
);
|
|
} else {
|
|
// for light theme we don't want to use a theme, so we show the purple scroll bar.
|
|
// for dark theme we do want to show the theme though.
|
|
rows = (
|
|
<LazyScroller
|
|
ref="scroller"
|
|
elementHeight={ROW_HEIGHT}
|
|
theme={theme === ThemeTypes.LIGHT ? Themes.LIGHT : Themes.DARK}
|
|
backgroundColor={theme === ThemeTypes.LIGHT ? undefined : Colors.CHANNELS_GREY}
|
|
keyboardScroll={true}>
|
|
{rows}
|
|
</LazyScroller>
|
|
);
|
|
}
|
|
|
|
const existingUsersCount = (channel == null ? 0 : channel.recipients.length) + 1; // + self
|
|
const remaining = MAX_GROUP_DM_PARTICIPANTS - selectedUsers.length - existingUsersCount;
|
|
const disabled =
|
|
selectedUsers.length == 0 || selectedUsers.length + existingUsersCount > MAX_GROUP_DM_PARTICIPANTS;
|
|
let buttonText = i18n.Messages.GROUP_DM_ADD_MEMBERS.format({number: selectedUsers.length});
|
|
if (channel == null || channel.type === ChannelTypes.DM) {
|
|
buttonText = i18n.Messages.CREATE_GROUP_DM;
|
|
}
|
|
|
|
if (remaining <= 0) {
|
|
subtitle = (
|
|
<div className={classNames('subtitle', {' subtitle-warning': remaining < 0})}>
|
|
{i18n.Messages.GROUP_DM_INVITE_FULL_SUB}
|
|
</div>
|
|
);
|
|
} else {
|
|
subtitle = (
|
|
<div className="subtitle">
|
|
{i18n.Messages.GROUP_DM_INVITE_REMAINING.format({number: remaining})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
footer = (
|
|
<div className="footer">
|
|
<button type="button" disabled={disabled} onClick={this.handleInviteUsers}>{buttonText}</button>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="private-channel-recipients-invite themed-popout">
|
|
<div className="header">
|
|
<div className="title">{title}</div>
|
|
{subtitle}
|
|
{hideSearchBar
|
|
? null
|
|
: <SearchBar
|
|
ref={SEARCH_REF}
|
|
query={this.state.query}
|
|
selectedSection={0}
|
|
selectedRow={selectedRow}
|
|
sections={[results ? results.length : 0]}
|
|
tags={selectedUsers.map(user => user.username)}
|
|
onSelect={this.handleSelect}
|
|
onSelectionChange={this.handleSelectionChange}
|
|
onQueryChange={this.handleQueryChange}
|
|
onRemoveTag={this.handleRemoveUser}
|
|
light={theme === ThemeTypes.LIGHT}
|
|
placeholder={selectedUsers.length === 0 ? i18n.Messages.GROUP_DM_SEARCH_PLACEHOLDER : null}
|
|
disabled={isPartyFull}
|
|
autoFocus={true}
|
|
/>}
|
|
</div>
|
|
<div className="body">
|
|
{rows}
|
|
</div>
|
|
{footer}
|
|
</div>
|
|
);
|
|
},
|
|
|
|
// Utils
|
|
|
|
handleAddFriend(user) {
|
|
this.props.onClose();
|
|
RelationshipActionCreators.sendRequest(user.tag, {
|
|
location: this.props.caller === PopoutCallers.GROUP_DM ? 'Group DM' : 'Call',
|
|
});
|
|
},
|
|
|
|
handleSelect(selectedSection, selectedRow, e) {
|
|
if (selectedSection == null) {
|
|
PrivateChannelRecipientsInviteActionCreators.clear();
|
|
} else {
|
|
const results = this.state.results;
|
|
if (results == null) {
|
|
throw new Error('[App]PrivateChannelRecipientsInvitePopout.handleSelect(): assert failed: results != null');
|
|
}
|
|
const user = results[selectedRow];
|
|
const added = this.state.selectedUsers.indexOf(user) > -1;
|
|
this.handleClick(e, !added, user);
|
|
}
|
|
},
|
|
|
|
handleSelectionChange(section, row) {
|
|
PrivateChannelRecipientsInviteActionCreators.select(row);
|
|
const scroller = this.refs.scroller;
|
|
if (scroller) {
|
|
scroller.scrollIntoViewRect(row * ROW_HEIGHT, row * ROW_HEIGHT + ROW_HEIGHT);
|
|
}
|
|
},
|
|
|
|
handleQueryChange(query) {
|
|
PrivateChannelRecipientsInviteActionCreators.search(query);
|
|
},
|
|
|
|
handleRemoveUser(index) {
|
|
PrivateChannelRecipientsInviteActionCreators.removeUser(this.state.selectedUsers[index]);
|
|
},
|
|
|
|
handleClick(e, added, user) {
|
|
if (added) {
|
|
PrivateChannelRecipientsInviteActionCreators.addUser(user);
|
|
if (this.state.query.length > 0) {
|
|
PrivateChannelRecipientsInviteActionCreators.clear();
|
|
}
|
|
} else {
|
|
PrivateChannelRecipientsInviteActionCreators.removeUser(user);
|
|
}
|
|
},
|
|
|
|
handleInviteUsers() {
|
|
const {channel} = this.props;
|
|
const {selectedUsers} = this.state;
|
|
|
|
if (channel != null) {
|
|
// invite users to current group chat
|
|
const callActive = CallStore.isCallActive(channel.id);
|
|
const selectedUserIds = selectedUsers.map(user => user.id);
|
|
ChannelActionCreators.addRecipients(channel.id, selectedUserIds).then(newChannelId => {
|
|
if (callActive) {
|
|
CallActionCreators.ring(newChannelId, selectedUserIds);
|
|
}
|
|
});
|
|
} else {
|
|
if (selectedUsers.length == 1) {
|
|
// open DM with selected user
|
|
ChannelActionCreators.openPrivateChannel(UserStore.getCurrentUser().id, selectedUsers[0].id);
|
|
} else {
|
|
// create a group chat with selected users
|
|
const selectedUserIds = selectedUsers.map(user => user.id);
|
|
ChannelActionCreators.openPrivateChannel(UserStore.getCurrentUser().id, selectedUserIds);
|
|
}
|
|
}
|
|
|
|
this.props.onClose();
|
|
},
|
|
|
|
renderRow(user, i) {
|
|
const checked = this.state.selectedUsers.indexOf(user) > -1;
|
|
|
|
return (
|
|
<div
|
|
key={user.id}
|
|
onClick={evt => this.handleClick(evt, !checked, user)}
|
|
className={classNames('friend', {selected: i === this.state.selectedRow})}>
|
|
<Avatar user={user} status={PresenceStore.getStatus(user.id)} />
|
|
<DiscordTag user={user} />
|
|
<Checkbox className="checkbox" checked={checked} readOnly />
|
|
</div>
|
|
);
|
|
},
|
|
});
|
|
|
|
class PrivateChannelRecipientsInviteButton extends React.Component {
|
|
popoutCaller: string;
|
|
|
|
constructor(props: PrivateChannelRecipientsInviteButtonProps) {
|
|
super(props);
|
|
|
|
this.popoutCaller = PopoutCallers.GROUP_DM;
|
|
|
|
lodash.bindAll(this, ['renderPopout']);
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.maybeForceOpen();
|
|
}
|
|
|
|
componentDidUpdate(prevProps: PrivateChannelRecipientsInviteButtonProps) {
|
|
if (prevProps.channel === this.props.channel) return;
|
|
this.maybeForceOpen();
|
|
}
|
|
|
|
render() {
|
|
const {channel} = this.props;
|
|
const iconLight = channel != null
|
|
? require('../images/header_icons/ic_person_add_grey_24px.svg')
|
|
: require('../images/header_icons/ic_compose_grey_24px.svg');
|
|
const iconDark = channel != null
|
|
? require('../images/icon-header-add-members-dark.svg')
|
|
: require('../images/ic_header_compose_dark_24px.svg');
|
|
|
|
const button = <HeaderToolbarButton tooltip={this.props.tooltip} iconLight={iconLight} iconDark={iconDark} />;
|
|
|
|
return (
|
|
<Popout
|
|
ref={BUTTON_REF}
|
|
closeOnScroll={false}
|
|
render={this.renderPopout}
|
|
position={Popout.TOP_RIGHT}
|
|
shadow={false}
|
|
offsetX={5}
|
|
uniqueId={POPOUT_ID}
|
|
animationType="none"
|
|
subscribeTo={ComponentActions.TOGGLE_DM_CREATE}>
|
|
{button}
|
|
</Popout>
|
|
);
|
|
}
|
|
|
|
renderPopout(props: $PropsOf<Popout>) {
|
|
return <PrivateChannelRecipientsInvitePopout {...props} channel={this.props.channel} caller={this.popoutCaller} />;
|
|
}
|
|
|
|
maybeForceOpen() {
|
|
// Force open the popout if this channel has no recipients.
|
|
if (
|
|
this.props.channel != null &&
|
|
this.props.channel.type === ChannelTypes.GROUP_DM &&
|
|
this.props.channel.recipients.length === 0 &&
|
|
!PopoutStore.isOpen(POPOUT_ID)
|
|
) {
|
|
const elem = ReactDOM.findDOMNode(this.refs[BUTTON_REF]);
|
|
if (!(elem instanceof HTMLElement)) {
|
|
throw new Error('elem is not instanceof HTMLElement');
|
|
}
|
|
elem.click();
|
|
}
|
|
}
|
|
|
|
// Triggered by Channel.js when the call icon button is clicked.
|
|
openFromDisabledCall() {
|
|
this.popoutCaller = PopoutCallers.CALL;
|
|
const elem = ReactDOM.findDOMNode(this.refs[BUTTON_REF]);
|
|
if (!(elem instanceof HTMLElement)) {
|
|
throw new Error('elem is not instanceof HTMLElement');
|
|
}
|
|
elem.click();
|
|
setImmediate(() => (this.popoutCaller = PopoutCallers.GROUP_DM));
|
|
}
|
|
}
|
|
|
|
export default PrivateChannelRecipientsInviteButton;
|
|
|
|
|
|
|
|
// WEBPACK FOOTER //
|
|
// ./discord_app/components/PrivateChannelRecipientsInvite.js
|