GH-4095 Favorite/Starred Channels (#4222)

Этот коммит содержится в:
Alexander Smaga
2016-10-26 15:20:45 +03:00
коммит произвёл Christopher Speller
родитель 66ed155a58
Коммит b354d25d37
8 изменённых файлов: 297 добавлений и 78 удалений

Просмотреть файл

@@ -26,7 +26,9 @@ import WebrtcStore from 'stores/webrtc_store.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import * as WebrtcActions from 'actions/webrtc_actions.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
@@ -63,18 +65,19 @@ export default class ChannelHeader extends React.Component {
}
getStateFromStores() {
const channel = ChannelStore.get(this.props.channelId);
const stats = ChannelStore.getStats(this.props.channelId);
const users = UserStore.getProfileListInChannel(this.props.channelId);
return {
channel: ChannelStore.get(this.props.channelId),
channel,
memberChannel: ChannelStore.getMyMember(this.props.channelId),
users,
userCount: stats.member_count,
currentUser: UserStore.getCurrentUser(),
enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
isBusy: WebrtcStore.isBusy()
isBusy: WebrtcStore.isBusy(),
isFavorite: channel && ChannelUtils.isFavoriteChannel(channel)
};
}
@@ -125,11 +128,17 @@ export default class ChannelHeader extends React.Component {
handleLeave() {
Client.leaveChannel(this.state.channel.id,
() => {
const channelId = this.state.channel.id;
AppDispatcher.handleViewAction({
type: ActionTypes.LEAVE_CHANNEL,
id: this.state.channel.id
id: channelId
});
if (this.state.isFavorite) {
ChannelActions.unmarkFavorite(channelId);
}
const townsquare = ChannelStore.getByName('town-square');
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
},
@@ -139,6 +148,16 @@ export default class ChannelHeader extends React.Component {
);
}
toggleFavorite = (e) => {
e.preventDefault();
if (this.state.isFavorite) {
ChannelActions.unmarkFavorite(this.state.channel.id);
} else {
ChannelActions.markFavorite(this.state.channel.id);
}
};
searchMentions(e) {
e.preventDefault();
const user = this.state.currentUser;
@@ -272,9 +291,9 @@ export default class ChannelHeader extends React.Component {
if (isDirect) {
const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
const contact = this.state.users[0];
if (contact) {
channelTitle = Utils.displayUsername(contact.id);
}
const teammateId = Utils.getUserIdFromChannelName(channel);
channelTitle = Utils.displayUsername(teammateId);
const webrtcEnabled = global.mm_config.EnableWebrtc === 'true' && global.mm_license.Webrtc === 'true' &&
global.mm_config.EnableDeveloper === 'true' && userMedia && Utils.isFeatureEnabled(PreReleaseFeatures.WEBRTC_PREVIEW);
@@ -607,6 +626,35 @@ export default class ChannelHeader extends React.Component {
headerText = channel.header;
}
const toggleFavoriteTooltip = (
<Tooltip id='favoriteTooltip'>
{this.state.isFavorite ?
<FormattedMessage
id='channelHeader.removeFromFavorites'
defaultMessage='Remove from Favorites'
/> :
<FormattedMessage
id='channelHeader.addToFavorites'
defaultMessage='Add to Favorites'
/>}
</Tooltip>
);
const toggleFavorite = (
<OverlayTrigger
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='bottom'
overlay={toggleFavoriteTooltip}
>
<a
href='#'
onClick={this.toggleFavorite}
className='channel-header__favorites'
>
<i className={'icon fa ' + (this.state.isFavorite ? 'fa-star' : 'fa-star-o')}/>
</a>
</OverlayTrigger>
);
return (
<div
id='channel-header'
@@ -618,6 +666,7 @@ export default class ChannelHeader extends React.Component {
<th>
<div className='channel-header__info'>
{webrtc}
{toggleFavorite}
<div className='dropdown'>
<a
href='#'

Просмотреть файл

@@ -18,15 +18,19 @@ import StatusIcon from './status_icon.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import ChannelSwitchModal from './channel_switch_modal.jsx';
import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import {FormattedMessage} from 'react-intl';
@@ -66,12 +70,15 @@ export default class Navbar extends React.Component {
}
getStateFromStores() {
const channel = ChannelStore.getCurrent();
return {
channel: ChannelStore.getCurrent(),
channel,
member: ChannelStore.getCurrentMember(),
users: [],
userCount: ChannelStore.getCurrentStats().member_count,
currentUser: UserStore.getCurrentUser()
currentUser: UserStore.getCurrentUser(),
isFavorite: channel && ChannelUtils.isFavoriteChannel(channel)
};
}
@@ -83,6 +90,7 @@ export default class Navbar extends React.Component {
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addStatsChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
PreferenceStore.addChangeListener(this.onChange);
$('.inner-wrap').click(this.hideSidebars);
document.addEventListener('keydown', this.showChannelSwitchModal);
}
@@ -91,6 +99,7 @@ export default class Navbar extends React.Component {
ChannelStore.removeChangeListener(this.onChange);
ChannelStore.removeStatsChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
PreferenceStore.removeChangeListener(this.onChange);
document.removeEventListener('keydown', this.showChannelSwitchModal);
}
@@ -99,10 +108,17 @@ export default class Navbar extends React.Component {
}
handleLeave() {
Client.leaveChannel(this.state.channel.id,
var channelId = this.state.channel.id;
Client.leaveChannel(channelId,
() => {
AsyncClient.getChannels(true);
browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square');
if (this.state.isFavorite) {
ChannelActions.unmarkFavorite(channelId);
}
const townsquare = ChannelStore.getByName('town-square');
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
},
(err) => {
AsyncClient.dispatchError(err, 'handleLeave');
@@ -214,6 +230,16 @@ export default class Navbar extends React.Component {
return true;
}
toggleFavorite = (e) => {
e.preventDefault();
if (this.state.isFavorite) {
ChannelActions.unmarkFavorite(this.state.channel.id);
} else {
ChannelActions.markFavorite(this.state.channel.id);
}
};
createDropdown(channel, channelTitle, isAdmin, isSystemAdmin, isDirect, popoverContent) {
if (channel) {
let channelTerm = (
@@ -425,6 +451,29 @@ export default class Navbar extends React.Component {
}
}
const toggleFavoriteOption = (
<li
key='toggle_favorite'
role='presentation'
>
<a
role='menuitem'
href='#'
onClick={this.toggleFavorite}
>
{this.state.isFavorite ?
<FormattedMessage
id='channelHeader.removeFromFavorites'
defaultMessage='Remove from Favorites'
/> :
<FormattedMessage
id='channelHeader.addToFavorites'
defaultMessage='Add to Favorites'
/>}
</a>
</li>
);
return (
<div className='navbar-brand'>
<div className='dropdown'>
@@ -461,6 +510,7 @@ export default class Navbar extends React.Component {
{renameChannelOption}
{deleteChannelOption}
{leaveChannelOption}
{toggleFavoriteOption}
</ul>
</div>
</div>

Просмотреть файл

@@ -14,10 +14,10 @@ import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import LocalizationStore from 'stores/localization_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
@@ -96,63 +96,13 @@ export default class Sidebar extends React.Component {
getStateFromStores() {
const members = ChannelStore.getMyMembers();
const currentChannelId = ChannelStore.getCurrentId();
const currentUserId = UserStore.getCurrentId();
const channels = Object.assign([], ChannelStore.getAll());
channels.sort(this.sortChannelsByDisplayName);
const publicChannels = channels.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
const privateChannels = channels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
const directChannels = [];
const directNonTeamChannels = [];
for (const [name, value] of preferences) {
if (value !== 'true') {
continue;
}
const teammateId = name;
let directChannel = channels.find(Utils.isDirectChannelForUser.bind(null, teammateId));
// a direct channel doesn't exist yet so create a fake one
if (directChannel == null) {
directChannel = {
name: Utils.getDirectChannelName(currentUserId, teammateId),
last_post_at: 0,
total_msg_count: 0,
type: Constants.DM_CHANNEL,
fake: true
};
} else {
directChannel = JSON.parse(JSON.stringify(directChannel));
}
directChannel.display_name = Utils.displayUsername(teammateId);
directChannel.teammate_id = teammateId;
directChannel.status = UserStore.getStatus(teammateId) || 'offline';
if (TeamStore.hasActiveMemberInTeam(TeamStore.getCurrentId(), teammateId)) {
directChannels.push(directChannel);
} else if (TeamStore.hasMemberNotInTeam(TeamStore.getCurrentId(), teammateId)) {
directNonTeamChannels.push(directChannel);
}
}
directChannels.sort(this.sortChannelsByDisplayName);
directNonTeamChannels.sort(this.sortChannelsByDisplayName);
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
const channelList = ChannelUtils.buildDisplayableChannelList(Object.assign([], ChannelStore.getAll()));
return {
activeId: currentChannelId,
members,
publicChannels,
privateChannels,
directChannels,
directNonTeamChannels,
...channelList,
unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER,
currentTeam: TeamStore.getCurrent(),
@@ -379,6 +329,10 @@ export default class Sidebar extends React.Component {
}
);
if (ChannelUtils.isFavoriteChannel(channel)) {
ChannelActions.unmarkFavorite(channel.id);
}
this.setState(this.getStateFromStores());
}
@@ -387,16 +341,6 @@ export default class Sidebar extends React.Component {
}
}
sortChannelsByDisplayName(a, b) {
const locale = LocalizationStore.getLocale();
if (a.display_name === b.display_name) {
return a.name.localeCompare(b.name, locale, {numeric: true});
}
return a.display_name.localeCompare(b.display_name, locale, {numeric: true});
}
showMoreChannelsModal() {
// manually show the modal because using data-toggle messes with keyboard focus when the modal is dismissed
$('#more_channels').modal({'data-channeltype': 'O'}).modal('show');
@@ -522,7 +466,7 @@ export default class Sidebar extends React.Component {
badge = <span className='badge pull-right small'>{unreadCount.mentions}</span>;
this.badgesActive = true;
}
} else if (this.state.loadingDMChannel === index && channel.type === 'D') {
} else if (this.state.loadingDMChannel === index && channel.type === Constants.DM_CHANNEL) {
badge = (
<img
className='channel-loading-gif pull-right'
@@ -536,9 +480,9 @@ export default class Sidebar extends React.Component {
}
var icon = null;
if (channel.type === 'O') {
if (channel.type === Constants.OPEN_CHANNEL) {
icon = <div className='status'><i className='fa fa-globe'/></div>;
} else if (channel.type === 'P') {
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
icon = <div className='status'><i className='fa fa-lock'/></div>;
} else {
// set up status icon for direct message channels (status is null for other channel types)
@@ -618,7 +562,15 @@ export default class Sidebar extends React.Component {
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;
// create elements for all 3 types of channels
// create elements for all 4 types of channels
const favoriteItems = this.state.favoriteChannels.map((channel, index, arr) => {
if (channel.type === Constants.DM_CHANNEL) {
return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
}
return this.createChannelElement(channel);
});
const publicChannelItems = this.state.publicChannels.map(this.createChannelElement);
const privateChannelItems = this.state.privateChannels.map(this.createChannelElement);
@@ -801,6 +753,17 @@ export default class Sidebar extends React.Component {
className='nav-pills__container'
onScroll={this.onScroll}
>
{favoriteItems.length !== 0 && <ul className='nav nav-pills nav-stacked'>
<li>
<h4>
<FormattedMessage
id='sidebar.favorite'
defaultMessage='Favorites'
/>
</h4>
</li>
{favoriteItems}
</ul>}
<ul className='nav nav-pills nav-stacked'>
<li>
<h4>