585 lines
16 KiB
JavaScript
585 lines
16 KiB
JavaScript
|
/* @flow */
|
||
|
|
||
|
import React from 'react';
|
||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||
|
import Flux from '../lib/flux';
|
||
|
import i18n from '../i18n';
|
||
|
import Platforms from '../lib/Platforms';
|
||
|
import Avatar from './common/Avatar';
|
||
|
import TabBar, {TabBarItem, TabBarTypes} from './common/TabBar';
|
||
|
import GuildIcon from './common/GuildIcon';
|
||
|
import UserProfileModalStore from '../stores/UserProfileModalStore';
|
||
|
import UserStore from '../stores/UserStore';
|
||
|
import StreamerModeStore from '../stores/StreamerModeStore';
|
||
|
import {popLayer} from '../actions/LayerActionCreators';
|
||
|
import UserProfileModalActionCreators from '../actions/UserProfileModalActionCreators';
|
||
|
import RelationshipActionCreators from '../actions/RelationshipActionCreators';
|
||
|
import ChannelActionCreators from '../actions/ChannelActionCreators';
|
||
|
import UserSettingsModalActionCreators from '../actions/UserSettingsModalActionCreators';
|
||
|
import ContextMenu from './common/ContextMenu';
|
||
|
import UserContextMenu from './contextmenus/UserContextMenu';
|
||
|
import GuildContextMenu from './contextmenus/GuildContextMenu';
|
||
|
import Scroller from './common/Scroller';
|
||
|
import Spinner from './common/Spinner';
|
||
|
import Popout from './common/Popout';
|
||
|
import Note from './common/Note';
|
||
|
import Tooltip from './common/Tooltip';
|
||
|
import DiscordTag from './common/DiscordTag';
|
||
|
import RouterUtils from '../utils/RouterUtils';
|
||
|
import {renderActivity} from '../utils/ActivityUtils';
|
||
|
import UserRecord from '../records/UserRecord';
|
||
|
import type GuildRecord from '../records/GuildRecord';
|
||
|
import '../styles/user_profile_modal.styl';
|
||
|
import '../styles/common/context_menu.styl';
|
||
|
import {
|
||
|
RelationshipTypes,
|
||
|
UserProfileSections,
|
||
|
Routes,
|
||
|
ContextMenuTypes,
|
||
|
UserSettingsSections,
|
||
|
UserFlags,
|
||
|
MARKETING_URLS,
|
||
|
} from '../Constants';
|
||
|
|
||
|
// User Info
|
||
|
const UserInfo = React.createClass({
|
||
|
mixins: [PureRenderMixin, Flux.StoreListenerMixin(StreamerModeStore)],
|
||
|
|
||
|
propTypes: {
|
||
|
user: React.PropTypes.instanceOf(UserRecord).isRequired,
|
||
|
connectedAccounts: React.PropTypes.array.isRequired,
|
||
|
},
|
||
|
|
||
|
getStateFromStores() {
|
||
|
return {
|
||
|
hide: StreamerModeStore.hidePersonalInformation,
|
||
|
};
|
||
|
},
|
||
|
|
||
|
render() {
|
||
|
if (this.state.hide) {
|
||
|
return (
|
||
|
<div className="empty">
|
||
|
<div className="empty-icon-streamer-mode" />
|
||
|
<div className="empty-text">{i18n.Messages.STREAMER_MODE_ENABLED}</div>
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
let connectedAccounts;
|
||
|
if (this.props.connectedAccounts != null && this.props.connectedAccounts.length > 0) {
|
||
|
const accountRows = this.props.connectedAccounts.map((account, i) => {
|
||
|
const platform = Platforms.get(account.type);
|
||
|
const platformUrl = platform.getPlatformUserUrl && platform.getPlatformUserUrl(account);
|
||
|
let openIcon;
|
||
|
if (platformUrl) {
|
||
|
openIcon = (
|
||
|
<a href={platformUrl} rel="noreferrer" target="_blank">
|
||
|
<div className="connected-account-open-icon" />
|
||
|
</a>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
let verifiedIcon;
|
||
|
if (account.verified) {
|
||
|
verifiedIcon = (
|
||
|
<Tooltip text={i18n.Messages.CONNECTION_VERIFIED}>
|
||
|
<i className="connected-account-verified-icon" />
|
||
|
</Tooltip>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<div key={i} className="connected-account">
|
||
|
<img className="connected-account-icon" src={platform.icon.color} />
|
||
|
<div className="connected-account-name-inner">
|
||
|
<div className="connected-account-name">{account.name}</div>
|
||
|
{verifiedIcon}
|
||
|
</div>
|
||
|
{openIcon}
|
||
|
</div>
|
||
|
);
|
||
|
});
|
||
|
connectedAccounts = (
|
||
|
<div className="section">
|
||
|
<div className="connected-accounts">
|
||
|
{accountRows}
|
||
|
</div>
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<Scroller className="guilds" fade={true}>
|
||
|
<div className="section">
|
||
|
<div className="section-header">{i18n.Messages.NOTE}</div>
|
||
|
<Note userId={this.props.user.id} autoFocus={false} />
|
||
|
</div>
|
||
|
{connectedAccounts}
|
||
|
</Scroller>
|
||
|
);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Mutual Guilds
|
||
|
const GuildRow = ({
|
||
|
guild,
|
||
|
nick,
|
||
|
onSelect,
|
||
|
onContextMenu,
|
||
|
}: {
|
||
|
guild: GuildRecord,
|
||
|
nick: string,
|
||
|
onSelect: Function,
|
||
|
onContextMenu: Function,
|
||
|
}) =>
|
||
|
<div className="guild" onClick={() => onSelect(guild.id)} onContextMenu={event => onContextMenu(event, guild)}>
|
||
|
<GuildIcon guild={guild} size="large" />
|
||
|
<div className="guild-inner">
|
||
|
<div className="guild-name">{guild.toString()}</div>
|
||
|
<div className="guild-nick">{nick}</div>
|
||
|
</div>
|
||
|
</div>;
|
||
|
|
||
|
const MutualGuilds = React.createClass({
|
||
|
propTypes: {
|
||
|
mutualGuilds: React.PropTypes.array.isRequired,
|
||
|
},
|
||
|
|
||
|
handleSelect(guildId) {
|
||
|
RouterUtils.transitionTo(Routes.GUILD(guildId));
|
||
|
UserProfileModalActionCreators.close();
|
||
|
popLayer();
|
||
|
},
|
||
|
|
||
|
handleContextMenu(event, guild) {
|
||
|
ContextMenu.openContextMenu(event, props =>
|
||
|
<GuildContextMenu {...props} type={ContextMenuTypes.GUILD_PROFILE_MUTUAL} guild={guild} />
|
||
|
);
|
||
|
},
|
||
|
|
||
|
render() {
|
||
|
if (this.props.mutualGuilds.length === 0) {
|
||
|
return (
|
||
|
<div className="empty">
|
||
|
<div className="empty-icon-guilds" />
|
||
|
<div className="empty-text">{i18n.Messages.NO_MUTUAL_GUILDS}</div>
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
const guildRows = this.props.mutualGuilds.map(({guild, nick}) =>
|
||
|
<GuildRow
|
||
|
key={guild.id}
|
||
|
guild={guild}
|
||
|
nick={nick}
|
||
|
onSelect={this.handleSelect}
|
||
|
onContextMenu={this.handleContextMenu}
|
||
|
/>
|
||
|
);
|
||
|
|
||
|
return (
|
||
|
<Scroller className="guilds" fade={true}>
|
||
|
{guildRows}
|
||
|
</Scroller>
|
||
|
);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Mutual Friends
|
||
|
const FriendRow = ({
|
||
|
user,
|
||
|
status,
|
||
|
onSelect,
|
||
|
onContextMenu,
|
||
|
}: {
|
||
|
user: UserRecord,
|
||
|
status: string,
|
||
|
onSelect: Function,
|
||
|
onContextMenu: Function,
|
||
|
}) =>
|
||
|
<div className="guild" onClick={() => onSelect(user.id)} onContextMenu={event => onContextMenu(event, user)}>
|
||
|
<Avatar user={user} size="large" status={status} />
|
||
|
<div className="guild-inner">
|
||
|
<DiscordTag user={user} className="guild-name" />
|
||
|
</div>
|
||
|
</div>;
|
||
|
|
||
|
const MutualFriends = React.createClass({
|
||
|
propTypes: {
|
||
|
mutualFriends: React.PropTypes.array,
|
||
|
},
|
||
|
|
||
|
handleSelect(userId) {
|
||
|
UserProfileModalActionCreators.open(userId);
|
||
|
},
|
||
|
|
||
|
handleContextMenu(event, user) {
|
||
|
ContextMenu.openContextMenu(event, props =>
|
||
|
<UserContextMenu {...props} type={ContextMenuTypes.USER_PROFILE_MUTUAL} user={user} />
|
||
|
);
|
||
|
},
|
||
|
|
||
|
render() {
|
||
|
const {mutualFriends} = this.props;
|
||
|
|
||
|
const friendRows = [];
|
||
|
|
||
|
if (!mutualFriends) {
|
||
|
return <div className="empty"><Spinner /></div>;
|
||
|
} else if (mutualFriends.length === 0) {
|
||
|
return (
|
||
|
<div className="empty">
|
||
|
<div className="empty-icon-friends" />
|
||
|
<div className="empty-text">{i18n.Messages.NO_MUTUAL_FRIENDS}</div>
|
||
|
</div>
|
||
|
);
|
||
|
} else {
|
||
|
mutualFriends.forEach(rel => {
|
||
|
friendRows.push(
|
||
|
<FriendRow
|
||
|
key={rel.key}
|
||
|
user={rel.user}
|
||
|
status={rel.status}
|
||
|
onSelect={this.handleSelect}
|
||
|
onContextMenu={this.handleContextMenu}
|
||
|
/>
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<Scroller className="guilds" fade={true}>
|
||
|
{friendRows}
|
||
|
</Scroller>
|
||
|
);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// Footer
|
||
|
const RequestActions = ({user, relationshipType, friendReqSent, handleAddFriend, handleIgnoreFriend}) => {
|
||
|
let actions = null;
|
||
|
|
||
|
if (
|
||
|
user.id !== UserStore.getCurrentUser().id &&
|
||
|
!user.bot &&
|
||
|
relationshipType != RelationshipTypes.FRIEND &&
|
||
|
relationshipType != RelationshipTypes.BLOCKED
|
||
|
) {
|
||
|
if (friendReqSent) {
|
||
|
actions = (
|
||
|
<div className="actions">
|
||
|
<button disabled={true} className="btn disabled">
|
||
|
{i18n.Messages.ADD_FRIEND_BUTTON_AFTER}
|
||
|
</button>
|
||
|
</div>
|
||
|
);
|
||
|
} else if (relationshipType === RelationshipTypes.PENDING_INCOMING) {
|
||
|
actions = (
|
||
|
<div className="actions">
|
||
|
<div className="pending">
|
||
|
<div className="pending-actions">
|
||
|
<button className="btn add-friend" onClick={handleAddFriend}>
|
||
|
{i18n.Messages.FRIEND_REQUEST_ACCEPT}
|
||
|
</button>
|
||
|
<button className="btn reject-friend " onClick={handleIgnoreFriend}>
|
||
|
{i18n.Messages.FRIEND_REQUEST_IGNORE}
|
||
|
</button>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
);
|
||
|
} else {
|
||
|
actions = (
|
||
|
<div className="actions">
|
||
|
<button className="btn add-friend" onClick={handleAddFriend}>
|
||
|
{i18n.Messages.ADD_FRIEND_BUTTON}
|
||
|
</button>
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return actions;
|
||
|
};
|
||
|
|
||
|
// Popout additional actions
|
||
|
const ProfileActionsPopout = React.createClass({
|
||
|
propTypes: {
|
||
|
relationshipType: React.PropTypes.number,
|
||
|
onRemoveFriend: React.PropTypes.func.isRequired,
|
||
|
onBlock: React.PropTypes.func.isRequired,
|
||
|
onMessage: React.PropTypes.func.isRequired,
|
||
|
onClose: React.PropTypes.func.isRequired,
|
||
|
},
|
||
|
|
||
|
performAction(action) {
|
||
|
action();
|
||
|
this.props.onClose();
|
||
|
},
|
||
|
|
||
|
render() {
|
||
|
const {relationshipType, onRemoveFriend, onBlock, onMessage} = this.props;
|
||
|
|
||
|
const options = [];
|
||
|
|
||
|
switch (relationshipType) {
|
||
|
case RelationshipTypes.BLOCKED:
|
||
|
options.push(
|
||
|
<div className="item" onClick={this.performAction.bind(this, onRemoveFriend)} key="user-unblock">
|
||
|
{i18n.Messages.UNBLOCK}
|
||
|
</div>
|
||
|
);
|
||
|
break;
|
||
|
case RelationshipTypes.FRIEND:
|
||
|
options.push(
|
||
|
<div className="item danger" onClick={this.performAction.bind(this, onRemoveFriend)} key="user-remove">
|
||
|
{i18n.Messages.REMOVE_FRIEND}
|
||
|
</div>
|
||
|
);
|
||
|
options.push(
|
||
|
<div className="item danger" onClick={this.performAction.bind(this, onBlock)} key="user-block">
|
||
|
{i18n.Messages.BLOCK}
|
||
|
</div>
|
||
|
);
|
||
|
break;
|
||
|
case RelationshipTypes.NONE:
|
||
|
case RelationshipTypes.PENDING_INCOMING:
|
||
|
default:
|
||
|
options.push(
|
||
|
<div className="item danger" onClick={this.performAction.bind(this, onBlock)} key="user-block">
|
||
|
{i18n.Messages.BLOCK}
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (relationshipType !== RelationshipTypes.BLOCKED) {
|
||
|
options.push(
|
||
|
<div className="item" onClick={this.performAction.bind(this, onMessage)} key="user-message">
|
||
|
{i18n.Messages.USER_POPOUT_MESSAGE}
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<div className="context-menu">
|
||
|
{options}
|
||
|
</div>
|
||
|
);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// MODAL
|
||
|
const UserProfileModal = React.createClass({
|
||
|
mixins: [PureRenderMixin],
|
||
|
|
||
|
statics: {
|
||
|
modalConfig: {
|
||
|
store: UserProfileModalStore,
|
||
|
},
|
||
|
},
|
||
|
|
||
|
propTypes: {
|
||
|
section: React.PropTypes.string.isRequired,
|
||
|
user: React.PropTypes.instanceOf(UserRecord).isRequired,
|
||
|
status: React.PropTypes.string,
|
||
|
activity: React.PropTypes.object,
|
||
|
relationshipType: React.PropTypes.number,
|
||
|
mutualGuilds: React.PropTypes.array,
|
||
|
mutualFriends: React.PropTypes.array,
|
||
|
connectedAccounts: React.PropTypes.array,
|
||
|
premiumSince: React.PropTypes.instanceOf(Date),
|
||
|
flags: React.PropTypes.number,
|
||
|
},
|
||
|
|
||
|
getInitialState() {
|
||
|
return {
|
||
|
friendReqSent: false,
|
||
|
};
|
||
|
},
|
||
|
|
||
|
hasFlag(flag) {
|
||
|
const {user} = this.props;
|
||
|
return (user.flags & flag) === flag;
|
||
|
},
|
||
|
|
||
|
render() {
|
||
|
const {user, status, activity, relationshipType, premiumSince, section: selectedSection} = this.props;
|
||
|
const {friendReqSent} = this.state;
|
||
|
|
||
|
let component;
|
||
|
const props = {};
|
||
|
switch (selectedSection) {
|
||
|
case UserProfileSections.MUTUAL_GUILDS:
|
||
|
component = MutualGuilds;
|
||
|
props.mutualGuilds = this.props.mutualGuilds;
|
||
|
break;
|
||
|
case UserProfileSections.MUTUAL_FRIENDS:
|
||
|
component = MutualFriends;
|
||
|
props.mutualFriends = this.props.mutualFriends;
|
||
|
break;
|
||
|
case UserProfileSections.USER_INFO:
|
||
|
default:
|
||
|
component = UserInfo;
|
||
|
props.user = this.props.user;
|
||
|
props.connectedAccounts = this.props.connectedAccounts;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
let badgeProps;
|
||
|
if (this.hasFlag(UserFlags.STAFF)) {
|
||
|
badgeProps = {
|
||
|
tooltip: i18n.Messages.STAFF_BADGE_TOOLTIP,
|
||
|
onClick: () => window.open(MARKETING_URLS.COMPANY, '_blank'),
|
||
|
class: 'badge-staff',
|
||
|
};
|
||
|
} else if (this.hasFlag(UserFlags.PARTNER)) {
|
||
|
badgeProps = {
|
||
|
tooltip: i18n.Messages.PARTNER_BADGE_TOOLTIP,
|
||
|
onClick: () => window.open(MARKETING_URLS.PARTNERS, '_blank'),
|
||
|
class: 'badge-partner',
|
||
|
};
|
||
|
} else if (this.hasFlag(UserFlags.HYPESQUAD)) {
|
||
|
badgeProps = {
|
||
|
tooltip: i18n.Messages.HYPESQUAD_BADGE_TOOLTIP,
|
||
|
onClick: () => window.open(MARKETING_URLS.HYPESQUAD, '_blank'),
|
||
|
class: 'badge-hypesquad',
|
||
|
};
|
||
|
} else if (premiumSince != null) {
|
||
|
badgeProps = {
|
||
|
tooltip: i18n.Messages.PREMIUM_BADGE_TOOLTIP.format({date: premiumSince}),
|
||
|
onClick: this.openPremiumSettings,
|
||
|
class: 'badge-premium',
|
||
|
};
|
||
|
}
|
||
|
|
||
|
let badge;
|
||
|
if (badgeProps) {
|
||
|
badge = (
|
||
|
<Tooltip position="top" text={badgeProps.tooltip}>
|
||
|
<div onClick={badgeProps.onClick} className={`profile-badge ${badgeProps.class}`} />
|
||
|
</Tooltip>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
let moreActions;
|
||
|
|
||
|
if (user.id !== UserStore.getCurrentUser().id && !user.isNonUserBot()) {
|
||
|
moreActions = (
|
||
|
<Popout
|
||
|
uniqueId="profile-actions"
|
||
|
closeOnScroll={false}
|
||
|
position={Popout.BOTTOM}
|
||
|
zIndexBoost={1}
|
||
|
animationType="none"
|
||
|
offsetX={-65}
|
||
|
offsetY={-10}
|
||
|
render={props =>
|
||
|
<ProfileActionsPopout
|
||
|
{...props}
|
||
|
relationshipType={relationshipType}
|
||
|
onRemoveFriend={this.handleRemoveFriend}
|
||
|
onBlock={this.handleBlock}
|
||
|
onMessage={this.handleSendMessage}
|
||
|
/>}>
|
||
|
<div className="additional-actions-icon" />
|
||
|
</Popout>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<div id="user-profile-modal">
|
||
|
<header className="header">
|
||
|
<div className="header-info">
|
||
|
{badge}
|
||
|
|
||
|
<div className="header-info-inner">
|
||
|
<DiscordTag user={user} />
|
||
|
{activity && <div className="activity">{renderActivity(activity)}</div>}
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<RequestActions
|
||
|
user={user}
|
||
|
relationshipType={relationshipType}
|
||
|
friendReqSent={friendReqSent}
|
||
|
handleAddFriend={this.handleAddFriend}
|
||
|
handleIgnoreFriend={this.handleRemoveFriend}
|
||
|
/>
|
||
|
|
||
|
{moreActions}
|
||
|
</header>
|
||
|
|
||
|
<div className="tab-bar-container">
|
||
|
<TabBar selectedItem={selectedSection} type={TabBarTypes.TOP} onItemSelect={this.handleSectionSelect}>
|
||
|
<TabBarItem key={UserProfileSections.USER_INFO}>
|
||
|
{i18n.Messages.USER_INFO}
|
||
|
</TabBarItem>
|
||
|
<TabBarItem key={UserProfileSections.MUTUAL_GUILDS}>
|
||
|
{i18n.Messages.MUTUAL_GUILDS}
|
||
|
</TabBarItem>
|
||
|
<TabBarItem key={UserProfileSections.MUTUAL_FRIENDS}>
|
||
|
{i18n.Messages.MUTUAL_FRIENDS}
|
||
|
</TabBarItem>
|
||
|
</TabBar>
|
||
|
</div>
|
||
|
|
||
|
<div className="avatar-wrapper">
|
||
|
<Avatar user={user} size="profile" status={status} animate />
|
||
|
</div>
|
||
|
|
||
|
{React.createElement(component, props)}
|
||
|
|
||
|
</div>
|
||
|
);
|
||
|
},
|
||
|
|
||
|
// utils
|
||
|
close() {
|
||
|
UserProfileModalActionCreators.close();
|
||
|
},
|
||
|
|
||
|
// action handlers
|
||
|
handleSendMessage() {
|
||
|
ChannelActionCreators.openPrivateChannel(UserStore.getCurrentUser().id, this.props.user.id);
|
||
|
this.close();
|
||
|
popLayer();
|
||
|
},
|
||
|
|
||
|
handleBlock() {
|
||
|
const {user} = this.props;
|
||
|
RelationshipActionCreators.addRelationship(user.id, {location: 'User Profile'}, RelationshipTypes.BLOCKED);
|
||
|
},
|
||
|
|
||
|
handleAddFriend() {
|
||
|
RelationshipActionCreators.addRelationship(this.props.user.id, {location: 'User Profile'});
|
||
|
|
||
|
this.setState({
|
||
|
friendReqSent: true,
|
||
|
});
|
||
|
},
|
||
|
|
||
|
handleRemoveFriend() {
|
||
|
RelationshipActionCreators.removeRelationship(this.props.user.id, {location: 'User Profile'});
|
||
|
|
||
|
this.setState({
|
||
|
friendReqSent: false,
|
||
|
});
|
||
|
},
|
||
|
|
||
|
handleSectionSelect(selectedSection: string) {
|
||
|
UserProfileModalActionCreators.setSection(selectedSection);
|
||
|
},
|
||
|
|
||
|
openPremiumSettings() {
|
||
|
UserSettingsModalActionCreators.open(UserSettingsSections.PREMIUM);
|
||
|
this.close();
|
||
|
},
|
||
|
});
|
||
|
|
||
|
export default UserProfileModal;
|
||
|
|
||
|
|
||
|
|
||
|
// WEBPACK FOOTER //
|
||
|
// ./discord_app/components/UserProfileModal.js
|