391 lines
12 KiB
JavaScript
Executable file
391 lines
12 KiB
JavaScript
Executable file
import React from 'react';
|
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
|
import Flux from '../lib/flux';
|
|
import TransitionGroup from '../lib/transitions/TransitionGroup';
|
|
import Animated from '../lib/animated';
|
|
import i18n from '../i18n';
|
|
import {Link} from 'react-router';
|
|
import lodash from 'lodash';
|
|
import classNames from 'classnames';
|
|
import Scroller from '../uikit/Scroller';
|
|
import LazyScroller from './common/LazyScroller';
|
|
import Avatar from './common/Avatar';
|
|
import Badge from './common/Badge';
|
|
import ConfirmModal from './ConfirmModal';
|
|
import UserStore from '../stores/UserStore';
|
|
import PresenceStore from '../stores/PresenceStore';
|
|
import RelationshipStore from '../stores/RelationshipStore';
|
|
import FriendSuggestionStore from '../stores/FriendSuggestionStore';
|
|
import UserContextMenu from './contextmenus/UserContextMenu';
|
|
import GroupDMContextMenu from './contextmenus/GroupDMContextMenu';
|
|
import ContextMenu from './common/ContextMenu';
|
|
import PrivateChannelsMixin from '../mixins/PrivateChannelsMixin';
|
|
import DelayedSelectionStore from '../stores/DelayedSelectionStore';
|
|
import DimensionStore from '../stores/DimensionStore';
|
|
import ChannelActionCreators from '../actions/ChannelActionCreators';
|
|
import MessageActionCreators from '../actions/MessageActionCreators';
|
|
import ModalActionCreators from '../actions/ModalActionCreators';
|
|
import DimensionActionCreators from '../actions/DimensionActionCreators';
|
|
import {show as quickSwitcherShow, search as quickSwitcherSearch} from '../actions/QuickSwitcherActionCreators';
|
|
import TypingStore from '../stores/TypingStore';
|
|
import {renderActivity, isStreaming} from '../utils/ActivityUtils';
|
|
import TutorialIndicator from './common/TutorialIndicator';
|
|
import SearchBar from './common/SearchBar';
|
|
import '../styles/private_channels.styl';
|
|
import {ME, Routes, ContextMenuTypes, ChannelTypes, Colors, ComponentActions} from '../Constants';
|
|
|
|
const SCROLLER_REF = 'SCROLLER_DMS';
|
|
// The combined height of all elements preceeding the DMs
|
|
const TO_START_OF_DMS = 104;
|
|
const LAZY_RENDER_THRESHOLD = 30;
|
|
const DM_HEIGHT = 42;
|
|
|
|
const NoPrivateChannels = () => <div className="no-private-channels" />;
|
|
|
|
const FriendsButton = React.createClass({
|
|
mixins: [Flux.StoreListenerMixin(RelationshipStore, FriendSuggestionStore)],
|
|
|
|
getStateFromStores() {
|
|
return {
|
|
badge: RelationshipStore.getPendingCount() + FriendSuggestionStore.getSuggestionCount(),
|
|
};
|
|
},
|
|
|
|
render() {
|
|
return (
|
|
<div className={classNames('channel btn-friends', {selected: this.props.selected})}>
|
|
<Link to={Routes.FRIENDS}>
|
|
<div className="icon-friends" />
|
|
<div className="channel-name">{i18n.Messages.FRIENDS}</div>
|
|
<Badge value={this.state.badge} />
|
|
</Link>
|
|
</div>
|
|
);
|
|
},
|
|
});
|
|
|
|
function createPrivateChannel(displayName, stores = [], getStateFromStores = () => {}) {
|
|
return React.createClass({
|
|
displayName,
|
|
|
|
mixins: [Flux.StoreListenerMixin(...stores), PureRenderMixin],
|
|
|
|
getInitialState() {
|
|
return {
|
|
animated: new Animated.Value(1),
|
|
};
|
|
},
|
|
|
|
getStateFromStores() {
|
|
return getStateFromStores(this.props.channel);
|
|
},
|
|
|
|
getDefaultProps() {
|
|
return {
|
|
selected: false,
|
|
};
|
|
},
|
|
|
|
componentWillEnter(callback) {
|
|
this.state.animated.setValue(0);
|
|
Animated.timing(this.state.animated, {
|
|
toValue: 1,
|
|
duration: 200,
|
|
}).start(callback);
|
|
},
|
|
|
|
componentWillLeave(callback) {
|
|
Animated.timing(this.state.animated, {
|
|
toValue: 0,
|
|
duration: 200,
|
|
}).start(callback);
|
|
},
|
|
|
|
handleClose(e) {
|
|
if (e != null) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
ChannelActionCreators.closePrivateChannel(this.props.channel.id, this.props.selected);
|
|
},
|
|
|
|
handleContextMenu(e) {
|
|
const {channel, selected} = this.props;
|
|
const {user} = this.state;
|
|
if (channel.type === ChannelTypes.GROUP_DM) {
|
|
ContextMenu.openContextMenu(
|
|
e,
|
|
props => <GroupDMContextMenu {...props} channelId={channel.id} selected={selected} />,
|
|
{noBlurEvent: true}
|
|
);
|
|
} else {
|
|
ContextMenu.openContextMenu(e, props =>
|
|
<UserContextMenu
|
|
{...props}
|
|
type={ContextMenuTypes.USER_PRIVATE_CHANNELS}
|
|
user={user}
|
|
channelId={channel.id}
|
|
selected={selected}
|
|
/>
|
|
);
|
|
}
|
|
},
|
|
|
|
handleLeaveGroup(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const name = this.props.channel.toString();
|
|
let header = i18n.Messages.LEAVE_GROUP_DM_TITLE.format({name});
|
|
let body = i18n.Messages.LEAVE_GROUP_DM_BODY.format({name});
|
|
if (this.props.channel.isManaged()) {
|
|
header = i18n.Messages.LEAVE_GROUP_DM_MANAGED_TITLE.format({name});
|
|
body = i18n.Messages.LEAVE_GROUP_DM_MANAGED_BODY.format({name});
|
|
}
|
|
ModalActionCreators.push(props => {
|
|
return (
|
|
<ConfirmModal
|
|
header={header}
|
|
confirmText={i18n.Messages.LEAVE_GROUP_DM}
|
|
cancelText={i18n.Messages.CANCEL}
|
|
onConfirm={this.handleClose}
|
|
{...props}>
|
|
<p>{body}</p>
|
|
</ConfirmModal>
|
|
);
|
|
});
|
|
},
|
|
|
|
handleMouseDown() {
|
|
MessageActionCreators.prefetchMessages(ME, this.props.channel.id);
|
|
},
|
|
|
|
render() {
|
|
const {channel} = this.props;
|
|
const {user, status, activity, typing} = this.state;
|
|
|
|
let channelName = <span className="channel-name">{channel.toString()}</span>;
|
|
if (activity) {
|
|
channelName = (
|
|
<div className="channel-name">
|
|
{channel.toString()}
|
|
<div className="channel-activity">{renderActivity(activity)}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const classes = {
|
|
channel: true,
|
|
selected: this.props.selected,
|
|
private: true,
|
|
};
|
|
|
|
const style = {
|
|
height: this.state.animated.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [0, 42],
|
|
}),
|
|
opacity: this.state.animated,
|
|
};
|
|
|
|
let avatar;
|
|
let onClick;
|
|
if (channel.type === ChannelTypes.GROUP_DM) {
|
|
avatar = <Avatar channel={channel} size="small" />;
|
|
channelName = (
|
|
<div className="channel-name">
|
|
{channel.toString()}
|
|
<div className="channel-activity">
|
|
{i18n.Messages.MEMBERS_HEADER.format({members: channel.recipients.length + 1})}
|
|
</div>
|
|
</div>
|
|
);
|
|
onClick = this.handleLeaveGroup;
|
|
} else {
|
|
avatar = <Avatar user={user} size="small" status={status} streaming={isStreaming(activity)} typing={typing} />;
|
|
onClick = this.handleClose;
|
|
}
|
|
|
|
return (
|
|
<Animated.div
|
|
className={classNames(classes)}
|
|
style={style}
|
|
key={channel.id}
|
|
ref={channel.id}
|
|
onContextMenu={this.handleContextMenu}
|
|
onMouseDown={this.handleMouseDown}>
|
|
<Link to={Routes.CHANNEL(ME, channel.id)}>
|
|
{avatar}
|
|
{channelName}
|
|
<button className="close" onClick={onClick} />
|
|
</Link>
|
|
</Animated.div>
|
|
);
|
|
},
|
|
});
|
|
}
|
|
|
|
const DirectMessage = createPrivateChannel('DirectMessage', [UserStore, PresenceStore, TypingStore], channel => {
|
|
const user = UserStore.getUser(channel.getRecipientId());
|
|
return {
|
|
user,
|
|
status: PresenceStore.getStatus(user.id),
|
|
activity: PresenceStore.getActivity(user.id),
|
|
typing: TypingStore.isTyping(channel.id, channel.getRecipientId()),
|
|
};
|
|
});
|
|
const GroupDirectMessage = createPrivateChannel('GroupDirectMessage');
|
|
|
|
const DirectMessages = React.createClass({
|
|
mixins: [
|
|
Flux.StoreListenerMixin(DelayedSelectionStore, DimensionStore, ...PrivateChannelsMixin.getPrivateChannelsStores()),
|
|
PrivateChannelsMixin,
|
|
PureRenderMixin,
|
|
],
|
|
|
|
getInitialState() {
|
|
return {
|
|
initialized: false,
|
|
};
|
|
},
|
|
|
|
getStateFromStores() {
|
|
const {scrollTo, scrollTop} = DimensionStore.getGuildDimensions(ME);
|
|
return {
|
|
selectedChannelId: DelayedSelectionStore.getChannelId(ME),
|
|
scrollTo,
|
|
scrollTop,
|
|
...this.getPrivateChannelsState(),
|
|
};
|
|
},
|
|
|
|
getSubscriptions() {
|
|
return {
|
|
[ComponentActions.SCROLLTO_CHANNEL]: ({channel, guild}) => {
|
|
if (guild !== ME) {
|
|
return;
|
|
}
|
|
this.setState({
|
|
scrollTo: {guild, channel},
|
|
});
|
|
},
|
|
};
|
|
},
|
|
|
|
componentDidMount() {
|
|
this.calculateScrollState({});
|
|
},
|
|
|
|
componentDidUpdate(prevProps, prevState) {
|
|
this.calculateScrollState(prevState);
|
|
},
|
|
|
|
calculateScrollState(prevState) {
|
|
if (this.state.privateChannelIds.length && !this.state.initialized) {
|
|
if (this.state.scrollTo) {
|
|
this.scrollToChannel(this.state.scrollTo);
|
|
DimensionActionCreators.clearChannelListScrollTo(ME);
|
|
} else if (this.state.scrollTop || this.state.scrollTop === 0) {
|
|
this.refs[SCROLLER_REF].scrollTo(this.state.scrollTop);
|
|
} else if (this.state.selectedChannelId) {
|
|
this.scrollToChannel(this.state.selectedChannelId);
|
|
}
|
|
this.setState({initialized: true});
|
|
} else if (this.state.privateChannelIds.length) {
|
|
if (this.state.scrollTo) {
|
|
this.scrollToChannel(this.state.scrollTo);
|
|
DimensionActionCreators.clearChannelListScrollTo(ME);
|
|
} else if (this.state.selectedChannelId !== prevState.selectedChannelId) {
|
|
this.scrollToChannel(this.state.selectedChannelId);
|
|
}
|
|
}
|
|
},
|
|
|
|
scrollToChannel(channelId, animated) {
|
|
const channelIndex = this.state.privateChannelIds.indexOf(channelId);
|
|
let scrollTo = 0;
|
|
if (channelId !== undefined && channelIndex === -1) {
|
|
return;
|
|
} else if (channelIndex !== -1) {
|
|
scrollTo += TO_START_OF_DMS + DM_HEIGHT * channelIndex;
|
|
}
|
|
|
|
this.refs[SCROLLER_REF].scrollIntoViewRect(scrollTo, scrollTo + DM_HEIGHT, animated);
|
|
},
|
|
|
|
render() {
|
|
const selectedChannelId = this.state.selectedChannelId;
|
|
let children = [
|
|
<FriendsButton key="friends-button" selected={selectedChannelId == null} />,
|
|
<header key={i18n.Messages.DIRECT_MESSAGES}>{i18n.Messages.DIRECT_MESSAGES}</header>,
|
|
];
|
|
this.state.privateChannelIds.forEach(channelId => {
|
|
const channel = this.state.channels[channelId];
|
|
if (channel.type === ChannelTypes.GROUP_DM) {
|
|
children.push(
|
|
<GroupDirectMessage key={channel.id} selected={selectedChannelId === channel.id} channel={channel} />
|
|
);
|
|
} else {
|
|
children.push(<DirectMessage key={channel.id} selected={selectedChannelId === channel.id} channel={channel} />);
|
|
}
|
|
});
|
|
|
|
const scrollerProps = {
|
|
theme: LazyScroller.Themes.GHOST_HAIRLINE,
|
|
fade: true,
|
|
ref: SCROLLER_REF,
|
|
onScroll: lodash.throttle(this.handleScroll, 100),
|
|
};
|
|
|
|
let ScrollerComponent = Scroller;
|
|
|
|
if (children.length === 2) {
|
|
children.push(<NoPrivateChannels key="no-private-channels" />);
|
|
scrollerProps.style = {paddingTop: 20};
|
|
} else if (children.length > LAZY_RENDER_THRESHOLD) {
|
|
ScrollerComponent = LazyScroller;
|
|
scrollerProps.elementHeight = DM_HEIGHT;
|
|
scrollerProps.batchEndMultiplier = 1.3;
|
|
scrollerProps.track = false;
|
|
scrollerProps.backgroundColor = Colors.PRIMARY_630;
|
|
scrollerProps.paddingTop = 20;
|
|
scrollerProps.paddingBottom = 20;
|
|
} else {
|
|
scrollerProps.style = {paddingTop: 20};
|
|
children = <TransitionGroup transitionAppear={false}>{children}</TransitionGroup>;
|
|
}
|
|
return (
|
|
<ScrollerComponent {...scrollerProps}>
|
|
{children}
|
|
</ScrollerComponent>
|
|
);
|
|
},
|
|
|
|
handleScroll() {
|
|
DimensionActionCreators.updateChannelListScroll(ME, this.refs[SCROLLER_REF].getScrollerNode().scrollTop);
|
|
},
|
|
});
|
|
|
|
class PrivateChannels extends React.PureComponent {
|
|
handleQueryChange(query) {
|
|
quickSwitcherShow('DM_SEARCH');
|
|
quickSwitcherSearch(`@${query}`);
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div className={classNames('private-channels')}>
|
|
<TutorialIndicator tutorialId="direct-messages" position={TutorialIndicator.RIGHT} offsetY={-18} offsetX={-52}>
|
|
<SearchBar onQueryChange={this.handleQueryChange} />
|
|
</TutorialIndicator>
|
|
<DirectMessages />
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default PrivateChannels;
|
|
|
|
|
|
|
|
// WEBPACK FOOTER //
|
|
// ./discord_app/components/PrivateChannels.js
|