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

613 lines
21 KiB
JavaScript
Executable file

import React from 'react';
import {Link} from 'react-router';
import Helmet from 'react-helmet';
import classNames from 'classnames';
import TransitionGroup from '../lib/transitions/TransitionGroup';
import lodash from 'lodash';
import Flux from '../lib/flux';
import Platforms from '../lib/Platforms';
import Badge from './common/Badge';
import Avatar from './common/Avatar';
import GuildIcon from './common/GuildIcon';
import TabBar, {TabBarItem, TabBarSeparator, TabBarTypes} from './common/TabBar';
import Scroller from './common/Scroller';
import Tooltip from './common/Tooltip';
import DiscordTag from './common/DiscordTag';
import ContextMenu from './common/ContextMenu';
import ConnectAccountButton from './common/ConnectAccountButton';
import DownloadApps from './DownloadApps';
import UserContextMenu from './contextmenus/UserContextMenu';
import ConfirmModal from './ConfirmModal';
import HeaderToolbar from './HeaderToolbar';
import PrivateChannelRecipientsInviteButton from './PrivateChannelRecipientsInvite';
import AddFriendInput from './AddFriendInput';
import FriendsStore from '../stores/views/FriendsStore';
import FriendSuggestionStore from '../stores/FriendSuggestionStore';
import UserStore from '../stores/UserStore';
import ChannelStore from '../stores/ChannelStore';
import UserSettingsStore from '../stores/UserSettingsStore';
import StreamerModeStore from '../stores/StreamerModeStore';
import ConnectedAccountsStore from '../stores/ConnectedAccountsStore';
import DetectedPlatformAccountsStore from '../stores/DetectedPlatformAccountsStore';
import VideoCallExperimentStore from '../stores/experiments/VideoCallExperimentStore';
import RouterUtils from '../utils/RouterUtils';
import NativeUtils from '../utils/NativeUtils';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import AudioActionCreators from '../actions/AudioActionCreators';
import FriendsActionCreators from '../actions/FriendsActionCreators';
import RelationshipActionCreators from '../actions/RelationshipActionCreators';
import ChannelActionCreators from '../actions/ChannelActionCreators';
import ModalActionCreators from '../actions/ModalActionCreators';
import UserProfileModalActionCreators from '../actions/UserProfileModalActionCreators';
import FriendSuggestionActionCreators from '../actions/FriendSuggestionActionCreators';
import UserSettingsModalActionCreators from '../actions/UserSettingsModalActionCreators';
import ExperimentStore from '../stores/ExperimentStore';
import {getStatusText} from '../utils/FriendsUtils';
import {renderActivity, isStreaming} from '../utils/ActivityUtils';
import i18n from '../i18n';
import Animated from '../lib/animated';
import '../styles/friends.styl';
import {
StatusTypes,
FriendsSections,
ME,
RelationshipTypes,
MAX_MUTUAL_GUILDS,
Routes,
ContextMenuTypes,
ChannelTypes,
ThemeTypes,
UserSettingsSections,
ExperimentTypes,
} from '../Constants';
const FriendsEmptyState = React.createClass({
mixins: [PureRenderMixin, Flux.StoreListenerMixin(ConnectedAccountsStore)],
getInitialState() {
return {
opacity: new Animated.Value(1),
};
},
getStateFromStores() {
return {
hasAccounts: ConnectedAccountsStore.getUnsafeAccounts().length > 0,
};
},
componentWillEnter(callback) {
this.state.opacity.setValue(0);
Animated.timing(this.state.opacity, {
toValue: 1,
duration: 250,
}).start(callback);
},
componentWillLeave(callback) {
Animated.timing(this.state.opacity, {
toValue: 0,
duration: 250,
}).start(callback);
},
handleDownloadApp() {
ModalActionCreators.push(DownloadApps, {source: 'Suggestions'});
},
handleConnectAccounts() {
UserSettingsModalActionCreators.open(UserSettingsSections.CONNECTIONS);
},
render() {
const {type, onClick} = this.props;
const {hasAccounts} = this.state;
const style = {opacity: this.state.opacity};
switch (type) {
case FriendsSections.ADD_FRIEND:
let description;
let button;
let className = 'section-add-friend-no-connection';
if (!hasAccounts && !NativeUtils.embedded) {
description = i18n.Messages.FRIENDS_EMPTY_STATE_ADD_FRIEND_DOWNLOAD;
button = (
<button className="btn" onClick={this.handleDownloadApp}>
{i18n.Messages.DOWNLOAD}
</button>
);
} else if (!hasAccounts && NativeUtils.embedded) {
description = i18n.Messages.FRIENDS_EMPTY_STATE_ADD_FRIEND_NO_ACCOUNTS;
button = (
<button className="btn" onClick={this.handleConnectAccounts}>
{i18n.Messages.CONNECT_ACCOUNTS}
</button>
);
} else {
className = 'section-add-friend-no-suggestion';
description = i18n.Messages.FRIENDS_EMPTY_STATE_ADD_FRIEND_NO_SUGGESTION;
}
return (
<Animated.div className={`friends-empty ${className}`} style={style}>
<Helmet title="Discord" />
<div className="empty-state-image" />
<p>{description}</p>
{button}
</Animated.div>
);
case FriendsSections.ALL:
return (
<Animated.div className="friends-empty section-all" style={style}>
<Helmet title="Discord" />
<div className="empty-state-image" />
<p>{i18n.Messages.FRIENDS_EMPTY_STATE_ALL}</p>
<button className="btn" onClick={onClick}>
{i18n.Messages.ADD_FRIEND}
</button>
</Animated.div>
);
case FriendsSections.ONLINE:
return (
<Animated.div className="friends-empty section-online" style={style}>
<Helmet title="Discord" />
<div className="empty-state-image" />
<p>{i18n.Messages.FRIENDS_EMPTY_STATE_ONLINE}</p>
</Animated.div>
);
case FriendsSections.PENDING:
return (
<Animated.div className="friends-empty section-pending" style={style}>
<Helmet title="Discord" />
<div className="empty-state-image" />
<p>{i18n.Messages.FRIENDS_EMPTY_STATE_PENDING}</p>
</Animated.div>
);
case FriendsSections.BLOCKED:
return (
<Animated.div className="friends-empty section-blocked" style={style}>
<Helmet title="Discord" />
<div className="empty-state-image" />
<p>{i18n.Messages.FRIENDS_EMPTY_STATE_BLOCKED}</p>
</Animated.div>
);
default:
return null;
}
},
});
const MutualGuild = ({guild}) =>
<Tooltip text={guild.toString()}>
<Link to={Routes.GUILD(guild.id)} onClick={e => e.stopPropagation()}>
<GuildIcon guild={guild} textScale={0.8} />
</Link>
</Tooltip>;
const MoreMutualGuildsButton = ({num, ...props}) => <div className="more-mutual-guilds-btn" {...props}>+{num}</div>;
const FriendsColumn = props => <div className={`friends-column friends-column-${props.column}`}>{props.children}</div>;
const FriendsColumnSeparator = () => <div className="friends-column-separator" />;
const RowMixin = {
getInitialState() {
return {
height: new Animated.Value(62),
opacity: new Animated.Value(1),
};
},
componentWillEnter(callback) {
this.state.opacity.setValue(0);
this.state.height.setValue(0);
Animated.parallel([
Animated.timing(this.state.opacity, {
toValue: 1,
duration: 200,
}),
Animated.timing(this.state.height, {
toValue: 62,
duration: 200,
}),
]).start(callback);
},
componentWillLeave(callback) {
Animated.parallel([
Animated.timing(this.state.opacity, {
toValue: 0,
duration: 200,
}),
Animated.timing(this.state.height, {
toValue: 0,
duration: 200,
}),
]).start(callback);
},
};
function Action({type, tooltip, onClick}) {
const action = onClick != null
? <div className={`friends-action friends-action-${type}`} onClick={onClick} />
: <div className={`friends-action friends-action-${type} disabled`} />;
return <Tooltip text={tooltip}>{action}</Tooltip>;
}
const FriendSuggestionRow = React.createClass({
mixins: [RowMixin, PureRenderMixin, Flux.StoreListenerMixin(UserSettingsStore, StreamerModeStore)],
getStateFromStores() {
return {
theme: UserSettingsStore.theme,
hide: StreamerModeStore.hidePersonalInformation,
};
},
handleOpenProfile(e) {
e.stopPropagation();
const {user} = this.props;
UserProfileModalActionCreators.open(user.id);
},
handleAddRelationship(e) {
e.stopPropagation();
const {user} = this.props;
RelationshipActionCreators.addRelationship(user.id, {location: 'Friend Suggestion'});
},
handleIgnoreFriendSuggestion(e) {
e.stopPropagation();
const {user} = this.props;
FriendSuggestionActionCreators.ignore(user.id);
},
render() {
const {user, reasons} = this.props;
const {hide, theme} = this.state;
let platforms;
if (!hide) {
platforms = reasons.map(({name, platform_type: platformType}, i) => {
const platform = Platforms.get(platformType);
return (
<div key={i} className="friends-suggestion-platform">
<img
className="friends-suggestion-platform-icon"
src={theme === ThemeTypes.DARK ? platform.icon.white : platform.icon.grey}
/>
<span className="friends-suggestion-platform-name">{name}</span>
</div>
);
});
}
return (
<Animated.div
className="friends-row"
onClick={this.handleOpenProfile}
style={{height: this.state.height, opacity: this.state.opacity}}>
<div className="friends-suggestion-inner">
<Avatar user={user} size="large" />
<div className="friends-suggestion-info">
<DiscordTag user={user} />
<div className="friends-suggestion-platforms">{platforms}</div>
</div>
</div>
<div className="friends-column friends-column-actions friends-column-actions-visible">
<Action type="add" tooltip={i18n.Messages.SEND_FRIEND_REQUEST} onClick={this.handleAddRelationship} />
<Action
type="ignore"
tooltip={i18n.Messages.FRIEND_REQUEST_IGNORE}
onClick={this.handleIgnoreFriendSuggestion}
/>
</div>
</Animated.div>
);
},
});
const FriendRow = React.createClass({
mixins: [RowMixin, PureRenderMixin],
getInitialState() {
// FIXME: Quick hack to enable video button without using an experiment
// wrapper because it breakes the component transitions
let video = false;
const override = ExperimentStore.getOverrideExperimentDescriptor(VideoCallExperimentStore.getExperimentId());
if (override != null && override.type === ExperimentTypes.DEVELOPER) {
video = true;
}
return {video};
},
handleOpenProfile(e) {
e.stopPropagation();
UserProfileModalActionCreators.open(this.props.user.id);
},
handleOpenPrivateChannel(e) {
e.stopPropagation();
const {user} = this.props;
const channel = lodash.find(
ChannelStore.getChannels(),
channel => channel.type == ChannelTypes.DM && channel.getRecipientId() === user.id
);
if (channel != null) {
RouterUtils.transitionTo(Routes.CHANNEL(ME, channel.id));
} else {
ChannelActionCreators.openPrivateChannel(UserStore.getCurrentUser().id, user.id);
}
},
handleRemoveFriend(e) {
e.stopPropagation();
ModalActionCreators.push(props => {
return (
<ConfirmModal
header={i18n.Messages.REMOVE_FRIEND_TITLE.format({name: this.props.user.toString()})}
confirmText={i18n.Messages.REMOVE_FRIEND}
cancelText={i18n.Messages.CANCEL}
onConfirm={this.handleRemoveRelationship}
{...props}>
<p>{i18n.Messages.REMOVE_FRIEND_BODY.format({name: this.props.user.toString()})}</p>
</ConfirmModal>
);
});
},
handleAddRelationship(e) {
e.stopPropagation();
const {user} = this.props;
RelationshipActionCreators.addRelationship(user.id, {location: 'Friends'});
},
handleRemoveRelationship(e) {
e && e.stopPropagation();
RelationshipActionCreators.removeRelationship(this.props.user.id, {location: 'Friends'});
},
handleContextMenu(event, user) {
ContextMenu.openContextMenu(event, props =>
<UserContextMenu {...props} type={ContextMenuTypes.USER_FRIEND_LIST} user={user} />
);
},
handleVideoCall(e) {
AudioActionCreators.setVideoEnabled(true);
this.handleVoiceCall(e);
},
handleVoiceCall(e) {
e.stopPropagation();
ChannelActionCreators.openPrivateChannel(UserStore.getCurrentUser().id, this.props.user.id, true);
},
render() {
const {user, type, mutualGuilds, mutualGuildsLength} = this.props;
const actions = [];
const addAction = (type, tooltip, onClick = null) => {
actions.push(<Action key={actions.length} type={type} tooltip={tooltip} onClick={onClick} />);
};
let status = this.props.status;
let statusText = renderActivity(this.props.activity) || getStatusText(this.props.status);
let actionsVisible = true;
switch (type) {
case RelationshipTypes.FRIEND:
actionsVisible = false;
if (this.state.video) {
addAction('video-call', i18n.Messages.START_VIDEO_CALL, this.handleVideoCall);
}
addAction('voice-call', i18n.Messages.START_VOICE_CALL, this.handleVoiceCall);
addAction('remove', i18n.Messages.REMOVE_FRIEND, this.handleRemoveFriend);
break;
case RelationshipTypes.BLOCKED:
status = StatusTypes.UNKNOWN;
statusText = i18n.Messages.BLOCKED;
addAction('remove', i18n.Messages.UNBLOCK, this.handleRemoveRelationship);
break;
case RelationshipTypes.PENDING_INCOMING:
status = status === StatusTypes.OFFLINE ? StatusTypes.UNKNOWN : status;
statusText = i18n.Messages.INCOMING_FRIEND_REQUEST;
addAction('accept', i18n.Messages.FRIEND_REQUEST_ACCEPT, this.handleAddRelationship);
addAction('ignore', i18n.Messages.FRIEND_REQUEST_IGNORE, this.handleRemoveRelationship);
break;
case RelationshipTypes.PENDING_OUTGOING:
actionsVisible = false;
status = status === StatusTypes.OFFLINE ? StatusTypes.UNKNOWN : status;
statusText = i18n.Messages.OUTGOING_FRIEND_REQUEST;
addAction('cancel', i18n.Messages.FRIEND_REQUEST_CANCEL, this.handleRemoveRelationship);
break;
}
let moreMutualGuildsButton;
if (mutualGuildsLength > MAX_MUTUAL_GUILDS) {
moreMutualGuildsButton = (
<MoreMutualGuildsButton num={mutualGuildsLength - MAX_MUTUAL_GUILDS} onClick={this.handleOpenProfile} />
);
}
return (
<Animated.div
className="friends-row"
onClick={this.handleOpenPrivateChannel}
onContextMenu={e => this.handleContextMenu(e, user)}
style={{height: this.state.height, opacity: this.state.opacity}}>
<div className="friends-column friends-column-name">
<Avatar user={user} />
<DiscordTag user={user} />
</div>
<div className="friends-column friends-column-status">
<span className={`status status-${isStreaming(this.props.activity) ? 'streaming' : status}`} />
<span className="status-text">{statusText}</span>
</div>
<div className="friends-column friends-column-guilds">
{mutualGuilds.map(guild => <MutualGuild key={guild.id} guild={guild} />)}
{moreMutualGuildsButton}
</div>
<div
className={classNames('friends-column friends-column-actions', {
'friends-column-actions-visible': actionsVisible,
})}>
{actions}
</div>
</Animated.div>
);
},
});
const FriendsTableHeader = () =>
<div className="friends-table-header">
<FriendsColumn column="name">{i18n.Messages.FRIENDS_COLUMN_NAME}</FriendsColumn>
<FriendsColumnSeparator />
<FriendsColumn column="status">{i18n.Messages.FRIENDS_COLUMN_STATUS}</FriendsColumn>
<FriendsColumnSeparator />
<FriendsColumn column="guilds">{i18n.Messages.MUTUAL_GUILDS}</FriendsColumn>
<FriendsColumnSeparator />
<FriendsColumn column="actions" />
</div>;
const AddFriendHeader = React.createClass({
mixins: [PureRenderMixin, Flux.StoreListenerMixin(DetectedPlatformAccountsStore)],
getInitialState() {
return {
addFriendError: null,
addFriendSuccess: null,
};
},
getStateFromStores() {
return {
availablePlatformTypes: DetectedPlatformAccountsStore.getAvailablePlatformTypes(),
};
},
handleAddFriendSubmit(error, success) {
this.setState({addFriendError: error, addFriendSuccess: success});
},
render() {
const {availablePlatformTypes, addFriendSuccess, addFriendError} = this.state;
let connectAccounts;
if (availablePlatformTypes.length > 0) {
connectAccounts = (
<div className="connect-accounts">
<h2>{i18n.Messages.CONNECT_ACCOUNTS_TITLE}</h2>
<h3>{i18n.Messages.CONNECT_ACCOUNTS_DESCRIPTION}</h3>
<div className="connect-account-btn-group">
{availablePlatformTypes
.slice(0, 4)
.map(type => <ConnectAccountButton key={type} type={type} location="Friends" />)}
</div>
</div>
);
}
let success = false;
let error = false;
let description = i18n.Messages.ADD_FRIEND_DESCRIPTION;
if (addFriendSuccess != null) {
success = true;
description = addFriendSuccess;
} else if (addFriendError != null) {
error = true;
description = addFriendError;
}
return (
<div className="friend-table-add-wrapper">
<div className="friend-table-add-header">
<div className="friends-table-add">
<h2>{i18n.Messages.ADD_FRIEND}</h2>
<div className={classNames('friend-table-add-description', {success, error})}>{description}</div>
<AddFriendInput onSubmit={this.handleAddFriendSubmit} />
</div>
{connectAccounts}
</div>
<div className="friend-table-suggestions-header">
<h2>{i18n.Messages.FRIEND_SUGGESTIONS}</h2>
</div>
</div>
);
},
});
const Friends = React.createClass({
mixins: [PureRenderMixin, Flux.StoreListenerMixin(FriendsStore, FriendSuggestionStore)],
getStateFromStores() {
return {
suggestionCount: FriendSuggestionStore.getSuggestionCount(),
suggestions: FriendSuggestionStore.getSuggestions(),
...FriendsStore.getState(),
};
},
handleItemSelect(section) {
FriendsActionCreators.setSection(section);
},
handleAddFriend() {
FriendsActionCreators.setSection(FriendsSections.ADD_FRIEND);
},
render() {
let rows;
let tableHeader;
if (this.state.section === FriendsSections.ADD_FRIEND) {
rows = this.state.suggestions.map(props => <FriendSuggestionRow {...props} />);
tableHeader = <AddFriendHeader />;
} else {
rows = this.state.rows.filter(this.state.section).map(props => <FriendRow {...props.toJS()} />);
tableHeader = <FriendsTableHeader />;
}
let emptyState;
if (rows.length === 0) {
emptyState = (
<FriendsEmptyState key={this.state.section} type={this.state.section} onClick={this.handleAddFriend} />
);
}
return (
<div id="friends">
<div className="friends-header">
<TabBar selectedItem={this.state.section} type={TabBarTypes.UNIQUE} onItemSelect={this.handleItemSelect}>
<TabBarItem className="tab-bar-item-primary" key={FriendsSections.ADD_FRIEND}>
{i18n.Messages.FRIENDS_SECTION_ADD_FRIEND} <Badge value={this.state.suggestionCount} />
</TabBarItem>
<TabBarSeparator />
<TabBarItem key={FriendsSections.ALL}>{i18n.Messages.FRIENDS_SECTION_ALL}</TabBarItem>
<TabBarItem key={FriendsSections.ONLINE}>{i18n.Messages.FRIENDS_SECTION_ONLINE}</TabBarItem>
<TabBarItem key={FriendsSections.PENDING}>
{i18n.Messages.FRIENDS_SECTION_PENDING} <Badge value={this.state.pendingCount} />
</TabBarItem>
<TabBarSeparator />
<TabBarItem key={FriendsSections.BLOCKED}>{i18n.Messages.BLOCKED}</TabBarItem>
</TabBar>
<HeaderToolbar>
<PrivateChannelRecipientsInviteButton tooltip={i18n.Messages.NEW_GROUP_DM} />
</HeaderToolbar>
</div>
<div className="friends-table">
{tableHeader}
<Scroller className="friends-table-body">
<TransitionGroup transitionAppear={false}>
{rows}
{emptyState}
</TransitionGroup>
</Scroller>
</div>
</div>
);
},
});
export default Friends;
// WEBPACK FOOTER //
// ./discord_app/components/Friends.js