diff --git a/webapp/actions/team_actions.jsx b/webapp/actions/team_actions.jsx index 4cb57961b5..b091692f82 100644 --- a/webapp/actions/team_actions.jsx +++ b/webapp/actions/team_actions.jsx @@ -114,6 +114,31 @@ export function addUserToTeamFromInvite(data, hash, inviteId, success, error) { ); } +export function addUsersToTeam(teamId, userIds, success, error) { + Client.addUsersToTeam( + teamId, + userIds, + (teamMembers) => { + teamMembers.forEach((member) => { + TeamStore.removeMemberNotInTeam(teamId, member.user_id); + UserStore.removeProfileNotInTeam(teamId, member.user_id); + }); + UserStore.emitNotInTeamChange(); + + if (success) { + success(teamMembers); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'addUsersToTeam'); + + if (error) { + error(err); + } + } + ); +} + export function getInviteInfo(inviteId, success, error) { Client.getInviteInfo( inviteId, diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx index b9d4ec3769..1ab85922d5 100644 --- a/webapp/actions/user_actions.jsx +++ b/webapp/actions/user_actions.jsx @@ -492,6 +492,28 @@ export function searchUsers(term, teamId = TeamStore.getCurrentId(), options = { ); } +export function searchUsersNotInTeam(term, teamId = TeamStore.getCurrentId(), options = {}, success, error) { + Client.searchUsersNotInTeam( + term, + teamId, + options, + (data) => { + loadStatusesForProfilesList(data); + + if (success) { + success(data); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'searchUsersNotInTeam'); + + if (error) { + error(err); + } + } + ); +} + export function autocompleteUsersInChannel(username, channelId, success, error) { Client.autocompleteUsersInChannel( username, diff --git a/webapp/actions/websocket_actions.jsx b/webapp/actions/websocket_actions.jsx index ab798df28c..e9ebea4720 100644 --- a/webapp/actions/websocket_actions.jsx +++ b/webapp/actions/websocket_actions.jsx @@ -153,6 +153,10 @@ function handleEvent(msg) { handleUpdateTeamEvent(msg); break; + case SocketEvents.ADDED_TO_TEAM: + handleTeamAddedEvent(msg); + break; + case SocketEvents.USER_ADDED: handleUserAddedEvent(msg); break; @@ -241,6 +245,27 @@ function handlePostDeleteEvent(msg) { GlobalActions.emitPostDeletedEvent(post); } +function handleTeamAddedEvent(msg) { + Client.getTeam(msg.data.team_id, (team) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_TEAM, + team + }); + + Client.getMyTeamMembers((data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_MY_TEAM_MEMBERS, + team_members: data + }); + AsyncClient.getMyTeamsUnread(); + }, (err) => { + AsyncClient.dispatchError(err, 'getMyTeamMembers'); + }); + }, (err) => { + AsyncClient.dispatchError(err, 'getTeam'); + }); +} + function handleLeaveTeamEvent(msg) { if (UserStore.getCurrentId() === msg.data.user_id) { TeamStore.removeMyTeamMember(msg.data.team_id); diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index 1f70300e8f..c1a9d2f85a 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -489,6 +489,15 @@ export default class Client { // Team Routes Section + getTeam(teamId, success, error) { + request. + get(`${this.getTeamsRoute()}/${teamId}/me`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getTeam', success, error)); + } + findTeamByName(teamName, success, error) { request. post(`${this.getTeamsRoute()}/find_team_by_name`). @@ -681,6 +690,30 @@ export default class Client { this.trackEvent('api', 'api_teams_invite_members'); } + addUsersToTeam(teamId, userIds, success, error) { + let nonEmptyTeamId = teamId; + if (nonEmptyTeamId === '') { + nonEmptyTeamId = this.getTeamId(); + } + + const teamMembers = userIds.map((userId) => { + return { + team_id: nonEmptyTeamId, + user_id: userId + }; + }); + + request. + post(`${this.url}/api/v4/teams/${nonEmptyTeamId}/members/batch`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(teamMembers). + end(this.handleResponse.bind(this, 'addUsersToTeam', success, error)); + + this.trackEvent('api', 'api_teams_batch_add_members', {team_id: nonEmptyTeamId, count: teamMembers.length}); + } + removeUserFromTeam(teamId, userId, success, error) { let nonEmptyTeamId = teamId; if (nonEmptyTeamId === '') { @@ -1124,6 +1157,29 @@ export default class Client { this.trackEvent('api', 'api_profiles_get_in_team', {team_id: teamId}); } + getProfilesNotInTeam(teamId, offset, limit, success, error) { + // Super hacky, but this option only exists in api v4 + function wrappedSuccess(data, res) { + // Convert the profile list provided by api v4 to a map to match similar v3 calls + const profiles = {}; + + for (const profile of data) { + profiles[profile.id] = profile; + } + + success(profiles, res); + } + + request. + get(`${this.url}/api/v4/users?not_in_team=${this.getTeamId()}&page=${offset}&per_page=${limit}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getProfilesNotInTeam', wrappedSuccess, error)); + + this.trackEvent('api', 'api_profiles_get_not_in_team', {team_id: teamId}); + } + getProfilesInChannel(channelId, offset, limit, success, error) { request. get(`${this.getChannelNeededRoute(channelId)}/users/${offset}/${limit}`). @@ -1191,6 +1247,19 @@ export default class Client { end(this.handleResponse.bind(this, 'searchUsers', success, error)); } + searchUsersNotInTeam(term, teamId, options, success, error) { + // Note that this is calling an APIv4 Endpoint since no APIv3 equivalent exists. + request. + post(`${this.url}/api/v4/users/search`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({term, not_in_team_id: teamId, ...options}). + end(this.handleResponse.bind(this, 'searchUsersNotInTeam', success, error)); + + this.trackEvent('api', 'api_search_users_not_in_team', {team_id: teamId}); + } + autocompleteUsersInChannel(term, channelId, success, error) { request. get(`${this.getChannelNeededRoute(channelId)}/users/autocomplete?term=${encodeURIComponent(term)}`). diff --git a/webapp/components/add_users_to_team.jsx b/webapp/components/add_users_to_team.jsx new file mode 100644 index 0000000000..fce651d9f5 --- /dev/null +++ b/webapp/components/add_users_to_team.jsx @@ -0,0 +1,270 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import MultiSelect from 'components/multiselect/multiselect.jsx'; +import ProfilePicture from 'components/profile_picture.jsx'; + +import {addUsersToTeam} from 'actions/team_actions.jsx'; +import {searchUsersNotInTeam} from 'actions/user_actions.jsx'; + +import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import Constants from 'utils/constants.jsx'; +import {displayUsernameForUser} from 'utils/utils.jsx'; +import Client from 'client/web_client.jsx'; + +import React from 'react'; +import {Modal} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; +import {browserHistory} from 'react-router/es6'; + +const USERS_PER_PAGE = 50; +const MAX_SELECTABLE_VALUES = 20; + +export default class AddUsersToTeam extends React.Component { + constructor(props) { + super(props); + + this.handleHide = this.handleHide.bind(this); + this.handleExit = this.handleExit.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleDelete = this.handleDelete.bind(this); + this.onChange = this.onChange.bind(this); + this.search = this.search.bind(this); + this.addValue = this.addValue.bind(this); + + this.searchTimeoutId = 0; + + this.state = { + users: null, + values: [], + show: true, + search: false + }; + } + + componentDidMount() { + UserStore.addChangeListener(this.onChange); + UserStore.addNotInTeamChangeListener(this.onChange); + UserStore.addStatusesChangeListener(this.onChange); + + AsyncClient.getProfilesNotInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2); + } + + componentWillUnmount() { + UserStore.removeChangeListener(this.onChange); + UserStore.removeNotInTeamChangeListener(this.onChange); + UserStore.removeStatusesChangeListener(this.onChange); + } + + handleHide() { + this.setState({show: false}); + } + + handleExit() { + if (this.exitToChannel) { + browserHistory.push(this.exitToChannel); + } + + if (this.props.onModalDismissed) { + this.props.onModalDismissed(); + } + } + + handleSubmit(e) { + if (e) { + e.preventDefault(); + } + + const userIds = this.state.values.map((v) => v.id); + if (userIds.length === 0) { + return; + } + + addUsersToTeam(TeamStore.getCurrentId(), userIds); + + this.handleHide(); + } + + addValue(value) { + const values = Object.assign([], this.state.values); + if (values.indexOf(value) === -1) { + values.push(value); + } + + this.setState({values}); + } + + onChange(force) { + if (this.state.search && !force) { + return; + } + + const users = Object.assign([], UserStore.getProfileListNotInTeam(TeamStore.getCurrentId(), true)); + + for (let i = 0; i < users.length; i++) { + const user = Object.assign({}, users[i]); + user.value = user.id; + user.label = '@' + user.username; + users[i] = user; + } + + this.setState({ + users + }); + } + + handlePageChange(page, prevPage) { + if (page > prevPage) { + AsyncClient.getProfilesNotInTeam((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); + } + } + + search(term) { + clearTimeout(this.searchTimeoutId); + + if (term === '') { + this.onChange(true); + this.setState({search: false}); + this.searchTimeoutId = ''; + return; + } + + const teamId = TeamStore.getCurrentId(); + + const searchTimeoutId = setTimeout( + () => { + searchUsersNotInTeam( + term, + teamId, + {}, + (users) => { + if (searchTimeoutId !== this.searchTimeoutId) { + return; + } + + let indexToDelete = -1; + for (let i = 0; i < users.length; i++) { + if (users[i].id === UserStore.getCurrentId()) { + indexToDelete = i; + } + users[i].value = users[i].id; + users[i].label = '@' + users[i].username; + } + + if (indexToDelete !== -1) { + users.splice(indexToDelete, 1); + } + this.setState({search: true, users}); + } + ); + }, + Constants.SEARCH_TIMEOUT_MILLISECONDS + ); + + this.searchTimeoutId = searchTimeoutId; + } + + handleDelete(values) { + this.setState({values}); + } + + renderOption(option, isSelected, onAdd) { + var rowSelected = ''; + if (isSelected) { + rowSelected = 'more-modal__row--selected'; + } + + return ( +
onAdd(option)} + > + +
+
+ {displayUsernameForUser(option)} +
+
+ {option.email} +
+
+
+
+ +
+
+
+ ); + } + + renderValue(user) { + return user.username; + } + + render() { + const numRemainingText = ( + + ); + + return ( + + + + {TeamStore.getCurrent().display_name} + ) + }} + /> + + + + + + + ); + } +} + +AddUsersToTeam.propTypes = { + onModalDismissed: React.PropTypes.func +}; diff --git a/webapp/components/sidebar_header_dropdown.jsx b/webapp/components/sidebar_header_dropdown.jsx index a0a0205be8..baa8aae7b1 100644 --- a/webapp/components/sidebar_header_dropdown.jsx +++ b/webapp/components/sidebar_header_dropdown.jsx @@ -14,6 +14,7 @@ import AboutBuildModal from './about_build_modal.jsx'; import SidebarHeaderDropdownButton from './sidebar_header_dropdown_button.jsx'; import TeamMembersModal from './team_members_modal.jsx'; import UserSettingsModal from './user_settings/user_settings_modal.jsx'; +import AddUsersToTeam from './add_users_to_team.jsx'; import {Constants, WebrtcActionTypes} from 'utils/constants.jsx'; @@ -43,6 +44,8 @@ export default class SidebarHeaderDropdown extends React.Component { this.handleAboutModal = this.handleAboutModal.bind(this); this.aboutModalDismissed = this.aboutModalDismissed.bind(this); this.toggleAccountSettingsModal = this.toggleAccountSettingsModal.bind(this); + this.showAddUsersToTeamModal = this.showAddUsersToTeamModal.bind(this); + this.hideAddUsersToTeamModal = this.hideAddUsersToTeamModal.bind(this); this.showInviteMemberModal = this.showInviteMemberModal.bind(this); this.showGetTeamInviteLinkModal = this.showGetTeamInviteLinkModal.bind(this); this.showTeamMembersModal = this.showTeamMembersModal.bind(this); @@ -61,7 +64,8 @@ export default class SidebarHeaderDropdown extends React.Component { showAboutModal: false, showDropdown: false, showTeamMembersModal: false, - showUserSettingsModal: false + showUserSettingsModal: false, + showAddUsersToTeamModal: false }; } @@ -102,6 +106,21 @@ export default class SidebarHeaderDropdown extends React.Component { }); } + showAddUsersToTeamModal(e) { + e.preventDefault(); + + this.setState({ + showAddUsersToTeamModal: true, + showDropdown: false + }); + } + + hideAddUsersToTeamModal() { + this.setState({ + showAddUsersToTeamModal: false + }); + } + showInviteMemberModal(e) { e.preventDefault(); @@ -181,6 +200,7 @@ export default class SidebarHeaderDropdown extends React.Component { const currentUser = this.props.currentUser; let teamLink = ''; let inviteLink = ''; + let addMemberToTeam = ''; let manageLink = ''; let sysAdminLink = ''; let isAdmin = false; @@ -204,7 +224,22 @@ export default class SidebarHeaderDropdown extends React.Component { > + + + ); + + addMemberToTeam = ( +
  • + +
  • @@ -230,9 +265,11 @@ export default class SidebarHeaderDropdown extends React.Component { if (config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { teamLink = null; inviteLink = null; + addMemberToTeam = null; } else if (config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { teamLink = null; inviteLink = null; + addMemberToTeam = null; } } } @@ -416,10 +453,8 @@ export default class SidebarHeaderDropdown extends React.Component { ); } - let nativeAppDivider = null; let nativeAppLink = null; if (global.window.mm_config.AppDownloadLink && !UserAgent.isMobileApp()) { - nativeAppDivider =
  • ; nativeAppLink = (
  • + ); + } + + const accountSettings = ( +
  • + + + +
  • + ); + + const about = ( +
  • + + + +
  • + ); + + const logout = ( +
  • + GlobalActions.emitUserLoggedOutEvent()} + > + + +
  • + ); + + const customEmoji = this.renderCustomEmojiLink(); + + // Dividers. + let inviteDivider = null; + if (inviteLink || teamLink || addMemberToTeam) { + inviteDivider =
  • ; + } + + let teamDivider = null; + if (teamSettings || manageLink || teams) { + teamDivider =
  • ; + } + + let backstageDivider = null; + if (integrationsLink || customEmoji) { + backstageDivider =
  • ; + } + + let sysAdminDivider = null; + if (sysAdminLink) { + sysAdminDivider =
  • ; + } + + let helpDivider = null; + if (helpLink || reportLink || nativeAppLink || about) { + helpDivider =
  • ; + } + + let logoutDivider = null; + if (logout) { + logoutDivider =
  • ; + } + return ( -
  • - - - -
  • + {accountSettings} + {inviteDivider} {inviteLink} {teamLink} -
  • - GlobalActions.emitUserLoggedOutEvent()} - > - - -
  • -
  • - {integrationsLink} - {this.renderCustomEmojiLink()} -
  • + {addMemberToTeam} + {teamDivider} {teamSettings} {manageLink} - {sysAdminLink} {teams} -
  • + {backstageDivider} + {integrationsLink} + {customEmoji} + {sysAdminDivider} + {sysAdminLink} + {helpDivider} {helpLink} {reportLink} -
  • - - - -
  • - {nativeAppDivider} {nativeAppLink} + {about} + {logoutDivider} + {logout} this.setState({showUserSettingsModal: false})} @@ -519,6 +611,7 @@ export default class SidebarHeaderDropdown extends React.Component { show={this.state.showAboutModal} onModalDismissed={this.aboutModalDismissed} /> + {addUsersToTeamModal} ); diff --git a/webapp/components/sidebar_right_menu.jsx b/webapp/components/sidebar_right_menu.jsx index c3c6d0f8fc..7e689d95e9 100644 --- a/webapp/components/sidebar_right_menu.jsx +++ b/webapp/components/sidebar_right_menu.jsx @@ -6,6 +6,7 @@ import TeamMembersModal from './team_members_modal.jsx'; import ToggleModalButton from './toggle_modal_button.jsx'; import UserSettingsModal from './user_settings/user_settings_modal.jsx'; import AboutBuildModal from './about_build_modal.jsx'; +import AddUsersToTeam from './add_users_to_team.jsx'; import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -36,6 +37,8 @@ export default class SidebarRightMenu extends React.Component { this.onChange = this.onChange.bind(this); this.handleClick = this.handleClick.bind(this); this.handleAboutModal = this.handleAboutModal.bind(this); + this.showAddUsersToTeamModal = this.showAddUsersToTeamModal.bind(this); + this.hideAddUsersToTeamModal = this.hideAddUsersToTeamModal.bind(this); this.searchMentions = this.searchMentions.bind(this); this.aboutModalDismissed = this.aboutModalDismissed.bind(this); this.getFlagged = this.getFlagged.bind(this); @@ -43,6 +46,7 @@ export default class SidebarRightMenu extends React.Component { const state = this.getStateFromStores(); state.showUserSettingsModal = false; state.showAboutModal = false; + state.showAddUsersToTeamModal = false; this.state = state; } @@ -62,6 +66,21 @@ export default class SidebarRightMenu extends React.Component { this.setState({showAboutModal: false}); } + showAddUsersToTeamModal(e) { + e.preventDefault(); + + this.setState({ + showAddUsersToTeamModal: true, + showDropdown: false + }); + } + + hideAddUsersToTeamModal() { + this.setState({ + showAddUsersToTeamModal: false + }); + } + getFlagged(e) { e.preventDefault(); getFlaggedPosts(); @@ -145,6 +164,7 @@ export default class SidebarRightMenu extends React.Component { const currentUser = UserStore.getCurrentUser(); let teamLink; let inviteLink; + let addUserToTeamLink; let teamSettingsLink; let manageLink; let consoleLink; @@ -165,7 +185,23 @@ export default class SidebarRightMenu extends React.Component { + + + ); + + addUserToTeamLink = ( +
  • + + +
  • @@ -192,9 +228,11 @@ export default class SidebarRightMenu extends React.Component { if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { teamLink = null; inviteLink = null; + addUserToTeamLink = null; } else if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { teamLink = null; inviteLink = null; + addUserToTeamLink = null; } } @@ -360,6 +398,25 @@ export default class SidebarRightMenu extends React.Component { ); } + let addUsersToTeamModal; + if (this.state.showAddUsersToTeamModal) { + addUsersToTeamModal = ( + + ); + } + + let teamDivider = null; + if (teamSettingsLink || manageLink || joinAnotherTeamLink) { + teamDivider =
  • ; + } + + let consoleDivider = null; + if (consoleLink) { + consoleDivider =
  • ; + } + return (
  • +
  • {inviteLink} {teamLink} - {joinAnotherTeamLink} -
  • + {addUserToTeamLink} + {teamDivider} {teamSettingsLink} {manageLink} + {joinAnotherTeamLink} + {consoleDivider} {consoleLink}
  • {helpLink} {reportLink} + {nativeAppLink}
  • - {nativeAppLink} -
  • + {addUsersToTeamModal} ); } diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 37207b2796..7515c52fd4 100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -137,6 +137,7 @@ "add_outgoing_webhook.triggerWordsTriggerWhen.help": "Choose when to trigger the outgoing webhook; if the first word of a message matches a Trigger Word exactly, or if it starts with a Trigger Word.", "add_outgoing_webhook.triggerWordsTriggerWhenFullWord": "First word matches a trigger word exactly", "add_outgoing_webhook.triggerWordsTriggerWhenStartsWith": "First word starts with a trigger word", + "add_users_to_team.title": "Add New Members To {teamName} Team", "admin.advance.cluster": "High Availability (Beta)", "admin.advance.metrics": "Performance Monitoring", "admin.audits.reload": "Reload User Activity Logs", @@ -1759,12 +1760,13 @@ "navbar.viewPinnedPosts": "View Pinned Posts", "navbar_dropdown.about": "About Mattermost", "navbar_dropdown.accountSettings": "Account Settings", + "navbar_dropdown.addMemberToTeam": "Add Members to Team", "navbar_dropdown.console": "System Console", "navbar_dropdown.create": "Create a New Team", "navbar_dropdown.emoji": "Custom Emoji", "navbar_dropdown.help": "Help", "navbar_dropdown.integrations": "Integrations", - "navbar_dropdown.inviteMember": "Invite New Member", + "navbar_dropdown.inviteMember": "Send Email Invite", "navbar_dropdown.join": "Join Another Team", "navbar_dropdown.leave": "Leave Team", "navbar_dropdown.logout": "Logout", @@ -1917,10 +1919,11 @@ "sidebar.unreadBelow": "Unread post(s) below", "sidebar_header.tutorial": "

    Main Menu

    The Main Menu is where you can Invite New Members, access your Account Settings and set your Theme Color.

    Team administrators can also access their Team Settings from this menu.

    System administrators will find a System Console option to administrate the entire system.

    ", "sidebar_right_menu.accountSettings": "Account Settings", + "sidebar_right_menu.addMemberToTeam": "Add Members to Team", "sidebar_right_menu.console": "System Console", "sidebar_right_menu.flagged": "Flagged Posts", "sidebar_right_menu.help": "Help", - "sidebar_right_menu.inviteNew": "Invite New Member", + "sidebar_right_menu.inviteNew": "Send Email Invite", "sidebar_right_menu.logout": "Logout", "sidebar_right_menu.manageMembers": "Manage Members", "sidebar_right_menu.nativeApps": "Download Apps", diff --git a/webapp/stores/team_store.jsx b/webapp/stores/team_store.jsx index 6f81a9345d..a77527d376 100644 --- a/webapp/stores/team_store.jsx +++ b/webapp/stores/team_store.jsx @@ -252,6 +252,12 @@ class TeamStoreClass extends EventEmitter { } } + removeMemberNotInTeam(teamId = this.getCurrentId(), userId) { + if (this.members_not_in_team[teamId]) { + Reflect.deleteProperty(this.members_not_in_team[teamId], userId); + } + } + getMembersInTeam(teamId = this.getCurrentId()) { return Object.assign({}, this.members_in_team[teamId]) || {}; } @@ -365,6 +371,10 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => { TeamStore.saveMyTeam(action.team); TeamStore.emitChange(); break; + case ActionTypes.RECEIVED_TEAM: + TeamStore.saveTeam(action.team); + TeamStore.emitChange(); + break; case ActionTypes.CREATED_TEAM: TeamStore.saveTeam(action.team); TeamStore.appendMyTeamMember(action.member); diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx index 6f4acae0ab..02adeb7899 100644 --- a/webapp/stores/user_store.jsx +++ b/webapp/stores/user_store.jsx @@ -15,6 +15,7 @@ const UserStatuses = Constants.UserStatuses; const CHANGE_EVENT_NOT_IN_CHANNEL = 'change_not_in_channel'; const CHANGE_EVENT_IN_CHANNEL = 'change_in_channel'; +const CHANGE_EVENT_NOT_IN_TEAM = 'change_not_in_team'; const CHANGE_EVENT_IN_TEAM = 'change_in_team'; const CHANGE_EVENT_WITHOUT_TEAM = 'change_without_team'; const CHANGE_EVENT = 'change'; @@ -36,6 +37,11 @@ class UserStoreClass extends EventEmitter { this.paging_offset = 0; this.paging_count = 0; + // Lists of sorted IDs for users in a team + this.profiles_not_in_team = {}; + this.not_in_team_offset = 0; + this.not_in_team_count = 0; + // Lists of sorted IDs for users in a team this.profiles_in_team = {}; this.in_team_offset = 0; @@ -85,6 +91,18 @@ class UserStoreClass extends EventEmitter { this.removeListener(CHANGE_EVENT_IN_TEAM, callback); } + emitNotInTeamChange() { + this.emit(CHANGE_EVENT_NOT_IN_TEAM); + } + + addNotInTeamChangeListener(callback) { + this.on(CHANGE_EVENT_NOT_IN_TEAM, callback); + } + + removeNotInTeamChangeListener(callback) { + this.removeListener(CHANGE_EVENT_NOT_IN_TEAM, callback); + } + emitInChannelChange() { this.emit(CHANGE_EVENT_IN_CHANNEL); } @@ -373,6 +391,75 @@ class UserStoreClass extends EventEmitter { userIds.splice(index, 1); } + // Not In Team Profiles + + saveProfilesNotInTeam(teamId, profiles) { + const oldProfileList = this.profiles_not_in_team[teamId] || []; + const oldProfileMap = {}; + for (let i = 0; i < oldProfileList.length; i++) { + oldProfileMap[oldProfileList[i]] = this.getProfile(oldProfileList[i]); + } + + const newProfileMap = Object.assign({}, oldProfileMap, profiles); + const newProfileList = Object.keys(newProfileMap); + + newProfileList.sort((a, b) => { + const aProfile = newProfileMap[a]; + const bProfile = newProfileMap[b]; + + if (aProfile.username < bProfile.username) { + return -1; + } + if (aProfile.username > bProfile.username) { + return 1; + } + return 0; + }); + + this.profiles_not_in_team[teamId] = newProfileList; + this.saveProfiles(profiles); + } + + removeProfileNotInTeam(teamId, userId) { + const userIds = this.profiles_not_in_team[teamId]; + if (!userIds) { + return; + } + + const index = userIds.indexOf(userId); + if (index === -1) { + return; + } + + userIds.splice(index, 1); + } + + getProfileListNotInTeam(teamId = TeamStore.getCurrentId(), skipCurrent = false, skipInactive = false) { + const userIds = this.profiles_not_in_team[teamId] || []; + const profiles = []; + const currentId = this.getCurrentId(); + + for (let i = 0; i < userIds.length; i++) { + const profile = this.getProfile(userIds[i]); + + if (!profile) { + continue; + } + + if (skipCurrent && profile.id === currentId) { + continue; + } + + if (skipInactive && profile.delete_at > 0) { + continue; + } + + profiles.push(profile); + } + + return profiles; + } + // Channel-Wide Profiles saveProfilesInChannel(channelId = ChannelStore.getCurrentId(), profiles) { @@ -646,6 +733,19 @@ class UserStoreClass extends EventEmitter { return this.in_team_count; } + setNotInTeamPage(offset, count) { + this.not_in_team_offset = offset + count; + this.not_in_team_count = this.not_in_team_count + count; + } + + getNotInTeamPagingOffset() { + return this.not_in_team_offset; + } + + getNotInTeamPagingCount() { + return this.not_in_team_count; + } + setInChannelPage(channelId, offset, count) { this.in_channel_offset[channelId] = offset + count; this.in_channel_count[channelId] = this.dm_paging_count + count; @@ -694,6 +794,13 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { } UserStore.emitInTeamChange(); break; + case ActionTypes.RECEIVED_PROFILES_NOT_IN_TEAM: + UserStore.saveProfilesNotInTeam(action.team_id, action.profiles); + if (action.offset != null && action.count != null) { + UserStore.setNotInTeamPage(action.offset, action.count); + } + UserStore.emitNotInTeamChange(); + break; case ActionTypes.RECEIVED_PROFILES_IN_CHANNEL: UserStore.saveProfilesInChannel(action.channel_id, action.profiles); if (action.offset != null && action.count != null) { diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index b4b361cb4f..faaf3aee65 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -403,6 +403,36 @@ export function getProfilesInTeam(teamId = TeamStore.getCurrentId(), offset = Us ); } +export function getProfilesNotInTeam(teamId = TeamStore.getCurrentId(), offset = UserStore.getInTeamPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { + const callName = `getProfilesNotInTeam${teamId}${offset}${limit}`; + + if (isCallInProgress(callName)) { + return; + } + + callTracker[callName] = utils.getTimestamp(); + Client.getProfilesNotInTeam( + teamId, + offset, + limit, + (data) => { + callTracker[callName] = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_PROFILES_NOT_IN_TEAM, + profiles: data, + team_id: teamId, + offset, + count: Object.keys(data).length + }); + }, + (err) => { + callTracker[callName] = 0; + dispatchError(err, 'getProfilesNotInTeam'); + } + ); +} + export function getProfilesInChannel(channelId = ChannelStore.getCurrentId(), offset = UserStore.getInChannelPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { const callName = `getProfilesInChannel${channelId}${offset}${limit}`; @@ -830,34 +860,6 @@ 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) { diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 5b4ab6611d..8428f71213 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -95,6 +95,7 @@ export const ActionTypes = keyMirror({ RECEIVED_PROFILES: null, RECEIVED_PROFILES_IN_TEAM: null, + RECEIVED_PROFILES_NOT_IN_TEAM: null, RECEIVED_PROFILE: null, RECEIVED_PROFILES_IN_CHANNEL: null, RECEIVED_PROFILES_NOT_IN_CHANNEL: null, @@ -137,6 +138,7 @@ export const ActionTypes = keyMirror({ RECEIVED_MSG: null, + RECEIVED_TEAM: null, RECEIVED_MY_TEAM: null, CREATED_TEAM: null, UPDATE_TEAM: null, @@ -219,6 +221,7 @@ export const SocketEvents = { CHANNEL_VIEWED: 'channel_viewed', DIRECT_ADDED: 'direct_added', NEW_USER: 'new_user', + ADDED_TO_TEAM: 'added_to_team', LEAVE_TEAM: 'leave_team', UPDATE_TEAM: 'update_team', USER_ADDED: 'user_added',