From 999d1553e1ce45adf58f6082b160bc1147dc592b Mon Sep 17 00:00:00 2001 From: enahum Date: Mon, 19 Dec 2016 10:05:46 -0300 Subject: 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) --- webapp/actions/global_actions.jsx | 38 ++++- webapp/actions/post_actions.jsx | 11 +- webapp/actions/websocket_actions.jsx | 25 ++- webapp/client/client.jsx | 24 +++ .../admin_console/admin_navbar_dropdown.jsx | 35 +++-- .../create_team/create_team_controller.jsx | 14 +- webapp/components/login/login_controller.jsx | 4 +- webapp/components/navbar.jsx | 2 + webapp/components/needs_team.jsx | 2 + webapp/components/notify_counts.jsx | 18 ++- webapp/components/root.jsx | 2 +- .../select_team/components/select_team_listing.jsx | 0 webapp/components/select_team/select_team.jsx | 108 +++++-------- webapp/components/sidebar.jsx | 14 +- webapp/components/sidebar_header_dropdown.jsx | 87 +++++------ webapp/components/sidebar_right.jsx | 1 + webapp/components/sidebar_right_menu.jsx | 9 -- .../components/signup/components/signup_email.jsx | 4 +- .../components/signup/components/signup_ldap.jsx | 7 +- webapp/components/signup/signup_controller.jsx | 2 +- .../team_sidebar/components/team_button.jsx | 84 ++++++++++ .../team_sidebar/team_sidebar_controller.jsx | 169 +++++++++++++++++++++ .../webrtc/components/webrtc_sidebar.jsx | 1 + webapp/i18n/en.json | 5 +- webapp/routes/route_team.jsx | 31 +++- webapp/sass/layout/_module.scss | 1 + webapp/sass/layout/_team-sidebar.scss | 90 +++++++++++ webapp/sass/responsive/_mobile.scss | 41 +++++ webapp/stores/channel_store.jsx | 5 +- webapp/stores/notification_store.jsx | 5 +- webapp/stores/preference_store.jsx | 5 +- webapp/stores/team_store.jsx | 77 +++++++++- webapp/tests/client_team.test.jsx | 14 ++ webapp/utils/async_client.jsx | 74 ++++++++- webapp/utils/constants.jsx | 2 + webapp/utils/utils.jsx | 12 +- 36 files changed, 846 insertions(+), 177 deletions(-) delete mode 100644 webapp/components/select_team/components/select_team_listing.jsx create mode 100644 webapp/components/team_sidebar/components/team_button.jsx create mode 100644 webapp/components/team_sidebar/team_sidebar_controller.jsx create mode 100644 webapp/sass/layout/_team-sidebar.scss (limited to 'webapp') 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(
  • ); + } else { + switchTeams = ( +
  • + + + + +
  • + ); } return ( @@ -104,17 +119,7 @@ export default class AdminNavbarDropdown extends React.Component { role='menu' > {teams} -
  • - - - - -
  • + {switchTeams}
  • - + { + 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 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( - - ); } - 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 = (
    ); - } else if (teamMembersCount === 0 && teamContents.length === 0 && openTeamContents.length === 0) { - teamContents = ( + } else if (openTeamContents.length === 0) { + openTeamContents = (
    ); - } else if (teamContents.length === 0 && openTeamContents.length > 0) { - teamContents = null; } - if (teamContents) { - content = ( -
    -

    - -

    -
    - {teamContents} -
    -
    - ); + if (Array.isArray(openTeamContents)) { + openTeamContents = openTeamContents.sort(this.teamContentsCompare); } - var openContent; - if (openTeamContents.length > 0) { - openContent = ( -
    -

    - -

    -
    - {openTeamContents.sort(this.teamContentsCompare)} -
    + let openContent = ( +
    +

    + +

    +
    + {openTeamContents}
    - ); - } +
    + ); if (!this.state.loaded) { openContent = ; @@ -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 = (
    + + + + ); + } else { + headerButton = ( + + + + + ); + } return (
    - - - - + {headerButton}
    @@ -249,7 +226,6 @@ export default class SelectTeam extends React.Component {

    {description}

    - {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( +
  • + + + +
  • + ); + } + teams.push(
  • ); - if (this.state.teamMembers && this.state.teamMembers.length > 1) { - teams.push( -
  • - ); - - 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( -
  • - - - {team.display_name} - -
  • - ); - } - } - } - } - 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(''); 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} -
  • - - - - -
  • {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 = ( + {this.props.mentions} + ); + } + } + + return ( +
    + + + {this.props.tip} + + } + > +
    + {badge} + {this.props.contents} +
    +
    + +
    + ); + } +} + +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 ( + + ); + }); + + if (moreTeams) { + teams.push( + + } + contents={} + /> + ); + } else if (global.window.mm_config.EnableTeamCreation === 'true' || isSystemAdmin) { + teams.push( + + } + contents={} + /> + ); + } + + return ( +
    +
    + {teams} +
    +
    + ); + } +} 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(''); 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) { -- cgit v1.2.3-1-g7c22