GH-4095 Favorite/Starred Channels (#4222)
Этот коммит содержится в:
коммит произвёл
Christopher Speller
родитель
66ed155a58
Коммит
b354d25d37
@@ -172,3 +172,17 @@ export function openDirectChannelToUser(user, success, error) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function markFavorite(channelId) {
|
||||||
|
AsyncClient.savePreference(Preferences.CATEGORY_FAVORITE_CHANNEL, channelId, 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unmarkFavorite(channelId) {
|
||||||
|
const pref = {
|
||||||
|
user_id: UserStore.getCurrentId(),
|
||||||
|
category: Preferences.CATEGORY_FAVORITE_CHANNEL,
|
||||||
|
name: channelId
|
||||||
|
};
|
||||||
|
|
||||||
|
AsyncClient.deletePreferences([pref]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ import WebrtcStore from 'stores/webrtc_store.jsx';
|
|||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||||
import * as WebrtcActions from 'actions/webrtc_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 Utils from 'utils/utils.jsx';
|
||||||
|
import * as ChannelUtils from 'utils/channel_utils.jsx';
|
||||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||||
import Client from 'client/web_client.jsx';
|
import Client from 'client/web_client.jsx';
|
||||||
import * as AsyncClient from 'utils/async_client.jsx';
|
import * as AsyncClient from 'utils/async_client.jsx';
|
||||||
@@ -63,18 +65,19 @@ export default class ChannelHeader extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStateFromStores() {
|
getStateFromStores() {
|
||||||
|
const channel = ChannelStore.get(this.props.channelId);
|
||||||
const stats = ChannelStore.getStats(this.props.channelId);
|
const stats = ChannelStore.getStats(this.props.channelId);
|
||||||
|
|
||||||
const users = UserStore.getProfileListInChannel(this.props.channelId);
|
const users = UserStore.getProfileListInChannel(this.props.channelId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
channel: ChannelStore.get(this.props.channelId),
|
channel,
|
||||||
memberChannel: ChannelStore.getMyMember(this.props.channelId),
|
memberChannel: ChannelStore.getMyMember(this.props.channelId),
|
||||||
users,
|
users,
|
||||||
userCount: stats.member_count,
|
userCount: stats.member_count,
|
||||||
currentUser: UserStore.getCurrentUser(),
|
currentUser: UserStore.getCurrentUser(),
|
||||||
enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
|
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() {
|
handleLeave() {
|
||||||
Client.leaveChannel(this.state.channel.id,
|
Client.leaveChannel(this.state.channel.id,
|
||||||
() => {
|
() => {
|
||||||
|
const channelId = this.state.channel.id;
|
||||||
|
|
||||||
AppDispatcher.handleViewAction({
|
AppDispatcher.handleViewAction({
|
||||||
type: ActionTypes.LEAVE_CHANNEL,
|
type: ActionTypes.LEAVE_CHANNEL,
|
||||||
id: this.state.channel.id
|
id: channelId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.state.isFavorite) {
|
||||||
|
ChannelActions.unmarkFavorite(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
const townsquare = ChannelStore.getByName('town-square');
|
const townsquare = ChannelStore.getByName('town-square');
|
||||||
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
|
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) {
|
searchMentions(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const user = this.state.currentUser;
|
const user = this.state.currentUser;
|
||||||
@@ -272,9 +291,9 @@ export default class ChannelHeader extends React.Component {
|
|||||||
if (isDirect) {
|
if (isDirect) {
|
||||||
const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
||||||
const contact = this.state.users[0];
|
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' &&
|
const webrtcEnabled = global.mm_config.EnableWebrtc === 'true' && global.mm_license.Webrtc === 'true' &&
|
||||||
global.mm_config.EnableDeveloper === 'true' && userMedia && Utils.isFeatureEnabled(PreReleaseFeatures.WEBRTC_PREVIEW);
|
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;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
id='channel-header'
|
id='channel-header'
|
||||||
@@ -618,6 +666,7 @@ export default class ChannelHeader extends React.Component {
|
|||||||
<th>
|
<th>
|
||||||
<div className='channel-header__info'>
|
<div className='channel-header__info'>
|
||||||
{webrtc}
|
{webrtc}
|
||||||
|
{toggleFavorite}
|
||||||
<div className='dropdown'>
|
<div className='dropdown'>
|
||||||
<a
|
<a
|
||||||
href='#'
|
href='#'
|
||||||
|
|||||||
@@ -18,15 +18,19 @@ import StatusIcon from './status_icon.jsx';
|
|||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
import ChannelStore from 'stores/channel_store.jsx';
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
|
import PreferenceStore from 'stores/preference_store.jsx';
|
||||||
|
|
||||||
import ChannelSwitchModal from './channel_switch_modal.jsx';
|
import ChannelSwitchModal from './channel_switch_modal.jsx';
|
||||||
|
|
||||||
import Client from 'client/web_client.jsx';
|
import Client from 'client/web_client.jsx';
|
||||||
import * as AsyncClient from 'utils/async_client.jsx';
|
import * as AsyncClient from 'utils/async_client.jsx';
|
||||||
import * as Utils from 'utils/utils.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';
|
import Constants from 'utils/constants.jsx';
|
||||||
const ActionTypes = Constants.ActionTypes;
|
const ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||||
|
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
@@ -66,12 +70,15 @@ export default class Navbar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStateFromStores() {
|
getStateFromStores() {
|
||||||
|
const channel = ChannelStore.getCurrent();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
channel: ChannelStore.getCurrent(),
|
channel,
|
||||||
member: ChannelStore.getCurrentMember(),
|
member: ChannelStore.getCurrentMember(),
|
||||||
users: [],
|
users: [],
|
||||||
userCount: ChannelStore.getCurrentStats().member_count,
|
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.addChangeListener(this.onChange);
|
||||||
ChannelStore.addStatsChangeListener(this.onChange);
|
ChannelStore.addStatsChangeListener(this.onChange);
|
||||||
UserStore.addStatusesChangeListener(this.onChange);
|
UserStore.addStatusesChangeListener(this.onChange);
|
||||||
|
PreferenceStore.addChangeListener(this.onChange);
|
||||||
$('.inner-wrap').click(this.hideSidebars);
|
$('.inner-wrap').click(this.hideSidebars);
|
||||||
document.addEventListener('keydown', this.showChannelSwitchModal);
|
document.addEventListener('keydown', this.showChannelSwitchModal);
|
||||||
}
|
}
|
||||||
@@ -91,6 +99,7 @@ export default class Navbar extends React.Component {
|
|||||||
ChannelStore.removeChangeListener(this.onChange);
|
ChannelStore.removeChangeListener(this.onChange);
|
||||||
ChannelStore.removeStatsChangeListener(this.onChange);
|
ChannelStore.removeStatsChangeListener(this.onChange);
|
||||||
UserStore.removeStatusesChangeListener(this.onChange);
|
UserStore.removeStatusesChangeListener(this.onChange);
|
||||||
|
PreferenceStore.removeChangeListener(this.onChange);
|
||||||
document.removeEventListener('keydown', this.showChannelSwitchModal);
|
document.removeEventListener('keydown', this.showChannelSwitchModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,10 +108,17 @@ export default class Navbar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleLeave() {
|
handleLeave() {
|
||||||
Client.leaveChannel(this.state.channel.id,
|
var channelId = this.state.channel.id;
|
||||||
|
|
||||||
|
Client.leaveChannel(channelId,
|
||||||
() => {
|
() => {
|
||||||
AsyncClient.getChannels(true);
|
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) => {
|
(err) => {
|
||||||
AsyncClient.dispatchError(err, 'handleLeave');
|
AsyncClient.dispatchError(err, 'handleLeave');
|
||||||
@@ -214,6 +230,16 @@ export default class Navbar extends React.Component {
|
|||||||
return true;
|
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) {
|
createDropdown(channel, channelTitle, isAdmin, isSystemAdmin, isDirect, popoverContent) {
|
||||||
if (channel) {
|
if (channel) {
|
||||||
let channelTerm = (
|
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 (
|
return (
|
||||||
<div className='navbar-brand'>
|
<div className='navbar-brand'>
|
||||||
<div className='dropdown'>
|
<div className='dropdown'>
|
||||||
@@ -461,6 +510,7 @@ export default class Navbar extends React.Component {
|
|||||||
{renameChannelOption}
|
{renameChannelOption}
|
||||||
{deleteChannelOption}
|
{deleteChannelOption}
|
||||||
{leaveChannelOption}
|
{leaveChannelOption}
|
||||||
|
{toggleFavoriteOption}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import ChannelStore from 'stores/channel_store.jsx';
|
|||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
import PreferenceStore from 'stores/preference_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 AsyncClient from 'utils/async_client.jsx';
|
||||||
import * as Utils from 'utils/utils.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 * as ChannelActions from 'actions/channel_actions.jsx';
|
||||||
import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
|
import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
|
||||||
|
|
||||||
@@ -96,63 +96,13 @@ export default class Sidebar extends React.Component {
|
|||||||
getStateFromStores() {
|
getStateFromStores() {
|
||||||
const members = ChannelStore.getMyMembers();
|
const members = ChannelStore.getMyMembers();
|
||||||
const currentChannelId = ChannelStore.getCurrentId();
|
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 tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
|
||||||
|
const channelList = ChannelUtils.buildDisplayableChannelList(Object.assign([], ChannelStore.getAll()));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeId: currentChannelId,
|
activeId: currentChannelId,
|
||||||
members,
|
members,
|
||||||
publicChannels,
|
...channelList,
|
||||||
privateChannels,
|
|
||||||
directChannels,
|
|
||||||
directNonTeamChannels,
|
|
||||||
unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
|
unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
|
||||||
showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER,
|
showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER,
|
||||||
currentTeam: TeamStore.getCurrent(),
|
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());
|
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() {
|
showMoreChannelsModal() {
|
||||||
// manually show the modal because using data-toggle messes with keyboard focus when the modal is dismissed
|
// 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');
|
$('#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>;
|
badge = <span className='badge pull-right small'>{unreadCount.mentions}</span>;
|
||||||
this.badgesActive = true;
|
this.badgesActive = true;
|
||||||
}
|
}
|
||||||
} else if (this.state.loadingDMChannel === index && channel.type === 'D') {
|
} else if (this.state.loadingDMChannel === index && channel.type === Constants.DM_CHANNEL) {
|
||||||
badge = (
|
badge = (
|
||||||
<img
|
<img
|
||||||
className='channel-loading-gif pull-right'
|
className='channel-loading-gif pull-right'
|
||||||
@@ -536,9 +480,9 @@ export default class Sidebar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var icon = null;
|
var icon = null;
|
||||||
if (channel.type === 'O') {
|
if (channel.type === Constants.OPEN_CHANNEL) {
|
||||||
icon = <div className='status'><i className='fa fa-globe'/></div>;
|
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>;
|
icon = <div className='status'><i className='fa fa-lock'/></div>;
|
||||||
} else {
|
} else {
|
||||||
// set up status icon for direct message channels (status is null for other channel types)
|
// 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.firstUnreadChannel = null;
|
||||||
this.lastUnreadChannel = 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 publicChannelItems = this.state.publicChannels.map(this.createChannelElement);
|
||||||
|
|
||||||
const privateChannelItems = this.state.privateChannels.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'
|
className='nav-pills__container'
|
||||||
onScroll={this.onScroll}
|
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'>
|
<ul className='nav nav-pills nav-stacked'>
|
||||||
<li>
|
<li>
|
||||||
<h4>
|
<h4>
|
||||||
|
|||||||
@@ -986,6 +986,8 @@
|
|||||||
"channel_header.flagged": "Flagged Posts",
|
"channel_header.flagged": "Flagged Posts",
|
||||||
"channel_header.group": "Group",
|
"channel_header.group": "Group",
|
||||||
"channel_header.leave": "Leave {term}",
|
"channel_header.leave": "Leave {term}",
|
||||||
|
"channel_header.addToFavorites": "Add to Favorites",
|
||||||
|
"channel_header.removeFromFavorites": "Remove from Favorites",
|
||||||
"channel_header.manageMembers": "Manage Members",
|
"channel_header.manageMembers": "Manage Members",
|
||||||
"channel_header.notificationPreferences": "Notification Preferences",
|
"channel_header.notificationPreferences": "Notification Preferences",
|
||||||
"channel_header.recentMentions": "Recent Mentions",
|
"channel_header.recentMentions": "Recent Mentions",
|
||||||
|
|||||||
@@ -378,6 +378,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channel-header__favorites {
|
||||||
|
float: left;
|
||||||
|
margin: 1px 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.app__body {
|
.app__body {
|
||||||
|
|
||||||
.channel-header__links {
|
.channel-header__links {
|
||||||
|
|||||||
135
webapp/utils/channel_utils.jsx
Обычный файл
135
webapp/utils/channel_utils.jsx
Обычный файл
@@ -0,0 +1,135 @@
|
|||||||
|
|
||||||
|
import Constants from 'utils/constants.jsx';
|
||||||
|
const Preferences = Constants.Preferences;
|
||||||
|
|
||||||
|
import * as Utils from 'utils/utils.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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of sorted channels grouped by type. Favorites here is considered as separated type.
|
||||||
|
*
|
||||||
|
* Example: {
|
||||||
|
* publicChannels: [...],
|
||||||
|
* privateChannels: [...],
|
||||||
|
* directChannels: [...],
|
||||||
|
* directNonTeamChannels: [...],
|
||||||
|
* favoriteChannels: [...]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function buildDisplayableChannelList(persistentChannels) {
|
||||||
|
const missingDMChannels = createMissingDirectChannels(persistentChannels);
|
||||||
|
|
||||||
|
const channels = persistentChannels.concat(missingDMChannels).map(completeDirectChannelInfo);
|
||||||
|
channels.sort(sortChannelsByDisplayName);
|
||||||
|
|
||||||
|
const favoriteChannels = channels.filter(isFavoriteChannel);
|
||||||
|
const notFavoriteChannels = channels.filter(not(isFavoriteChannel));
|
||||||
|
const directChannels = notFavoriteChannels.filter(andX(isDirectChannel, isDirectChannelVisible));
|
||||||
|
|
||||||
|
return {
|
||||||
|
favoriteChannels,
|
||||||
|
publicChannels: notFavoriteChannels.filter(isOpenChannel),
|
||||||
|
privateChannels: notFavoriteChannels.filter(isPrivateChannel),
|
||||||
|
directChannels: directChannels.filter(isConnectedToTeamMember),
|
||||||
|
directNonTeamChannels: directChannels.filter(not(isConnectedToTeamMember))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFavoriteChannel(channel) {
|
||||||
|
return PreferenceStore.getBool(Preferences.CATEGORY_FAVORITE_CHANNEL, channel.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOpenChannel(channel) {
|
||||||
|
return channel.type === Constants.OPEN_CHANNEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPrivateChannel(channel) {
|
||||||
|
return channel.type === Constants.PRIVATE_CHANNEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDirectChannel(channel) {
|
||||||
|
return channel.type === Constants.DM_CHANNEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDirectChannelVisible(channel) {
|
||||||
|
const channelId = Utils.getUserIdFromChannelName(channel);
|
||||||
|
|
||||||
|
return PreferenceStore.getBool(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeDirectChannelInfo(channel) {
|
||||||
|
if (!isDirectChannel(channel)) {
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dmChannelClone = JSON.parse(JSON.stringify(channel));
|
||||||
|
const teammateId = Utils.getUserIdFromChannelName(channel);
|
||||||
|
|
||||||
|
return Object.assign(dmChannelClone, {
|
||||||
|
display_name: Utils.displayUsername(teammateId),
|
||||||
|
teammate_id: teammateId,
|
||||||
|
status: UserStore.getStatus(teammateId) || 'offline'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortChannelsByDisplayName(a, b) {
|
||||||
|
const locale = LocalizationStore.getLocale();
|
||||||
|
|
||||||
|
return buildDisplayNameAndTypeComparable(a).localeCompare(buildDisplayNameAndTypeComparable(b), locale, {numeric: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* not exported helpers
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createMissingDirectChannels(channels) {
|
||||||
|
const directChannelsDisplayPreferences = PreferenceStore.getCategory(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
|
||||||
|
|
||||||
|
return Array.
|
||||||
|
from(directChannelsDisplayPreferences).
|
||||||
|
filter((entry) => entry[1] === 'true').
|
||||||
|
map((entry) => entry[0]).
|
||||||
|
filter((teammateId) => !channels.some(Utils.isDirectChannelForUser.bind(null, teammateId))).
|
||||||
|
map(createFakeChannelCurried(UserStore.getCurrentId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFakeChannel(userId, otherUserId) {
|
||||||
|
return {
|
||||||
|
name: Utils.getDirectChannelName(userId, otherUserId),
|
||||||
|
last_post_at: 0,
|
||||||
|
total_msg_count: 0,
|
||||||
|
type: Constants.DM_CHANNEL,
|
||||||
|
fake: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFakeChannelCurried(userId) {
|
||||||
|
return (otherUserId) => createFakeChannel(userId, otherUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isConnectedToTeamMember(channel) {
|
||||||
|
return isTeamMember(channel.teammate_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTeamMember(userId) {
|
||||||
|
return TeamStore.hasActiveMemberInTeam(TeamStore.getCurrentId(), userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function not(f) {
|
||||||
|
return (...args) => !f(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function andX(...fns) {
|
||||||
|
return (...args) => fns.every((f) => f(...args));
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPrefix = 'D'; // fallback for future types
|
||||||
|
const typeToPrefixMap = {[Constants.OPEN_CHANNEL]: 'A', [Constants.PRIVATE_CHANNEL]: 'B', [Constants.DM_CHANNEL]: 'C'};
|
||||||
|
|
||||||
|
function buildDisplayNameAndTypeComparable(channel) {
|
||||||
|
return (typeToPrefixMap[channel.type] || defaultPrefix) + channel.display_name + channel.name;
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ export const Preferences = {
|
|||||||
CATEGORY_THEME: 'theme',
|
CATEGORY_THEME: 'theme',
|
||||||
CATEGORY_FLAGGED_POST: 'flagged_post',
|
CATEGORY_FLAGGED_POST: 'flagged_post',
|
||||||
CATEGORY_NOTIFICATIONS: 'notifications',
|
CATEGORY_NOTIFICATIONS: 'notifications',
|
||||||
|
CATEGORY_FAVORITE_CHANNEL: 'favorite_channel',
|
||||||
EMAIL_INTERVAL: 'email_interval'
|
EMAIL_INTERVAL: 'email_interval'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user