256 lines
7.6 KiB
JavaScript
Executable file
256 lines
7.6 KiB
JavaScript
Executable file
import React from 'react';
|
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
|
import classNames from 'classnames';
|
|
import TimerMixin from 'react-timer-mixin';
|
|
import List from './common/List';
|
|
import Animated from '../lib/animated';
|
|
import shallowEqual from '../../discord_common/js/lib/shallowEqual';
|
|
import UserContextMenu from './contextmenus/UserContextMenu';
|
|
import BotTag from './common/BotTag';
|
|
import Avatar from './common/Avatar';
|
|
import Popout from './common/Popout';
|
|
import UserPopout from './UserPopout';
|
|
import ContextMenu from './common/ContextMenu';
|
|
import ChannelMemberStore from '../stores/views/ChannelMemberStore';
|
|
import TypingStore from '../stores/TypingStore';
|
|
import GuildSyncStore from '../stores/GuildSyncStore';
|
|
import WindowStore from '../stores/WindowStore';
|
|
import UserSettingsStore from '../stores/UserSettingsStore';
|
|
import Flux from '../lib/flux';
|
|
import TutorialIndicator from './common/TutorialIndicator';
|
|
import PermissionMixin from '../mixins/PermissionMixin';
|
|
import {renderActivity, isStreaming} from '../utils/ActivityUtils';
|
|
import {ContextMenuTypes, StatusTypes, ThemeTypes} from '../Constants';
|
|
import '../styles/channel_members.styl';
|
|
|
|
const PLACEHOLDER_MEMBER_ROW_HEIGHT = 40; // In sync with the CSS
|
|
|
|
const ChannelMembersLoading = React.createClass({
|
|
mixins: [TimerMixin, PureRenderMixin],
|
|
|
|
propTypes: {
|
|
memberCount: React.PropTypes.number.isRequired,
|
|
isSyncing: React.PropTypes.bool.isRequired,
|
|
},
|
|
|
|
getInitialState() {
|
|
const {isSyncing} = this.props;
|
|
return {
|
|
shouldRender: isSyncing,
|
|
opacity: new Animated.Value(isSyncing ? 1 : 0),
|
|
};
|
|
},
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
if (nextProps.isSyncing && !this.state.shouldRender) {
|
|
this.state.opacity.setValue(1);
|
|
this.setState({
|
|
shouldRender: true,
|
|
});
|
|
}
|
|
},
|
|
|
|
componentDidUpdate(prevProps) {
|
|
if (this.state.shouldRender && !this.props.isSyncing && prevProps.isSyncing) {
|
|
// On next frame, start fadeOut
|
|
this.setImmediate(() => {
|
|
Animated.timing(this.state.opacity, {
|
|
toValue: 0,
|
|
duration: 170,
|
|
easing: Animated.Easing.easeOut,
|
|
}).start(() => {
|
|
this.setState({
|
|
shouldRender: false,
|
|
});
|
|
});
|
|
});
|
|
}
|
|
},
|
|
|
|
render() {
|
|
if (!this.state.shouldRender) return null;
|
|
|
|
const rows = [];
|
|
for (let i = 0; i < this.props.memberCount; i++) {
|
|
rows.push(<div key={i} className="member" />);
|
|
}
|
|
|
|
return (
|
|
<Animated.div className="channel-members-loading" style={{opacity: this.state.opacity}}>
|
|
<div className="background" />
|
|
<div className="heading" />
|
|
{rows}
|
|
</Animated.div>
|
|
);
|
|
},
|
|
});
|
|
|
|
const ChannelMember = React.createClass({
|
|
mixins: [Flux.StoreListenerMixin(TypingStore)],
|
|
|
|
propTypes: {
|
|
colorString: React.PropTypes.any, // seems to be a string or number right now :|
|
|
darkenColor: React.PropTypes.bool,
|
|
user: React.PropTypes.object.isRequired,
|
|
channelId: React.PropTypes.string.isRequired,
|
|
guildId: React.PropTypes.string.isRequired,
|
|
status: React.PropTypes.oneOf(Object.values(StatusTypes)),
|
|
},
|
|
|
|
getStateFromStores() {
|
|
return {
|
|
typing: TypingStore.isTyping(this.props.channelId, this.props.user.id),
|
|
};
|
|
},
|
|
|
|
shouldComponentUpdate(nextProps, nextState) {
|
|
return !shallowEqual(this.props, nextProps, ['channelId']) || !shallowEqual(this.state, nextState);
|
|
},
|
|
|
|
renderUserContextMenu(e) {
|
|
ContextMenu.openContextMenu(e, props =>
|
|
<UserContextMenu
|
|
{...props}
|
|
type={ContextMenuTypes.USER_CHANNEL_MEMBERS}
|
|
user={this.props.user}
|
|
guildId={this.props.guildId}
|
|
channelId={this.props.channelId}
|
|
/>
|
|
);
|
|
},
|
|
|
|
renderUserPopout(props) {
|
|
return (
|
|
<UserPopout {...props} user={this.props.user} guildId={this.props.guildId} channelId={this.props.channelId} />
|
|
);
|
|
},
|
|
|
|
renderContent() {
|
|
const {user, colorString, darkenColor, activity, status, nick} = this.props;
|
|
|
|
let activityNode;
|
|
if (activity) {
|
|
activityNode = <div className="member-activity">{renderActivity(activity)}</div>;
|
|
}
|
|
|
|
let color;
|
|
if (colorString) {
|
|
color = {color: colorString};
|
|
|
|
if (darkenColor) {
|
|
color.WebkitFilter = 'grayscale(50%)';
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
onContextMenu={this.renderUserContextMenu}
|
|
className={classNames('member', 'member-status', `member-status-${status}`)}>
|
|
<Avatar user={user} status={status} streaming={isStreaming(activity)} typing={this.state.typing} />
|
|
<div className="member-inner">
|
|
<div className="member-username" style={color}>
|
|
<span className="member-username-inner">{nick}</span>
|
|
{user.bot ? <BotTag /> : null}
|
|
</div>
|
|
{activityNode}
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
|
|
render() {
|
|
return (
|
|
<Popout closeOnScroll={false} render={this.renderUserPopout} position={Popout.LEFT} offsetX={15}>
|
|
{this.renderContent()}
|
|
</Popout>
|
|
);
|
|
},
|
|
});
|
|
|
|
const ChannelMembers = React.createClass({
|
|
mixins: [Flux.StoreListenerMixin(ChannelMemberStore, GuildSyncStore, WindowStore), PermissionMixin],
|
|
|
|
getStateFromStores() {
|
|
return {
|
|
...ChannelMemberStore.getMemberGroups(this.props.channel.id),
|
|
isSyncing: GuildSyncStore.isSyncing(this.props.channel['guild_id']),
|
|
windowSize: WindowStore.windowSize(),
|
|
theme: UserSettingsStore.theme,
|
|
};
|
|
},
|
|
|
|
shouldComponentUpdate(nextProps, nextState) {
|
|
return (
|
|
nextState.version !== this.state.version ||
|
|
this.didPermissionsUpdate(nextState, nextProps.channel) ||
|
|
nextState.isSyncing !== this.state.isSyncing ||
|
|
(!nextState.isSyncing && nextState.windowSize.height !== this.state.windowSize.height)
|
|
);
|
|
},
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
this.setState({
|
|
...ChannelMemberStore.getMemberGroups(nextProps.channel.id),
|
|
isSyncing: GuildSyncStore.isSyncing(nextProps.channel['guild_id']),
|
|
});
|
|
},
|
|
|
|
renderSection(section) {
|
|
const {key, title, users} = this.state.memberGroups[section];
|
|
return <h2 key={key}>{title}—{users.length}</h2>;
|
|
},
|
|
|
|
renderRow(section, row) {
|
|
const {channel} = this.props;
|
|
const {darken, users} = this.state.memberGroups[section];
|
|
const props = users[row];
|
|
return (
|
|
<ChannelMember
|
|
key={props.user.id}
|
|
{...props}
|
|
darkenColor={darken}
|
|
channelId={channel.id}
|
|
guildId={channel['guild_id']}
|
|
/>
|
|
);
|
|
},
|
|
|
|
render() {
|
|
const {theme} = this.state;
|
|
const sections = this.state.memberGroups.map(memberGroup => memberGroup.users.length);
|
|
const memberCount = Math.ceil(this.state.windowSize.height / PLACEHOLDER_MEMBER_ROW_HEIGHT);
|
|
// FIXME: Fix these colors when updating channel members to new theme colors
|
|
const backgroundColor = theme === ThemeTypes.LIGHT ? '#f3f3f3' : '#2e3136';
|
|
|
|
return (
|
|
<TutorialIndicator
|
|
tutorialId="whos-online"
|
|
position={TutorialIndicator.LEFT}
|
|
offsetYPercent={-50}
|
|
offsetX={16}
|
|
offsetY={16}>
|
|
<div className="channel-members-wrap">
|
|
<List
|
|
className="channel-members"
|
|
paddingTop={0}
|
|
backgroundColor={backgroundColor}
|
|
theme={List.Themes.GHOST}
|
|
sectionHeight={50}
|
|
rowHeight={40}
|
|
renderSection={this.renderSection}
|
|
renderRow={this.renderRow}
|
|
sections={sections}
|
|
/>
|
|
<ChannelMembersLoading memberCount={memberCount} isSyncing={this.state.isSyncing} />
|
|
</div>
|
|
</TutorialIndicator>
|
|
);
|
|
},
|
|
});
|
|
|
|
export default ChannelMembers;
|
|
|
|
|
|
|
|
// WEBPACK FOOTER //
|
|
// ./discord_app/components/ChannelMembers.js
|