summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorenahum <nahumhbl@gmail.com>2016-12-19 10:05:46 -0300
committerJoram Wilander <jwawilander@gmail.com>2016-12-19 08:05:46 -0500
commit999d1553e1ce45adf58f6082b160bc1147dc592b (patch)
tree369a9b7f46dd44d136a79a050469429169433cec /webapp
parent3ce2ce9dc882ed962dc3ce7550bdb07963f376b6 (diff)
downloadchat-999d1553e1ce45adf58f6082b160bc1147dc592b.tar.gz
chat-999d1553e1ce45adf58f6082b160bc1147dc592b.tar.bz2
chat-999d1553e1ce45adf58f6082b160bc1147dc592b.zip
PLT-4167 Team Sidebar (#4569)
* PLT-4167 Team Sidebar * Address feedback from PM * change route from my_members to members * bug fixes * Updating styles for teams sidebar (#4681) * Added PM changes * Fix corner cases * Addressing feedback * use two different endpoints * Bug fixes * Rename model and client functions, using preferences to store last team and channel viewed * Fix mobile notification count and closing the team sidebar * unit test, fixed bad merge and retrieve from cached when available * bug fixes * use id for last channel in preferences, query optimization * Updating multi team css (#4830)
Diffstat (limited to 'webapp')
-rw-r--r--webapp/actions/global_actions.jsx38
-rw-r--r--webapp/actions/post_actions.jsx11
-rw-r--r--webapp/actions/websocket_actions.jsx25
-rw-r--r--webapp/client/client.jsx24
-rw-r--r--webapp/components/admin_console/admin_navbar_dropdown.jsx35
-rw-r--r--webapp/components/create_team/create_team_controller.jsx14
-rw-r--r--webapp/components/login/login_controller.jsx4
-rw-r--r--webapp/components/navbar.jsx2
-rw-r--r--webapp/components/needs_team.jsx2
-rw-r--r--webapp/components/notify_counts.jsx18
-rw-r--r--webapp/components/root.jsx2
-rw-r--r--webapp/components/select_team/components/select_team_listing.jsx0
-rw-r--r--webapp/components/select_team/select_team.jsx108
-rw-r--r--webapp/components/sidebar.jsx14
-rw-r--r--webapp/components/sidebar_header_dropdown.jsx87
-rw-r--r--webapp/components/sidebar_right.jsx1
-rw-r--r--webapp/components/sidebar_right_menu.jsx9
-rw-r--r--webapp/components/signup/components/signup_email.jsx4
-rw-r--r--webapp/components/signup/components/signup_ldap.jsx7
-rw-r--r--webapp/components/signup/signup_controller.jsx2
-rw-r--r--webapp/components/team_sidebar/components/team_button.jsx84
-rw-r--r--webapp/components/team_sidebar/team_sidebar_controller.jsx169
-rw-r--r--webapp/components/webrtc/components/webrtc_sidebar.jsx1
-rw-r--r--webapp/i18n/en.json5
-rw-r--r--webapp/routes/route_team.jsx31
-rw-r--r--webapp/sass/layout/_module.scss1
-rw-r--r--webapp/sass/layout/_team-sidebar.scss90
-rw-r--r--webapp/sass/responsive/_mobile.scss41
-rw-r--r--webapp/stores/channel_store.jsx5
-rw-r--r--webapp/stores/notification_store.jsx5
-rw-r--r--webapp/stores/preference_store.jsx5
-rw-r--r--webapp/stores/team_store.jsx77
-rw-r--r--webapp/tests/client_team.test.jsx14
-rw-r--r--webapp/utils/async_client.jsx74
-rw-r--r--webapp/utils/constants.jsx2
-rw-r--r--webapp/utils/utils.jsx12
36 files changed, 846 insertions, 177 deletions
diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx
index d743b787b..9d135dd26 100644
--- a/webapp/actions/global_actions.jsx
+++ b/webapp/actions/global_actions.jsx
@@ -43,6 +43,7 @@ export function emitChannelClickEvent(channel) {
);
}
function switchToChannel(chan) {
+ const channelMember = ChannelStore.getMyMember(chan.id);
const getMyChannelMembersPromise = AsyncClient.getChannelMember(chan.id, UserStore.getCurrentId());
getMyChannelMembersPromise.then(() => {
@@ -56,6 +57,9 @@ export function emitChannelClickEvent(channel) {
type: ActionTypes.CLICK_CHANNEL,
name: chan.name,
id: chan.id,
+ team_id: chan.team_id,
+ total_msg_count: chan.total_msg_count,
+ channelMember,
prev: ChannelStore.getCurrentId()
});
}
@@ -443,7 +447,7 @@ export function viewLoggedIn() {
PostStore.clearPendingPosts();
}
-var lastTimeTypingSent = 0;
+let lastTimeTypingSent = 0;
export function emitLocalUserTypingEvent(channelId, parentId) {
const t = Date.now();
if ((t - lastTimeTypingSent) > Constants.UPDATE_TYPING_MS) {
@@ -534,3 +538,35 @@ export function emitBrowserFocus(focus) {
focus
});
}
+
+export function redirectUserToDefaultTeam() {
+ const teams = TeamStore.getAll();
+ const teamMembers = TeamStore.getMyTeamMembers();
+ let teamId = PreferenceStore.get('last', 'team');
+
+ if (!teams[teamId] && teamMembers.length > 0) {
+ let myTeams = [];
+ for (const index in teamMembers) {
+ if (teamMembers.hasOwnProperty(index)) {
+ const teamMember = teamMembers[index];
+ myTeams.push(teams[teamMember.team_id]);
+ }
+ }
+
+ if (myTeams.length > 0) {
+ myTeams = myTeams.sort((a, b) => a.display_name.localeCompare(b.display_name));
+ teamId = myTeams[0].id;
+ }
+ }
+
+ if (teams[teamId]) {
+ const channelId = PreferenceStore.get(teamId, 'channel');
+ let channel = ChannelStore.getChannelById(channelId);
+ if (!channel) {
+ channel = 'town-square';
+ }
+ browserHistory.push(`/${teams[teamId].name}/channels/${channel}`);
+ } else {
+ browserHistory.push('/select_team');
+ }
+}
diff --git a/webapp/actions/post_actions.jsx b/webapp/actions/post_actions.jsx
index 736aef033..d1e69cda7 100644
--- a/webapp/actions/post_actions.jsx
+++ b/webapp/actions/post_actions.jsx
@@ -18,23 +18,30 @@ const ActionTypes = Constants.ActionTypes;
const Preferences = Constants.Preferences;
export function handleNewPost(post, msg) {
+ const teamId = TeamStore.getCurrentId();
+
if (ChannelStore.getCurrentId() === post.channel_id) {
if (window.isActive) {
AsyncClient.updateLastViewedAt(null, false);
} else {
AsyncClient.getChannel(post.channel_id);
}
- } else if (msg && (TeamStore.getCurrentId() === msg.data.team_id || msg.data.channel_type === Constants.DM_CHANNEL)) {
+ } else if (msg && (teamId === msg.data.team_id || msg.data.channel_type === Constants.DM_CHANNEL)) {
if (Client.teamId) {
AsyncClient.getChannel(post.channel_id);
}
}
- var websocketMessageProps = null;
+ let websocketMessageProps = null;
if (msg) {
websocketMessageProps = msg.data;
}
+ const myTeams = TeamStore.getMyTeamMembers();
+ if (msg.data.team_id !== teamId && myTeams.filter((m) => m.team_id === msg.data.team_id).length) {
+ AsyncClient.getMyTeamsUnread(teamId);
+ }
+
if (post.root_id && PostStore.getPost(post.channel_id, post.root_id) == null) {
Client.getPost(
post.channel_id,
diff --git a/webapp/actions/websocket_actions.jsx b/webapp/actions/websocket_actions.jsx
index ec433aab5..6c81a4ac9 100644
--- a/webapp/actions/websocket_actions.jsx
+++ b/webapp/actions/websocket_actions.jsx
@@ -6,6 +6,7 @@ import $ from 'jquery';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PostStore from 'stores/post_store.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import ErrorStore from 'stores/error_store.jsx';
@@ -122,6 +123,10 @@ function handleEvent(msg) {
handleLeaveTeamEvent(msg);
break;
+ case SocketEvents.UPDATE_TEAM:
+ handleUpdateTeamEvent(msg);
+ break;
+
case SocketEvents.USER_ADDED:
handleUserAddedEvent(msg);
break;
@@ -229,21 +234,37 @@ function handleNewUserEvent(msg) {
AsyncClient.getUser(msg.data.user_id);
AsyncClient.getChannelStats();
loadProfilesAndTeamMembersForDMSidebar();
+
+ if (msg.data.user_id === UserStore.getCurrentId()) {
+ AsyncClient.getMyTeamMembers();
+ }
}
function handleLeaveTeamEvent(msg) {
if (UserStore.getCurrentId() === msg.data.user_id) {
TeamStore.removeMyTeamMember(msg.data.team_id);
- // if they are on the team being removed redirect them to the root
+ // if they are on the team being removed redirect them to default team
if (TeamStore.getCurrentId() === msg.data.team_id) {
TeamStore.setCurrentId('');
Client.setTeamId('');
- browserHistory.push('/');
+ PreferenceStore.deletePreference({
+ category: 'last',
+ name: 'team',
+ value: msg.data.team_id
+ });
+ GlobalActions.redirectUserToDefaultTeam();
}
+ } else {
+ UserStore.removeProfileFromTeam(msg.data.team_id, msg.data.user_id);
+ TeamStore.removeMemberInTeam(msg.data.team_id, msg.data.user_id);
}
}
+function handleUpdateTeamEvent(msg) {
+ TeamStore.updateTeam(msg.data.team);
+}
+
function handleDirectAddedEvent(msg) {
AsyncClient.getChannel(msg.broadcast.channel_id);
loadProfilesAndTeamMembersForDMSidebar();
diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx
index 370d40ac0..3ec36644f 100644
--- a/webapp/client/client.jsx
+++ b/webapp/client/client.jsx
@@ -598,6 +598,30 @@ export default class Client {
end(this.handleResponse.bind(this, 'getTeamMember', success, error));
}
+ getMyTeamMembers(success, error) {
+ request.
+ get(`${this.getTeamsRoute()}/members`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ end(this.handleResponse.bind(this, 'getMyTeamMembers', success, error));
+ }
+
+ getMyTeamsUnread(teamId, success, error) {
+ let url = `${this.getTeamsRoute()}/unread`;
+
+ if (teamId) {
+ url += `?id=${encodeURIComponent(teamId)}`;
+ }
+
+ request.
+ get(url).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ end(this.handleResponse.bind(this, 'getMyTeamsUnread', success, error));
+ }
+
getTeamMembersByIds(teamId, userIds, success, error) {
request.
post(`${this.getTeamNeededRoute(teamId)}/members/ids`).
diff --git a/webapp/components/admin_console/admin_navbar_dropdown.jsx b/webapp/components/admin_console/admin_navbar_dropdown.jsx
index f20451b4b..5b65868e9 100644
--- a/webapp/components/admin_console/admin_navbar_dropdown.jsx
+++ b/webapp/components/admin_console/admin_navbar_dropdown.jsx
@@ -50,13 +50,14 @@ export default class AdminNavbarDropdown extends React.Component {
}
render() {
- var teams = [];
+ const teams = [];
+ let switchTeams;
if (this.state.teamMembers && this.state.teamMembers.length > 0) {
- for (var index in this.state.teamMembers) {
+ for (const index in this.state.teamMembers) {
if (this.state.teamMembers.hasOwnProperty(index)) {
- var teamMember = this.state.teamMembers[index];
- var team = this.state.teams[teamMember.team_id];
+ const teamMember = this.state.teamMembers[index];
+ const team = this.state.teams[teamMember.team_id];
teams.push(
<li key={'team_' + team.name}>
<Link
@@ -79,6 +80,20 @@ export default class AdminNavbarDropdown extends React.Component {
className='divider'
/>
);
+ } else {
+ switchTeams = (
+ <li>
+ <Link
+ to={'/select_team'}
+ >
+ <i className='fa fa-exchange'/>
+ <FormattedMessage
+ id='admin.nav.switch'
+ defaultMessage='Team Selection'
+ />
+ </Link>
+ </li>
+ );
}
return (
@@ -104,17 +119,7 @@ export default class AdminNavbarDropdown extends React.Component {
role='menu'
>
{teams}
- <li>
- <Link
- to={'/select_team'}
- >
- <i className='fa fa-exchange'/>
- <FormattedMessage
- id='admin.nav.switch'
- defaultMessage='Team Selection'
- />
- </Link>
- </li>
+ {switchTeams}
<li
key='teamDiv'
className='divider'
diff --git a/webapp/components/create_team/create_team_controller.jsx b/webapp/components/create_team/create_team_controller.jsx
index 77a7f659f..84b890e26 100644
--- a/webapp/components/create_team/create_team_controller.jsx
+++ b/webapp/components/create_team/create_team_controller.jsx
@@ -2,6 +2,8 @@
// See License.txt for license information.
import ErrorBar from 'components/error_bar.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
import {FormattedMessage} from 'react-intl';
import {browserHistory, Link} from 'react-router/es6';
@@ -47,11 +49,21 @@ export default class CreateTeamController extends React.Component {
);
}
+ let url = '/select_team';
+ const team = TeamStore.getCurrent();
+ const channel = ChannelStore.getCurrent();
+ if (team) {
+ url = `/${team.name}`;
+ if (channel) {
+ url += `/channels/${channel.name}`;
+ }
+ }
+
return (
<div>
<ErrorBar/>
<div className='signup-header'>
- <Link to='/select_team'>
+ <Link to={url}>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx
index 4ed6b67e8..726853cf2 100644
--- a/webapp/components/login/login_controller.jsx
+++ b/webapp/components/login/login_controller.jsx
@@ -54,7 +54,7 @@ export default class LoginController extends React.Component {
document.title = global.window.mm_config.SiteName;
if (UserStore.getCurrentUser()) {
- browserHistory.push('/select_team');
+ GlobalActions.redirectUserToDefaultTeam();
}
AsyncClient.checkVersion();
@@ -205,7 +205,7 @@ export default class LoginController extends React.Component {
} else if (team) {
browserHistory.push(`/${team.name}`);
} else {
- browserHistory.push('/select_team');
+ GlobalActions.redirectUserToDefaultTeam();
}
}
);
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index 0a5f04394..0a49d2497 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -146,6 +146,7 @@ export default class Navbar extends React.Component {
if (e.target.className !== 'navbar-toggle' && e.target.className !== 'icon-bar') {
$('.app__body .inner-wrap').removeClass('move--right move--left move--left-small');
$('.app__body .sidebar--left').removeClass('move--right');
+ $('.multi-teams .team-sidebar').removeClass('move--right');
$('.app__body .sidebar--right').removeClass('move--left');
$('.app__body .sidebar--menu').removeClass('move--left');
}
@@ -155,6 +156,7 @@ export default class Navbar extends React.Component {
toggleLeftSidebar() {
$('.app__body .inner-wrap').toggleClass('move--right');
$('.app__body .sidebar--left').toggleClass('move--right');
+ $('.multi-teams .team-sidebar').toggleClass('move--right');
}
toggleRightSidebar() {
diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx
index f8a774389..f90297065 100644
--- a/webapp/components/needs_team.jsx
+++ b/webapp/components/needs_team.jsx
@@ -141,6 +141,7 @@ export default class NeedsTeam extends React.Component {
content.push(
this.props.navbar
);
+ content.push(this.props.team_sidebar);
content.push(
this.props.sidebar
);
@@ -197,6 +198,7 @@ NeedsTeam.propTypes = {
]),
navbar: React.PropTypes.element,
sidebar: React.PropTypes.element,
+ team_sidebar: React.PropTypes.element,
center: React.PropTypes.element,
params: React.PropTypes.object,
user: React.PropTypes.object
diff --git a/webapp/components/notify_counts.jsx b/webapp/components/notify_counts.jsx
index 6ccbd228b..d49925780 100644
--- a/webapp/components/notify_counts.jsx
+++ b/webapp/components/notify_counts.jsx
@@ -3,14 +3,22 @@
import * as utils from 'utils/utils.jsx';
import ChannelStore from 'stores/channel_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
function getCountsStateFromStores() {
- var count = 0;
- var channels = ChannelStore.getAll();
- var members = ChannelStore.getMyMembers();
+ let count = 0;
+ const teamMembers = TeamStore.getMyTeamMembers();
+ const channels = ChannelStore.getAll();
+ const members = ChannelStore.getMyMembers();
+
+ teamMembers.forEach((member) => {
+ if (member.team_id !== TeamStore.getCurrentId()) {
+ count += ((member.msg_count || 0) + (member.mention_count || 0));
+ }
+ });
channels.forEach((channel) => {
- var channelMember = members[channel.id];
+ const channelMember = members[channel.id];
if (channelMember == null) {
return;
}
@@ -41,10 +49,12 @@ export default class NotifyCounts extends React.Component {
componentDidMount() {
this.mounted = true;
ChannelStore.addChangeListener(this.onListenerChange);
+ TeamStore.addChangeListener(this.onListenerChange);
}
componentWillUnmount() {
this.mounted = false;
ChannelStore.removeChangeListener(this.onListenerChange);
+ TeamStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
if (this.mounted) {
diff --git a/webapp/components/root.jsx b/webapp/components/root.jsx
index 9bb7d2970..2cfd9d303 100644
--- a/webapp/components/root.jsx
+++ b/webapp/components/root.jsx
@@ -50,7 +50,7 @@ export default class Root extends React.Component {
if (UserStore.getNoAccounts()) {
browserHistory.push('/signup_user_complete');
} else if (UserStore.getCurrentUser()) {
- browserHistory.push('/select_team');
+ GlobalActions.redirectUserToDefaultTeam();
} else {
browserHistory.push('/login');
}
diff --git a/webapp/components/select_team/components/select_team_listing.jsx b/webapp/components/select_team/components/select_team_listing.jsx
deleted file mode 100644
index e69de29bb..000000000
--- a/webapp/components/select_team/components/select_team_listing.jsx
+++ /dev/null
diff --git a/webapp/components/select_team/select_team.jsx b/webapp/components/select_team/select_team.jsx
index eef3b7931..361c95c49 100644
--- a/webapp/components/select_team/select_team.jsx
+++ b/webapp/components/select_team/select_team.jsx
@@ -24,6 +24,7 @@ export default class SelectTeam extends React.Component {
super(props);
this.onTeamChange = this.onTeamChange.bind(this);
this.handleTeamClick = this.handleTeamClick.bind(this);
+ this.teamContentsCompare = this.teamContentsCompare.bind(this);
const state = this.getStateFromStores(false);
state.loadingTeamId = '';
@@ -61,33 +62,15 @@ export default class SelectTeam extends React.Component {
}
render() {
- let content = null;
- let teamContents = [];
+ let openTeamContents = [];
const isAlreadyMember = new Map();
const isSystemAdmin = Utils.isSystemAdmin(UserStore.getCurrentUser().roles);
- let teamMembersCount = 0;
for (const teamMember of this.state.teamMembers) {
const teamId = teamMember.team_id;
- const team = this.state.teams[teamId];
isAlreadyMember[teamId] = true;
- teamMembersCount++;
-
- teamContents.push(
- <SelectTeamItem
- key={'team_' + team.name}
- team={team}
- url={'/' + team.name + '/channels/town-square'}
- onTeamClick={this.handleTeamClick}
- loading={this.state.loadingTeamId === teamId}
- />
- );
}
- teamContents = teamContents.sort(this.teamContentsCompare);
-
- var openTeamContents = [];
-
for (const id in this.state.teamListings) {
if (this.state.teamListings.hasOwnProperty(id) && !isAlreadyMember[id]) {
const openTeam = this.state.teamListings[id];
@@ -103,8 +86,8 @@ export default class SelectTeam extends React.Component {
}
}
- if (teamMembersCount === 0 && teamContents.length === 0 && openTeamContents.length === 0 && (global.window.mm_config.EnableTeamCreation === 'true' || isSystemAdmin)) {
- teamContents = (
+ if (openTeamContents.length === 0 && (global.window.mm_config.EnableTeamCreation === 'true' || isSystemAdmin)) {
+ openTeamContents = (
<div className='signup-team-dir-err'>
<div>
<FormattedMessage
@@ -114,8 +97,8 @@ export default class SelectTeam extends React.Component {
</div>
</div>
);
- } else if (teamMembersCount === 0 && teamContents.length === 0 && openTeamContents.length === 0) {
- teamContents = (
+ } else if (openTeamContents.length === 0) {
+ openTeamContents = (
<div className='signup-team-dir-err'>
<div>
<FormattedMessage
@@ -125,42 +108,25 @@ export default class SelectTeam extends React.Component {
</div>
</div>
);
- } else if (teamContents.length === 0 && openTeamContents.length > 0) {
- teamContents = null;
}
- if (teamContents) {
- content = (
- <div className='signup__content'>
- <h4>
- <FormattedMessage
- id='signup_team.choose'
- defaultMessage='Your teams:'
- />
- </h4>
- <div className='signup-team-all'>
- {teamContents}
- </div>
- </div>
- );
+ if (Array.isArray(openTeamContents)) {
+ openTeamContents = openTeamContents.sort(this.teamContentsCompare);
}
- var openContent;
- if (openTeamContents.length > 0) {
- openContent = (
- <div className='signup__content'>
- <h4>
- <FormattedMessage
- id='signup_team.join_open'
- defaultMessage='Teams you can join: '
- />
- </h4>
- <div className='signup-team-all'>
- {openTeamContents.sort(this.teamContentsCompare)}
- </div>
+ let openContent = (
+ <div className='signup__content'>
+ <h4>
+ <FormattedMessage
+ id='signup_team.join_open'
+ defaultMessage='Teams you can join: '
+ />
+ </h4>
+ <div className='signup-team-all'>
+ {openTeamContents}
</div>
- );
- }
+ </div>
+ );
if (!this.state.loaded) {
openContent = <LoadingScreen/>;
@@ -177,7 +143,7 @@ export default class SelectTeam extends React.Component {
}
let teamSignUp;
- if (isSystemAdmin || (global.window.mm_config.EnableTeamCreation === 'true' && !UserAgent.isMobileApp())) {
+ if (isSystemAdmin || global.window.mm_config.EnableTeamCreation === 'true') {
teamSignUp = (
<div className='margin--extra'>
<Link
@@ -197,7 +163,7 @@ export default class SelectTeam extends React.Component {
}
let adminConsoleLink;
- if (isSystemAdmin) {
+ if (isSystemAdmin && !UserAgent.isMobileApp()) {
adminConsoleLink = (
<div className='margin--extra hidden-xs'>
<Link
@@ -225,19 +191,30 @@ export default class SelectTeam extends React.Component {
);
}
+ let headerButton;
+ if (this.state.teamMembers.length) {
+ headerButton = (
+ <Link to='/'>
+ <span className='fa fa-chevron-left'/>
+ <FormattedMessage id='web.header.back'/>
+ </Link>
+ );
+ } else {
+ headerButton = (
+ <a
+ href='#'
+ onClick={GlobalActions.emitUserLoggedOutEvent}
+ >
+ <span className='fa fa-chevron-left'/>
+ <FormattedMessage id='web.header.logout'/>
+ </a>
+ );
+ }
return (
<div>
<ErrorBar/>
<div className='signup-header'>
- <a
- href='#'
- onClick={GlobalActions.emitUserLoggedOutEvent}
- >
- <span className='fa fa-chevron-left'/>
- <FormattedMessage
- id='navbar_dropdown.logout'
- />
- </a>
+ {headerButton}
</div>
<div className='col-sm-12'>
<div className={'signup-team__container'}>
@@ -249,7 +226,6 @@ export default class SelectTeam extends React.Component {
<h4 className='color--light'>
{description}
</h4>
- {content}
{openContent}
{teamSignUp}
{adminConsoleLink}
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index fcfa9496c..203464600 100644
--- a/webapp/components/sidebar.jsx
+++ b/webapp/components/sidebar.jsx
@@ -82,11 +82,19 @@ export default class Sidebar extends React.Component {
let msgs = 0;
let mentions = 0;
const unreadCounts = this.state.unreadCounts;
+ const teamMembers = this.state.teamMembers;
+
+ teamMembers.forEach((member) => {
+ if (member.team_id !== this.state.currentTeam.id) {
+ msgs += member.msg_count || 0;
+ mentions += member.mention_count || 0;
+ }
+ });
Object.keys(unreadCounts).forEach((chId) => {
const channel = ChannelStore.get(chId);
- if (channel && (!channel.team_id || channel.team_id === this.state.currentTeam.id)) {
+ if (channel && (channel.type === 'D' || channel.team_id === this.state.currentTeam.id)) {
msgs += unreadCounts[chId].msgs;
mentions += unreadCounts[chId].mentions;
}
@@ -97,6 +105,7 @@ export default class Sidebar extends React.Component {
getStateFromStores() {
const members = ChannelStore.getMyMembers();
+ const teamMembers = TeamStore.getMyTeamMembers();
const currentChannelId = ChannelStore.getCurrentId();
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
const channelList = ChannelUtils.buildDisplayableChannelList(Object.assign([], ChannelStore.getAll()));
@@ -104,6 +113,7 @@ export default class Sidebar extends React.Component {
return {
activeId: currentChannelId,
members,
+ teamMembers,
...channelList,
unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER,
@@ -153,6 +163,7 @@ export default class Sidebar extends React.Component {
if (this.state.activeId !== prevState.activeId) {
$('.app__body .inner-wrap').removeClass('move--right');
$('.app__body .sidebar--left').removeClass('move--right');
+ $('.multi-teams .team-sidebar').removeClass('move--right');
}
}
@@ -169,6 +180,7 @@ export default class Sidebar extends React.Component {
onChange() {
this.setState(this.getStateFromStores());
+ this.updateTitle();
}
updateTitle() {
diff --git a/webapp/components/sidebar_header_dropdown.jsx b/webapp/components/sidebar_header_dropdown.jsx
index dcd791cc6..cfa4d2f80 100644
--- a/webapp/components/sidebar_header_dropdown.jsx
+++ b/webapp/components/sidebar_header_dropdown.jsx
@@ -45,7 +45,6 @@ export default class SidebarHeaderDropdown extends React.Component {
this.showGetTeamInviteLinkModal = this.showGetTeamInviteLinkModal.bind(this);
this.showTeamMembersModal = this.showTeamMembersModal.bind(this);
this.hideTeamMembersModal = this.hideTeamMembersModal.bind(this);
- this.handleSwitchTeams = this.handleSwitchTeams.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
this.openAccountSettings = this.openAccountSettings.bind(this);
@@ -55,8 +54,8 @@ export default class SidebarHeaderDropdown extends React.Component {
this.handleClick = this.handleClick.bind(this);
this.state = {
- teams: TeamStore.getAll(),
teamMembers: TeamStore.getMyTeamMembers(),
+ teamListings: TeamStore.getTeamListings(),
showAboutModal: false,
showDropdown: false,
showTeamMembersModal: false,
@@ -131,11 +130,6 @@ export default class SidebarHeaderDropdown extends React.Component {
});
}
- handleSwitchTeams() {
- // The actual switching of teams is handled by the react-router Link
- this.setState({showDropdown: false});
- }
-
componentDidMount() {
TeamStore.addChangeListener(this.onTeamChange);
document.addEventListener('keydown', this.openAccountSettings);
@@ -143,8 +137,8 @@ export default class SidebarHeaderDropdown extends React.Component {
onTeamChange() {
this.setState({
- teams: TeamStore.getAll(),
- teamMembers: TeamStore.getMyTeamMembers()
+ teamMembers: TeamStore.getMyTeamMembers(),
+ teamListings: TeamStore.getTeamListings()
});
}
@@ -182,14 +176,14 @@ export default class SidebarHeaderDropdown extends React.Component {
render() {
const config = global.mm_config;
- var teamLink = '';
- var inviteLink = '';
- var manageLink = '';
- var sysAdminLink = '';
- var currentUser = this.props.currentUser;
- var isAdmin = false;
- var isSystemAdmin = false;
- var teamSettings = null;
+ const currentUser = this.props.currentUser;
+ let teamLink = '';
+ let inviteLink = '';
+ let manageLink = '';
+ let sysAdminLink = '';
+ let isAdmin = false;
+ let isSystemAdmin = false;
+ let teamSettings = null;
let integrationsLink = null;
if (!currentUser) {
@@ -322,6 +316,7 @@ export default class SidebarHeaderDropdown extends React.Component {
}
const teams = [];
+ let moreTeams = false;
if (config.EnableTeamCreation === 'true') {
teams.push(
@@ -340,6 +335,31 @@ export default class SidebarHeaderDropdown extends React.Component {
);
}
+ const isAlreadyMember = this.state.teamMembers.reduce((result, item) => {
+ result[item.team_id] = true;
+ return result;
+ }, {});
+
+ for (const id in this.state.teamListings) {
+ if (this.state.teamListings.hasOwnProperty(id) && !isAlreadyMember[id]) {
+ moreTeams = true;
+ break;
+ }
+ }
+
+ if (moreTeams) {
+ teams.push(
+ <li key='joinTeam_li'>
+ <Link to='/select_team'>
+ <FormattedMessage
+ id='navbar_dropdown.join'
+ defaultMessage='Join Another Team'
+ />
+ </Link>
+ </li>
+ );
+ }
+
teams.push(
<li key='leaveTeam_li'>
<a
@@ -354,39 +374,6 @@ export default class SidebarHeaderDropdown extends React.Component {
</li>
);
- if (this.state.teamMembers && this.state.teamMembers.length > 1) {
- teams.push(
- <li
- key='teamDiv'
- className='divider'
- />
- );
-
- for (var index in this.state.teamMembers) {
- if (this.state.teamMembers.hasOwnProperty(index)) {
- var teamMember = this.state.teamMembers[index];
- var team = this.state.teams[teamMember.team_id];
-
- if (team.name !== this.props.teamName) {
- teams.push(
- <li key={'team_' + team.name}>
- <Link
- to={'/' + team.name + '/channels/town-square'}
- onClick={this.handleSwitchTeams}
- >
- <FormattedMessage
- id='navbar_dropdown.switchTo'
- defaultMessage='Switch to '
- />
- {team.display_name}
- </Link>
- </li>
- );
- }
- }
- }
- }
-
let helpLink = null;
if (config.HelpLink) {
helpLink = (
diff --git a/webapp/components/sidebar_right.jsx b/webapp/components/sidebar_right.jsx
index 1f4c394bb..1be2a3287 100644
--- a/webapp/components/sidebar_right.jsx
+++ b/webapp/components/sidebar_right.jsx
@@ -82,6 +82,7 @@ export default class SidebarRight extends React.Component {
$('.app__body .inner-wrap').removeClass('.move--right');
$('.app__body .inner-wrap').addClass('move--left');
$('.app__body .sidebar--left').removeClass('move--right');
+ $('.multi-teams .team-sidebar').removeClass('move--right');
$('.app__body .sidebar--right').addClass('move--left');
//$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
diff --git a/webapp/components/sidebar_right_menu.jsx b/webapp/components/sidebar_right_menu.jsx
index f201adfcf..982562c2c 100644
--- a/webapp/components/sidebar_right_menu.jsx
+++ b/webapp/components/sidebar_right_menu.jsx
@@ -391,15 +391,6 @@ export default class SidebarRightMenu extends React.Component {
{teamSettingsLink}
{manageLink}
{consoleLink}
- <li>
- <Link to='/select_team'>
- <i className='icon fa fa-exchange'/>
- <FormattedMessage
- id='sidebar_right_menu.switch_team'
- defaultMessage='Team Selection'
- />
- </Link>
- </li>
<li className='divider'/>
{helpLink}
{reportLink}
diff --git a/webapp/components/signup/components/signup_email.jsx b/webapp/components/signup/components/signup_email.jsx
index b67179604..aa3493c96 100644
--- a/webapp/components/signup/components/signup_email.jsx
+++ b/webapp/components/signup/components/signup_email.jsx
@@ -110,7 +110,7 @@ export default class SignupEmail extends React.Component {
if (query.redirect_to) {
browserHistory.push(query.redirect_to);
} else {
- browserHistory.push('/select_team');
+ GlobalActions.redirectUserToDefaultTeam();
}
}
);
@@ -133,7 +133,7 @@ export default class SignupEmail extends React.Component {
if (query.redirect_to) {
browserHistory.push(query.redirect_to);
} else {
- browserHistory.push('/select_team');
+ GlobalActions.redirectUserToDefaultTeam();
}
}
);
diff --git a/webapp/components/signup/components/signup_ldap.jsx b/webapp/components/signup/components/signup_ldap.jsx
index bc8c073ad..d80b27159 100644
--- a/webapp/components/signup/components/signup_ldap.jsx
+++ b/webapp/components/signup/components/signup_ldap.jsx
@@ -90,8 +90,13 @@ export default class SignupLdap extends React.Component {
finishSignup() {
GlobalActions.emitInitialLoad(
() => {
+ const query = this.props.location.query;
GlobalActions.loadDefaultLocale();
- browserHistory.push('/select_team');
+ if (query.redirect_to) {
+ browserHistory.push(query.redirect_to);
+ } else {
+ GlobalActions.redirectUserToDefaultTeam();
+ }
}
);
}
diff --git a/webapp/components/signup/signup_controller.jsx b/webapp/components/signup/signup_controller.jsx
index 0f0b6c867..3d853a9ee 100644
--- a/webapp/components/signup/signup_controller.jsx
+++ b/webapp/components/signup/signup_controller.jsx
@@ -122,7 +122,7 @@ export default class SignupController extends React.Component {
}
if (userLoggedIn) {
- browserHistory.push('/select_team');
+ GlobalActions.redirectUserToDefaultTeam();
}
}
}
diff --git a/webapp/components/team_sidebar/components/team_button.jsx b/webapp/components/team_sidebar/components/team_button.jsx
new file mode 100644
index 000000000..0033ae25a
--- /dev/null
+++ b/webapp/components/team_sidebar/components/team_button.jsx
@@ -0,0 +1,84 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Constants from 'utils/constants.jsx';
+
+import React from 'react';
+import {Link} from 'react-router/es6';
+import {Tooltip, OverlayTrigger} from 'react-bootstrap';
+
+export default class TeamButton extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleDisabled = this.handleDisabled.bind(this);
+ }
+
+ handleDisabled(e) {
+ e.preventDefault();
+ }
+
+ render() {
+ let teamClass = this.props.active ? 'active' : '';
+ const disabled = this.props.disabled ? 'team-disabled' : '';
+ const handleClick = (this.props.active || this.props.disabled) ? this.handleDisabled : null;
+ let badge;
+
+ if (!teamClass) {
+ teamClass = this.props.unread ? 'unread' : '';
+
+ if (this.props.mentions) {
+ badge = (
+ <span className='badge pull-right small'>{this.props.mentions}</span>
+ );
+ }
+ }
+
+ return (
+ <div
+ className={`team-container ${teamClass}`}
+ >
+ <Link
+ className={disabled}
+ to={this.props.url}
+ onClick={handleClick}
+ >
+ <OverlayTrigger
+ delayShow={Constants.OVERLAY_TIME_DELAY}
+ placement={this.props.placement}
+ overlay={
+ <Tooltip id={`tooltip-${this.props.url}`}>
+ {this.props.tip}
+ </Tooltip>
+ }
+ >
+ <div className='team-btn'>
+ {badge}
+ {this.props.contents}
+ </div>
+ </OverlayTrigger>
+ </Link>
+ </div>
+ );
+ }
+}
+
+TeamButton.defaultProps = {
+ tip: '',
+ placement: 'right',
+ active: false,
+ disabled: false,
+ unread: false,
+ mentions: 0
+};
+
+TeamButton.propTypes = {
+ url: React.PropTypes.string.isRequired,
+ contents: React.PropTypes.node.isRequired,
+ tip: React.PropTypes.node,
+ active: React.PropTypes.bool,
+ disabled: React.PropTypes.bool,
+ unread: React.PropTypes.bool,
+ mentions: React.PropTypes.number,
+ placement: React.PropTypes.oneOf(['left', 'right', 'top', 'bottom'])
+};
diff --git a/webapp/components/team_sidebar/team_sidebar_controller.jsx b/webapp/components/team_sidebar/team_sidebar_controller.jsx
new file mode 100644
index 000000000..f005afeb3
--- /dev/null
+++ b/webapp/components/team_sidebar/team_sidebar_controller.jsx
@@ -0,0 +1,169 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import TeamButton from './components/team_button.jsx';
+
+import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import $ from 'jquery';
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+export default class TeamSidebar extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getStateFromStores = this.getStateFromStores.bind(this);
+ this.onChange = this.onChange.bind(this);
+ this.handleResize = this.handleResize.bind(this);
+ this.setStyles = this.setStyles.bind(this);
+
+ this.state = this.getStateFromStores();
+ }
+
+ getStateFromStores() {
+ const teamMembers = TeamStore.getMyTeamMembers();
+ const currentTeamId = TeamStore.getCurrentId();
+
+ return {
+ teams: TeamStore.getAll(),
+ teamListings: TeamStore.getTeamListings(),
+ teamMembers,
+ currentTeamId,
+ show: teamMembers && teamMembers.length > 1
+ };
+ }
+
+ componentDidMount() {
+ window.addEventListener('resize', this.handleResize);
+ TeamStore.addChangeListener(this.onChange);
+ TeamStore.addUnreadChangeListener(this.onChange);
+ AsyncClient.getAllTeamListings();
+ this.setStyles();
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleResize);
+ TeamStore.removeChangeListener(this.onChange);
+ TeamStore.removeUnreadChangeListener(this.onChange);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ $('.team-wrapper').perfectScrollbar();
+
+ // reset the scrollbar upon switching teams
+ if (this.state.currentTeam !== prevState.currentTeam) {
+ this.refs.container.scrollTop = 0;
+ $('.team-wrapper').perfectScrollbar('update');
+ }
+ }
+
+ onChange() {
+ this.setState(this.getStateFromStores());
+ this.setStyles();
+ }
+
+ handleResize() {
+ const teamMembers = this.state.teamMembers;
+ this.setState({show: teamMembers && teamMembers.length > 1});
+ this.setStyles();
+ }
+
+ setStyles() {
+ const root = document.querySelector('#root');
+
+ if (this.state.show) {
+ root.classList.add('multi-teams');
+ } else {
+ root.classList.remove('multi-teams');
+ }
+ }
+
+ render() {
+ if (!this.state.show) {
+ return null;
+ }
+
+ const myTeams = [];
+ const isSystemAdmin = Utils.isSystemAdmin(UserStore.getCurrentUser().roles);
+ const isAlreadyMember = new Map();
+ let moreTeams = false;
+
+ for (const index in this.state.teamMembers) {
+ if (this.state.teamMembers.hasOwnProperty(index)) {
+ const teamMember = this.state.teamMembers[index];
+ const teamId = teamMember.team_id;
+ myTeams.push(Object.assign({
+ unread: teamMember.msg_count > 0,
+ mentions: teamMember.mention_count
+ }, this.state.teams[teamId]));
+ isAlreadyMember[teamId] = true;
+ }
+ }
+
+ for (const id in this.state.teamListings) {
+ if (this.state.teamListings.hasOwnProperty(id) && !isAlreadyMember[id]) {
+ moreTeams = true;
+ break;
+ }
+ }
+
+ const teams = myTeams.
+ sort((a, b) => a.display_name.localeCompare(b.display_name)).
+ map((team) => {
+ return (
+ <TeamButton
+ key={'switch_team_' + team.name}
+ url={`/${team.name}`}
+ tip={team.display_name}
+ active={team.id === this.state.currentTeamId}
+ contents={team.display_name.substring(0, 1).toUpperCase()}
+ unread={team.unread}
+ mentions={team.mentions}
+ />
+ );
+ });
+
+ if (moreTeams) {
+ teams.push(
+ <TeamButton
+ key='more_teams'
+ url='/select_team'
+ tip={
+ <FormattedMessage
+ id='team_sidebar.join'
+ defaultMessage='Other teams you can join.'
+ />
+ }
+ contents={<i className='fa fa-plus'/>}
+ />
+ );
+ } else if (global.window.mm_config.EnableTeamCreation === 'true' || isSystemAdmin) {
+ teams.push(
+ <TeamButton
+ key='more_teams'
+ url='/create_team'
+ tip={
+ <FormattedMessage
+ id='navbar_dropdown.create'
+ defaultMessage='Create a New Team'
+ />
+ }
+ contents={<i className='fa fa-plus'/>}
+ />
+ );
+ }
+
+ return (
+ <div className='team-sidebar'>
+ <div className='team-wrapper'>
+ {teams}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/webrtc/components/webrtc_sidebar.jsx b/webapp/components/webrtc/components/webrtc_sidebar.jsx
index d28158b09..2979e6025 100644
--- a/webapp/components/webrtc/components/webrtc_sidebar.jsx
+++ b/webapp/components/webrtc/components/webrtc_sidebar.jsx
@@ -50,6 +50,7 @@ export default class SidebarRight extends React.Component {
$('.app__body .inner-wrap').removeClass('move--right');
$('.app__body .inner-wrap').addClass('webrtc--show');
$('.app__body .sidebar--left').removeClass('move--right');
+ $('.multi-teams .team-sidebar').removeClass('move--right');
$('.app__body .webrtc').addClass('webrtc--show');
//$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 556987ee4..10460e2b8 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -1602,6 +1602,7 @@
"navbar_dropdown.help": "Help",
"navbar_dropdown.integrations": "Integrations",
"navbar_dropdown.inviteMember": "Invite New Member",
+ "navbar_dropdown.join": "Join Another Team",
"navbar_dropdown.leave": "Leave Team",
"navbar_dropdown.logout": "Logout",
"navbar_dropdown.manageMembers": "Manage Members",
@@ -1750,7 +1751,6 @@
"sidebar_right_menu.nativeApps": "Download Apps",
"sidebar_right_menu.recentMentions": "Recent Mentions",
"sidebar_right_menu.report": "Report a Problem",
- "sidebar_right_menu.switch_team": "Team Selection",
"sidebar_right_menu.teamLink": "Get Team Invite Link",
"sidebar_right_menu.teamSettings": "Team Settings",
"sidebar_right_menu.viewMembers": "View Members",
@@ -1760,7 +1760,6 @@
"signup.ldap": "AD/LDAP Credentials",
"signup.office365": "Office 365",
"signup.title": "Create an account with:",
- "signup_team.choose": "Your teams: ",
"signup_team.createTeam": "Or Create a Team",
"signup_team.disabled": "Team creation has been disabled. Please contact an administrator for access.",
"signup_team.join_open": "Teams you can join: ",
@@ -1853,6 +1852,7 @@
"team_settings_modal.generalTab": "General",
"team_settings_modal.importTab": "Import",
"team_settings_modal.title": "Team Settings",
+ "team_sidebar.join": "Other teams you can join.",
"textbox.bold": "**bold**",
"textbox.edit": "Edit message",
"textbox.help": "Help",
@@ -2170,6 +2170,7 @@
"web.footer.privacy": "Privacy",
"web.footer.terms": "Terms",
"web.header.back": "Back",
+ "web.header.logout": "Logout",
"web.root.signup_info": "All team communication in one place, searchable and accessible anywhere",
"webrtc.busy": "{username} is busy.",
"webrtc.call": "Call",
diff --git a/webapp/routes/route_team.jsx b/webapp/routes/route_team.jsx
index 88fab8d45..10bbcc4df 100644
--- a/webapp/routes/route_team.jsx
+++ b/webapp/routes/route_team.jsx
@@ -14,6 +14,7 @@ const ActionTypes = Constants.ActionTypes;
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import ChannelStore from 'stores/channel_store.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
import emojiRoute from 'routes/route_emoji.jsx';
import integrationsRoute from 'routes/route_integrations.jsx';
@@ -63,7 +64,7 @@ function preNeedsTeam(nextState, replace, callback) {
// First check to make sure you're in the current team
// for the current url.
const teamName = nextState.params.team;
- var team = TeamStore.getByName(teamName);
+ const team = TeamStore.getByName(teamName);
if (!team) {
browserHistory.push('/');
@@ -75,9 +76,10 @@ function preNeedsTeam(nextState, replace, callback) {
TeamStore.saveMyTeam(team);
TeamStore.emitChange();
loadProfilesAndTeamMembersForDMSidebar();
+ AsyncClient.getMyTeamsUnread();
AsyncClient.getMyChannelMembers();
- var d1 = $.Deferred(); //eslint-disable-line new-cap
+ const d1 = $.Deferred(); //eslint-disable-line new-cap
Client.getChannels(
(data) => {
@@ -101,6 +103,20 @@ function preNeedsTeam(nextState, replace, callback) {
});
}
+function selectLastChannel(nextState, replace, callback) {
+ const team = TeamStore.getByName(nextState.params.team);
+ const channelId = PreferenceStore.get(team.id, 'channel');
+ const channel = ChannelStore.getChannelById(channelId);
+
+ let channelName = 'town-square';
+ if (channel) {
+ channelName = channel.name;
+ }
+
+ replace(`/${team.name}/channels/${channelName}`);
+ callback();
+}
+
function onPermalinkEnter(nextState, replace, callback) {
const postId = nextState.params.postid;
GlobalActions.emitPostFocusEvent(
@@ -112,7 +128,7 @@ function onPermalinkEnter(nextState, replace, callback) {
export default {
path: ':team',
onEnter: preNeedsTeam,
- indexRoute: {onEnter: (nextState, replace) => replace('/' + nextState.params.team + '/channels/town-square')},
+ indexRoute: {onEnter: selectLastChannel},
childRoutes: [
integrationsRoute,
emojiRoute,
@@ -126,10 +142,11 @@ export default {
onEnter: onChannelEnter,
getComponents: (location, callback) => {
Promise.all([
+ System.import('components/team_sidebar/team_sidebar_controller.jsx'),
System.import('components/sidebar.jsx'),
System.import('components/channel_view.jsx')
]).then(
- (comarr) => callback(null, {sidebar: comarr[0].default, center: comarr[1].default})
+ (comarr) => callback(null, {team_sidebar: comarr[0].default, sidebar: comarr[1].default, center: comarr[2].default})
);
}
},
@@ -138,10 +155,11 @@ export default {
onEnter: onPermalinkEnter,
getComponents: (location, callback) => {
Promise.all([
+ System.import('components/team_sidebar/team_sidebar_controller.jsx'),
System.import('components/sidebar.jsx'),
System.import('components/permalink_view.jsx')
]).then(
- (comarr) => callback(null, {sidebar: comarr[0].default, center: comarr[1].default})
+ (comarr) => callback(null, {team_sidebar: comarr[0].default, sidebar: comarr[1].default, center: comarr[2].default})
);
}
},
@@ -149,10 +167,11 @@ export default {
path: 'tutorial',
getComponents: (location, callback) => {
Promise.all([
+ System.import('components/team_sidebar/team_sidebar_controller.jsx'),
System.import('components/sidebar.jsx'),
System.import('components/tutorial/tutorial_view.jsx')
]).then(
- (comarr) => callback(null, {sidebar: comarr[0].default, center: comarr[1].default})
+ (comarr) => callback(null, {team_sidebar: comarr[0].default, sidebar: comarr[1].default, center: comarr[2].default})
);
}
}
diff --git a/webapp/sass/layout/_module.scss b/webapp/sass/layout/_module.scss
index 39ab2c6f8..bdaa12acb 100644
--- a/webapp/sass/layout/_module.scss
+++ b/webapp/sass/layout/_module.scss
@@ -10,4 +10,5 @@
@import 'sidebar-left';
@import 'sidebar-menu';
@import 'sidebar-right';
+@import 'team-sidebar';
@import 'webhooks';
diff --git a/webapp/sass/layout/_team-sidebar.scss b/webapp/sass/layout/_team-sidebar.scss
new file mode 100644
index 000000000..dd80a4d12
--- /dev/null
+++ b/webapp/sass/layout/_team-sidebar.scss
@@ -0,0 +1,90 @@
+.multi-teams {
+ .team-sidebar {
+ height: 100%;
+ left: 0;
+ position: fixed;
+ text-align: center;
+ width: 75px;
+ z-index: 5;
+
+ .team-wrapper {
+ background-color: rgba(0, 0, 0, 0.2);
+ height: 100%;
+ overflow: auto;
+ padding-top: 15px;
+
+ .team-container {
+ width: 100%;
+ text-align: center;
+ display: table;
+ margin-bottom: 10px;
+ position: relative;
+
+ .team-btn {
+ border: none;
+ height: 42px;
+ text-align: center;
+ font-weight: bold;
+ border-radius: 5px;
+ position: relative;
+ width: 42px;
+ background-color: rgba(0, 0, 0, 0.3);
+ display: table-cell;
+ vertical-align: middle;
+
+ .badge {
+ position: absolute;
+ top: -3px;
+ right: -6px;
+ font-size: 10px;
+ }
+ }
+
+ &.active {
+ .team-btn {
+ border: 1px solid #fff;
+ background-color: transparent;
+ }
+ }
+
+ &.active:before {
+ background: black;
+ content: '';
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 5px;
+ }
+
+ &.unread:before {
+ background: black;
+ border-radius: 50%;
+ content: '';
+ height: 5px;
+ left: 0;
+ position: absolute;
+ top: 18px;
+ width: 5px;
+ }
+
+ a.team-disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+ }
+ }
+
+ .team-container a:hover {
+ text-decoration: none;
+ }
+ }
+ }
+
+ .sidebar--left {
+ left: 75px;
+ }
+
+ .app__content {
+ margin-left: 295px;
+ }
+}
diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss
index 66f9550d7..ab305a5b0 100644
--- a/webapp/sass/responsive/_mobile.scss
+++ b/webapp/sass/responsive/_mobile.scss
@@ -1163,6 +1163,33 @@
.post-comments {
padding: 9px 21px 10px 10px !important;
}
+
+ .multi-teams {
+ .team-sidebar {
+ width: 75px;
+ }
+
+ .app__content {
+ margin-left: 0;
+ }
+
+ .sidebar--left {
+ left: 0;
+
+ &.move--right {
+ left: 75px;
+ }
+ }
+
+ .team-sidebar {
+ display: none;
+
+ &.move--right {
+ display: block;
+ @include translate3d(0, 0, 0);
+ }
+ }
+ }
}
@media screen and (max-width: 640px) {
@@ -1493,6 +1520,20 @@
}
@media screen and (max-width: 320px) {
+ .multi-teams {
+ .team-sidebar {
+ width: 65px;
+ }
+
+ .sidebar--left {
+ width: 220px;
+
+ &.move--right {
+ left: 65px;
+ }
+ }
+ }
+
.post {
.post__header {
.col__name {
diff --git a/webapp/stores/channel_store.jsx b/webapp/stores/channel_store.jsx
index af7238267..0264ada4a 100644
--- a/webapp/stores/channel_store.jsx
+++ b/webapp/stores/channel_store.jsx
@@ -215,6 +215,10 @@ class ChannelStoreClass extends EventEmitter {
return this.channels;
}
+ getChannelById(id) {
+ return this.channels.filter((c) => c.id === id)[0];
+ }
+
storeMyChannelMember(channelMember) {
const members = Object.assign({}, this.getMyMembers());
members[channelMember.channel_id] = channelMember;
@@ -348,7 +352,6 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
switch (action.type) {
case ActionTypes.CLICK_CHANNEL:
ChannelStore.setCurrentId(action.id);
- ChannelStore.resetCounts(action.id);
ChannelStore.setPostMode(ChannelStore.POST_MODE_CHANNEL);
ChannelStore.emitChange();
break;
diff --git a/webapp/stores/notification_store.jsx b/webapp/stores/notification_store.jsx
index 9d0e6c790..878ac3c9d 100644
--- a/webapp/stores/notification_store.jsx
+++ b/webapp/stores/notification_store.jsx
@@ -45,7 +45,7 @@ class NotificationStoreClass extends EventEmitter {
}
const teamId = msgProps.team_id;
- const channel = ChannelStore.get(post.channel_id);
+ let channel = ChannelStore.get(post.channel_id);
const user = UserStore.getCurrentUser();
const member = ChannelStore.getMyMember(post.channel_id);
@@ -72,6 +72,9 @@ class NotificationStoreClass extends EventEmitter {
let title = Utils.localizeMessage('channel_loader.posted', 'Posted');
if (!channel) {
title = msgProps.channel_display_name;
+ channel = {
+ name: msgProps.channel_name
+ };
} else if (channel.type === Constants.DM_CHANNEL) {
title = Utils.localizeMessage('notification.dm', 'Direct Message');
} else {
diff --git a/webapp/stores/preference_store.jsx b/webapp/stores/preference_store.jsx
index 8aecfff40..3197ac7e9 100644
--- a/webapp/stores/preference_store.jsx
+++ b/webapp/stores/preference_store.jsx
@@ -140,9 +140,12 @@ class PreferenceStore extends EventEmitter {
}
this.emitChange();
break;
+ case ActionTypes.CLICK_CHANNEL:
+ this.setPreference(action.team_id, 'channel', action.id);
+ break;
}
}
}
global.PreferenceStore = new PreferenceStore();
-export default global.PreferenceStore; \ No newline at end of file
+export default global.PreferenceStore;
diff --git a/webapp/stores/team_store.jsx b/webapp/stores/team_store.jsx
index 7775a4a3a..bb0926ec5 100644
--- a/webapp/stores/team_store.jsx
+++ b/webapp/stores/team_store.jsx
@@ -10,6 +10,7 @@ const ActionTypes = Constants.ActionTypes;
const CHANGE_EVENT = 'change';
const STATS_EVENT = 'stats';
+const UNREAD_EVENT = 'unread';
var Utils;
@@ -53,17 +54,31 @@ class TeamStoreClass extends EventEmitter {
this.removeListener(STATS_EVENT, callback);
}
+ emitUnreadChange() {
+ this.emit(UNREAD_EVENT);
+ }
+
+ addUnreadChangeListener(callback) {
+ this.on(UNREAD_EVENT, callback);
+ }
+
+ removeUnreadChangeListener(callback) {
+ this.removeListener(UNREAD_EVENT, callback);
+ }
+
get(id) {
var c = this.getAll();
return c[id];
}
getByName(name) {
- var t = this.getAll();
+ const t = this.getAll();
- for (var id in t) {
- if (t[id].name === name) {
- return t[id];
+ for (const id in t) {
+ if (t.hasOwnProperty(id)) {
+ if (t[id].name === name) {
+ return t[id];
+ }
}
}
@@ -158,6 +173,25 @@ class TeamStoreClass extends EventEmitter {
this.teams = teams;
}
+ updateTeam(team) {
+ const t = JSON.parse(team);
+ if (this.teams && this.teams[t.id]) {
+ this.teams[t.id] = t;
+ }
+
+ if (this.teamListings && this.teamListings[t.id]) {
+ if (t.allow_open_invite) {
+ this.teamListings[t.id] = t;
+ } else {
+ Reflect.deleteProperty(this.teamListings, t.id);
+ }
+ } else if (t.allow_open_invite) {
+ this.teamListings[t.id] = t;
+ }
+
+ this.emitChange();
+ }
+
saveMyTeam(team) {
this.saveTeam(team);
this.currentTeamId = team.id;
@@ -175,12 +209,29 @@ class TeamStoreClass extends EventEmitter {
this.my_team_members.push(member);
}
+ saveMyTeamMembersUnread(members) {
+ for (let i = 0; i < this.my_team_members.length; i++) {
+ const team = this.my_team_members[i];
+ const member = members.filter((m) => m.team_id === team.team_id)[0];
+
+ if (member) {
+ this.my_team_members[i] = Object.assign({},
+ team,
+ {
+ msg_count: member.msg_count,
+ mention_count: member.mention_count
+ });
+ }
+ }
+ }
+
removeMyTeamMember(teamId) {
for (let i = 0; i < this.my_team_members.length; i++) {
if (this.my_team_members[i].team_id === teamId) {
this.my_team_members.splice(i, 1);
}
}
+ this.emitChange();
}
getMyTeamMembers() {
@@ -248,6 +299,14 @@ class TeamStoreClass extends EventEmitter {
return false;
}
+
+ updateUnreadCount(teamId, totalMsgCount, channelMember) {
+ const member = this.my_team_members.filter((m) => m.team_id === teamId)[0];
+ if (member) {
+ member.msg_count -= (totalMsgCount - channelMember.msg_count);
+ member.mention_count -= channelMember.mention_count;
+ }
+ }
}
var TeamStore = new TeamStoreClass();
@@ -277,6 +336,10 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => {
TeamStore.saveMyTeamMembers(action.team_members);
TeamStore.emitChange();
break;
+ case ActionTypes.RECEIVED_MY_TEAMS_UNREAD:
+ TeamStore.saveMyTeamMembersUnread(action.team_members);
+ TeamStore.emitChange();
+ break;
case ActionTypes.RECEIVED_ALL_TEAM_LISTINGS:
TeamStore.saveTeamListings(action.teams);
TeamStore.emitChange();
@@ -292,6 +355,12 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => {
TeamStore.saveStats(action.team_id, action.stats);
TeamStore.emitStatsChange();
break;
+ case ActionTypes.CLICK_CHANNEL:
+ if (action.channelMember) {
+ TeamStore.updateUnreadCount(action.team_id, action.total_msg_count, action.channelMember);
+ TeamStore.emitUnreadChange();
+ }
+ break;
default:
}
});
diff --git a/webapp/tests/client_team.test.jsx b/webapp/tests/client_team.test.jsx
index 009d084f8..61449fb15 100644
--- a/webapp/tests/client_team.test.jsx
+++ b/webapp/tests/client_team.test.jsx
@@ -130,6 +130,20 @@ describe('Client.Team', function() {
});
});
+ it('getMyTeamMembers', function(done) {
+ TestHelper.initBasic(() => {
+ TestHelper.basicClient().getMyTeamMembers(
+ function(data) {
+ assert.equal(data.length > 0, true);
+ done();
+ },
+ function(err) {
+ done(new Error(err.message));
+ }
+ );
+ });
+ });
+
it('getTeamMembers', function(done) {
TestHelper.initBasic(() => {
TestHelper.basicClient().getTeamMembers(
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index 9bcb4e07f..2d8e76fc2 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -166,12 +166,21 @@ export function updateLastViewedAt(id, active) {
channelId,
isActive,
() => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_PREFERENCE,
+ preference: {
+ category: 'last',
+ name: TeamStore.getCurrentId(),
+ value: channelId
+ }
+ });
+
callTracker[`updateLastViewed${channelId}`] = 0;
ErrorStore.clearLastError();
},
(err) => {
callTracker[`updateLastViewed${channelId}`] = 0;
- var count = ErrorStore.getConnectionErrorCount();
+ const count = ErrorStore.getConnectionErrorCount();
ErrorStore.setConnectionErrorCount(count + 1);
dispatchError(err, 'updateLastViewedAt');
}
@@ -203,6 +212,14 @@ export function setLastViewedAt(lastViewedAt, id) {
channelId,
lastViewedAt,
() => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_PREFERENCE,
+ preference: {
+ category: 'last',
+ name: TeamStore.getCurrentId(),
+ value: channelId
+ }
+ });
callTracker[`setLastViewedAt${channelId}${lastViewedAt}`] = 0;
ErrorStore.clearLastError();
},
@@ -847,6 +864,61 @@ export function getTeamMember(teamId, userId) {
);
}
+export function getMyTeamMembers() {
+ const callName = 'getMyTeamMembers';
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+ Client.getMyTeamMembers(
+ (data) => {
+ callTracker[callName] = 0;
+
+ const members = {};
+ for (const member of data) {
+ members[member.team_id] = member;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_MY_TEAM_MEMBERS_UNREAD,
+ team_members: members
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+ dispatchError(err, 'getMyTeamMembers');
+ }
+ );
+}
+
+export function getMyTeamsUnread(teamId) {
+ const members = TeamStore.getMyTeamMembers();
+ if (members.length > 1) {
+ const callName = 'getMyTeamsUnread';
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+ Client.getMyTeamsUnread(
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_MY_TEAMS_UNREAD,
+ team_members: data
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+ dispatchError(err, 'getMyTeamsUnread');
+ }
+ );
+ }
+}
+
export function getTeamStats(teamId) {
const callName = `getTeamStats${teamId}`;
if (isCallInProgress(callName)) {
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index 40d3482d6..133b824ff 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -139,6 +139,7 @@ export const ActionTypes = keyMirror({
RECEIVED_ALL_TEAMS: null,
RECEIVED_ALL_TEAM_LISTINGS: null,
RECEIVED_MY_TEAM_MEMBERS: null,
+ RECEIVED_MY_TEAMS_UNREAD: null,
RECEIVED_MEMBERS_IN_TEAM: null,
RECEIVED_TEAM_STATS: null,
@@ -202,6 +203,7 @@ export const SocketEvents = {
DIRECT_ADDED: 'direct_added',
NEW_USER: 'new_user',
LEAVE_TEAM: 'leave_team',
+ UPDATE_TEAM: 'update_team',
USER_ADDED: 'user_added',
USER_REMOVED: 'user_removed',
USER_UPDATED: 'user_updated',
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index 8d899b5ca..a96c791c7 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -104,7 +104,9 @@ export function notifyMe(title, body, channel, teamId, duration, silent) {
var notification = new Notification(title, {body, tag: body, icon: icon50, requireInteraction: notificationDuration === 0, silent});
notification.onclick = () => {
window.focus();
- if (channel) {
+ if (channel && channel.type === Constants.DM_CHANNEL) {
+ browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name);
+ } else if (channel) {
browserHistory.push(TeamStore.getTeamUrl(teamId) + '/channels/' + channel.name);
} else if (teamId) {
browserHistory.push(TeamStore.getTeamUrl(teamId) + '/channels/town-square');
@@ -491,6 +493,8 @@ export function applyTheme(theme) {
changeCss('.sidebar--left .nav li.active a:before, .app__body .modal .settings-modal .nav-pills>li.active a:before', 'background:' + theme.sidebarTextActiveBorder);
changeCss('.sidebar--left .sidebar__divider:before', 'background:' + changeOpacity(theme.sidebarTextActiveBorder, 0.5));
changeCss('.sidebar--left .sidebar__divider', 'color:' + theme.sidebarTextActiveBorder);
+ changeCss('.multi-teams .team-sidebar .team-wrapper .team-container.active:before', 'background:' + theme.sidebarTextActiveBorder);
+ changeCss('.multi-teams .team-sidebar .team-wrapper .team-container.unread:before', 'background:' + theme.sidebarTextActiveBorder);
}
if (theme.sidebarTextActiveColor) {
@@ -501,13 +505,13 @@ export function applyTheme(theme) {
if (theme.sidebarHeaderBg) {
changeCss('.sidebar--left .team__header, .app__body .sidebar--menu .team__header, .app__body .post-list__timestamp > div', 'background:' + theme.sidebarHeaderBg);
changeCss('.app__body .modal .modal-header', 'background:' + theme.sidebarHeaderBg);
- changeCss('.app__body #navbar .navbar-default', 'background:' + theme.sidebarHeaderBg);
+ changeCss('.app__body .multi-teams .team-sidebar, .app__body #navbar .navbar-default', 'background:' + theme.sidebarHeaderBg);
changeCss('@media(max-width: 768px){.app__body .search-bar__container', 'background:' + theme.sidebarHeaderBg);
changeCss('.app__body .attachment .attachment__container', 'border-left-color:' + theme.sidebarHeaderBg);
}
if (theme.sidebarHeaderTextColor) {
- changeCss('.sidebar--left .team__header .header__info, .app__body .sidebar--menu .team__header .header__info, .app__body .post-list__timestamp > div', 'color:' + theme.sidebarHeaderTextColor);
+ changeCss('.multi-teams .team-sidebar .team-wrapper .team-container .team-btn, .sidebar--left .team__header .header__info, .app__body .sidebar--menu .team__header .header__info, .app__body .post-list__timestamp > div', 'color:' + theme.sidebarHeaderTextColor);
changeCss('.app__body .sidebar-header-dropdown__icon', 'fill:' + theme.sidebarHeaderTextColor);
changeCss('.sidebar--left .team__header .user__name, .app__body .sidebar--menu .team__header .user__name', 'color:' + changeOpacity(theme.sidebarHeaderTextColor, 0.8));
changeCss('.sidebar--left .team__header:hover .user__name, .app__body .sidebar--menu .team__header:hover .user__name', 'color:' + theme.sidebarHeaderTextColor);
@@ -534,11 +538,13 @@ export function applyTheme(theme) {
if (theme.mentionBj) {
changeCss('.sidebar--left .nav-pills__unread-indicator, .app__body .new-messages__button div', 'background:' + theme.mentionBj);
changeCss('.sidebar--left .badge', 'background:' + theme.mentionBj + '!important;');
+ changeCss('.multi-teams .team-sidebar .team-wrapper .team-container .team-btn .badge', 'background:' + theme.mentionBj + '!important;');
}
if (theme.mentionColor) {
changeCss('.sidebar--left .nav-pills__unread-indicator, .app__body .new-messages__button div', 'color:' + theme.mentionColor);
changeCss('.sidebar--left .badge', 'color:' + theme.mentionColor + '!important;');
+ changeCss('.multi-teams .team-sidebar .team-wrapper .team-container .team-btn .badge', 'color:' + theme.mentionColor + '!important;');
}
if (theme.centerChannelBg) {