diff options
author | Joram Wilander <jwawilander@gmail.com> | 2016-10-19 14:49:25 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-10-19 14:49:25 -0400 |
commit | 365b8b465e8a53ebb2da2bf3aef659ac81a2bc6a (patch) | |
tree | 643b2dd52b478c2c0b049ac28798d870b9dfd397 /webapp/components | |
parent | 0512bd26ee85473aa47206d5f207a9a506019138 (diff) | |
download | chat-365b8b465e8a53ebb2da2bf3aef659ac81a2bc6a.tar.gz chat-365b8b465e8a53ebb2da2bf3aef659ac81a2bc6a.tar.bz2 chat-365b8b465e8a53ebb2da2bf3aef659ac81a2bc6a.zip |
Merging performance branch into master (#4268)
* improve performance on sendNotifications
* Fix SQL queries
* Remove get direct profiles, not needed anymore
* Add raw data to error details if AppError fails to decode
* men
* Fix decode (#4052)
* Fixing json decode
* Adding unit test
* Initial work for client scaling (#4051)
* Begin adding paging to profiles API
* Added more paging functionality
* Finish hooking up admin console user lists
* Add API for searching users and add searching to all user lists
* Add lazy loading of profiles
* Revert config.json
* Fix unit tests and some style issues
* Add GetProfilesFromList to Go driver and fix web unit test
* Update etag for GetProfiles
* Updating ui for filters and pagination (#4044)
* Updating UI for pagination
* Adjusting margins for filter row
* Adjusting margin for specific modals
* Adding relative padding to system console
* Adjusting responsive view
* Update client user tests
* Minor fixes for direct messages modal (#4056)
* Remove some unneeded initial load calls (#4057)
* UX updates to user lists, added smart counts and bug fixes (#4059)
* Improved getExplicitMentions and unit tests (#4064)
* Refactor getting posts to lazy load profiles correctly (#4062)
* Comment out SetActiveChannel test (#4066)
* Profiler cpu, block, and memory profiler. (#4081)
* Fix TestSetActiveChannel unit test (#4071)
* Fixing build failure caused by dependancies updating (#4076)
* Adding profiler
* Fix admin_team_member_dropdown eslint errors
* Bumping session cache size (#4077)
* Bumping session cache size
* Bumping status cache
* Refactor how the client handles channel members to be large team friendly (#4106)
* Refactor how the client handles channel members to be large team friendly
* Change Id to ChannelId in ChannelStats model
* Updated getChannelMember and getProfilesByIds routes to match proposal
* Performance improvements (#4100)
* Performance improvements
* Fixing re-connect issue
* Fixing error message
* Some other minor perf tweaks
* Some other minor perf tweaks
* Fixing config file
* Fixing buffer size
* Fixing web socket send message
* adding some error logging
* fix getMe to be user required
* Fix websocket event for new user
* Fixing shutting down
* Reverting web socket changes
* Fixing logging lvl
* Adding caching to GetMember
* Adding some logging
* Fixing caching
* Fixing caching invalidate
* Fixing direct message caching
* Fixing caching
* Fixing caching
* Remove GetDirectProfiles from initial load
* Adding logging and fixing websocket client
* Adding back caching from bad merge.
* Explicitly close go driver requests (#4162)
* Refactored how the client handles team members to be more large team friendly (#4159)
* Refactor getProfilesForDirectMessageList API into getAllProfiles API
* Refactored how the client handles team members to be more large team friendly
* Fix js error when receiving a notification
* Fix JS error caused by current user being overwritten with sanitized version (#4165)
* Adding error message to status failure (#4167)
* Fix a few bugs caused by client scaling refactoring (#4170)
* When there is no read replica, don't open a second set of connections to the master database (#4173)
* Adding connection tacking to stats (#4174)
* Reduce DB writes for statuses and other status related changes (#4175)
* Fix bug preventing opening of DM channels from more modal (#4181)
* Fixing socket timing error (#4183)
* Fixing ping/pong handler
* Fixing socket timing error
* Commenting out status broadcasting
* Removing user status changes
* Removing user status changes
* Removing user status changes
* Removing user status changes
* Adding DoPreComputeJson()
* Performance improvements (#4194)
* * Fix System Console Analytics queries
* Add db.SetConnMaxLifetime to 15 minutes
* Add "net/http/pprof" for profiling
* Add FreeOSMemory() to manually release memory on reload config
* Add flag to enable http profiler
* Fix memory leak (#4197)
* Fix memory leak
* removed unneeded nil assignment
* Fixing go routine leak (#4208)
* Merge fixes
* Merge fix
* Refactored statuses to be queried by the client rather than broadcast by the server (#4212)
* Refactored server code to reduce status broadcasts and to allow getting statuses by IDs
* Refactor client code to periodically fetch statuses
* Add store unit test for getting statuses by ids
* Fix status unit test
* Add getStatusesByIds REST API and move the client over to use that instead of the WebSocket
* Adding multiple threads to websocket hub (#4230)
* Adding multiple threads to websocket hub
* Fixing unit tests
* Fixing so websocket connections from the same user end up in the sameā¦ (#4240)
* Fixing so websocket connections from the same user end up in the same list
* Removing old comment
* Refactor user autocomplete to query the server (#4239)
* Add API for autocompleting users
* Converted at mention autocomplete to query server
* Converted user search autocomplete to query server
* Switch autocomplete API naming to use term instead of username
* Split autocomplete API into two, one for channels and for teams
* Fix copy/paste error
* Some final client scaling fixes (#4246)
* Add lazy loading of profiles to integration pages
* Add lazy loading of profiles to emoji page
* Fix JS error when receiving post in select team menu and also clean up channel store
Diffstat (limited to 'webapp/components')
44 files changed, 1218 insertions, 811 deletions
diff --git a/webapp/components/admin_console/admin_navbar_dropdown.jsx b/webapp/components/admin_console/admin_navbar_dropdown.jsx index 7b958cbb0..f20451b4b 100644 --- a/webapp/components/admin_console/admin_navbar_dropdown.jsx +++ b/webapp/components/admin_console/admin_navbar_dropdown.jsx @@ -22,7 +22,7 @@ export default class AdminNavbarDropdown extends React.Component { this.state = { teams: TeamStore.getAll(), - teamMembers: TeamStore.getTeamMembers() + teamMembers: TeamStore.getMyTeamMembers() }; } @@ -45,7 +45,7 @@ export default class AdminNavbarDropdown extends React.Component { onTeamChange() { this.setState({ teams: TeamStore.getAll(), - teamMembers: TeamStore.getTeamMembers() + teamMembers: TeamStore.getMyTeamMembers() }); } diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/admin_team_members_dropdown.jsx index ac548afe0..85daa86ba 100644 --- a/webapp/components/admin_console/user_item.jsx +++ b/webapp/components/admin_console/admin_team_members_dropdown.jsx @@ -8,11 +8,11 @@ import UserStore from 'stores/user_store.jsx'; import ConfirmModal from '../confirm_modal.jsx'; import TeamStore from 'stores/team_store.jsx'; -import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; +import {FormattedMessage} from 'react-intl'; import React from 'react'; -export default class UserItem extends React.Component { +export default class AdminTeamMembersDropdown extends React.Component { constructor(props) { super(props); @@ -50,7 +50,7 @@ export default class UserItem extends React.Component { } ); Client.updateTeamMemberRoles( - this.props.team.id, + this.props.teamMember.team_id, this.props.user.id, 'team_user', () => { @@ -74,7 +74,7 @@ export default class UserItem extends React.Component { handleRemoveFromTeam() { Client.removeUserFromTeam( - this.props.team.id, + this.props.teamMember.team_id, this.props.user.id, () => { this.props.refreshProfiles(); @@ -111,7 +111,7 @@ export default class UserItem extends React.Component { doMakeTeamAdmin() { Client.updateTeamMemberRoles( - this.props.team.id, + this.props.teamMember.team_id, this.props.user.id, 'team_user team_admin', () => { @@ -241,7 +241,6 @@ export default class UserItem extends React.Component { } const me = UserStore.getCurrentUser(); - const email = user.email; let showMakeMember = Utils.isAdmin(teamMember.roles) || Utils.isSystemAdmin(user.roles); let showMakeAdmin = !Utils.isAdmin(teamMember.roles) && !Utils.isSystemAdmin(user.roles); let showMakeSystemAdmin = !Utils.isSystemAdmin(user.roles); @@ -406,39 +405,8 @@ export default class UserItem extends React.Component { ); } - let mfaActiveText; - if (mfaEnabled) { - if (user.mfa_active) { - mfaActiveText = ( - <FormattedHTMLMessage - id='admin.user_item.mfaYes' - defaultMessage=', <strong>MFA</strong>: Yes' - /> - ); - } else { - mfaActiveText = ( - <FormattedHTMLMessage - id='admin.user_item.mfaNo' - defaultMessage=', <strong>MFA</strong>: No' - /> - ); - } - } - - let authServiceText; let passwordReset; if (user.auth_service) { - const service = (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) ? user.auth_service.toUpperCase() : Utils.toTitleCase(user.auth_service); - authServiceText = ( - <FormattedHTMLMessage - id='admin.user_item.authServiceNotEmail' - defaultMessage=', <strong>Sign-in Method:</strong> {service}' - values={{ - service - }} - /> - ); - passwordReset = ( <li role='presentation'> <a @@ -454,13 +422,6 @@ export default class UserItem extends React.Component { </li> ); } else { - authServiceText = ( - <FormattedHTMLMessage - id='admin.user_item.authServiceEmail' - defaultMessage=', <strong>Sign-in Method:</strong> Email' - /> - ); - passwordReset = ( <li role='presentation'> <a @@ -531,63 +492,38 @@ export default class UserItem extends React.Component { } return ( - <div className='more-modal__row'> - <img - className='more-modal__image pull-left' - src={`${Client.getUsersRoute()}/${user.id}/image?time=${user.update_at}`} - height='36' - width='36' - /> - <div className='more-modal__details'> - <div className='more-modal__name'>{displayedName}</div> - <div className='more-modal__description'> - <FormattedHTMLMessage - id='admin.user_item.emailTitle' - defaultMessage='<strong>Email:</strong> {email}' - values={{ - email - }} - /> - {authServiceText} - {mfaActiveText} - </div> - {serverError} - </div> - <div className='more-modal__actions'> - <div className='dropdown member-drop'> - <a - href='#' - className='dropdown-toggle theme' - type='button' - data-toggle='dropdown' - aria-expanded='true' - > - <span>{currentRoles} </span> - <span className='caret'/> - </a> - <ul - className='dropdown-menu member-menu' - role='menu' - > - {removeFromTeam} - {makeAdmin} - {makeMember} - {makeActive} - {makeNotActive} - {makeSystemAdmin} - {mfaReset} - {passwordReset} - </ul> - </div> - </div> + <div className='dropdown member-drop'> + <a + href='#' + className='dropdown-toggle theme' + type='button' + data-toggle='dropdown' + aria-expanded='true' + > + <span>{currentRoles} </span> + <span className='caret'/> + </a> + <ul + className='dropdown-menu member-menu' + role='menu' + > + {removeFromTeam} + {makeAdmin} + {makeMember} + {makeActive} + {makeNotActive} + {makeSystemAdmin} + {mfaReset} + {passwordReset} + </ul> {makeDemoteModal} + {serverError} </div> ); } } -UserItem.propTypes = { - team: React.PropTypes.object.isRequired, +AdminTeamMembersDropdown.propTypes = { user: React.PropTypes.object.isRequired, teamMember: React.PropTypes.object.isRequired, refreshProfiles: React.PropTypes.func.isRequired, diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx index 56b76c195..8fa73b084 100644 --- a/webapp/components/admin_console/team_users.jsx +++ b/webapp/components/admin_console/team_users.jsx @@ -1,16 +1,25 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import AdminStore from 'stores/admin_store.jsx'; -import Client from 'client/web_client.jsx'; -import FormError from 'components/form_error.jsx'; -import LoadingScreen from '../loading_screen.jsx'; -import UserItem from './user_item.jsx'; +import SearchableUserList from 'components/searchable_user_list.jsx'; +import AdminTeamMembersDropdown from './admin_team_members_dropdown.jsx'; import ResetPasswordModal from './reset_password_modal.jsx'; +import FormError from 'components/form_error.jsx'; + +import AdminStore from 'stores/admin_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import {searchUsers, loadProfilesAndTeamMembers, loadTeamMembersForProfilesList} from 'actions/user_actions.jsx'; +import {getTeamStats} from 'utils/async_client.jsx'; -import {FormattedMessage} from 'react-intl'; +import Constants from 'utils/constants.jsx'; +import * as Utils from 'utils/utils.jsx'; import React from 'react'; +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; + +const USERS_PER_PAGE = 50; export default class UserList extends React.Component { static get propTypes() { @@ -23,34 +32,49 @@ export default class UserList extends React.Component { super(props); this.onAllTeamsChange = this.onAllTeamsChange.bind(this); + this.onStatsChange = this.onStatsChange.bind(this); + this.onUsersChange = this.onUsersChange.bind(this); + this.onTeamChange = this.onTeamChange.bind(this); - this.getTeamProfiles = this.getTeamProfiles.bind(this); - this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this); this.doPasswordReset = this.doPasswordReset.bind(this); this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this); this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this); - this.getTeamMemberForUser = this.getTeamMemberForUser.bind(this); + this.nextPage = this.nextPage.bind(this); + this.search = this.search.bind(this); + this.loadComplete = this.loadComplete.bind(this); + + const stats = TeamStore.getStats(this.props.params.team); this.state = { team: AdminStore.getTeam(this.props.params.team), - users: null, - teamMembers: null, + users: [], + teamMembers: TeamStore.getMembersInTeam(this.props.params.team), + total: stats.member_count, serverError: null, showPasswordModal: false, + loading: true, user: null }; } componentDidMount() { - this.getCurrentTeamProfiles(); - AdminStore.addAllTeamsChangeListener(this.onAllTeamsChange); + UserStore.addInTeamChangeListener(this.onUsersChange); + TeamStore.addChangeListener(this.onTeamChange); + TeamStore.addStatsChangeListener(this.onStatsChange); + + loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, this.props.params.team, this.loadComplete); + getTeamStats(this.props.params.team); } componentWillReceiveProps(nextProps) { if (nextProps.params.team !== this.props.params.team) { + const stats = TeamStore.getStats(nextProps.params.team); this.setState({ - team: AdminStore.getTeam(nextProps.params.team) + team: AdminStore.getTeam(nextProps.params.team), + users: [], + teamMembers: TeamStore.getMembersInTeam(nextProps.params.team), + total: stats.member_count }); this.getTeamProfiles(nextProps.params.team); @@ -59,6 +83,13 @@ export default class UserList extends React.Component { componentWillUnmount() { AdminStore.removeAllTeamsChangeListener(this.onAllTeamsChange); + UserStore.removeInTeamChangeListener(this.onUsersChange); + TeamStore.removeChangeListener(this.onTeamChange); + TeamStore.removeStatsChangeListener(this.onStatsChange); + } + + loadComplete() { + this.setState({loading: false}); } onAllTeamsChange() { @@ -67,59 +98,21 @@ export default class UserList extends React.Component { }); } - getCurrentTeamProfiles() { - this.getTeamProfiles(this.props.params.team); + onStatsChange() { + const stats = TeamStore.getStats(this.props.params.team); + this.setState({total: stats.member_count}); } - getTeamProfiles(teamId) { - Client.getTeamMembers( - teamId, - (data) => { - this.setState({ - teamMembers: data - }); - }, - (err) => { - this.setState({ - teamMembers: null, - serverError: err.message - }); - } - ); - - Client.getProfilesForTeam( - teamId, - (users) => { - var memberList = []; - for (var id in users) { - if (users.hasOwnProperty(id)) { - memberList.push(users[id]); - } - } - - memberList.sort((a, b) => { - if (a.username < b.username) { - return -1; - } + onUsersChange() { + this.setState({users: UserStore.getProfileListInTeam(this.props.params.team)}); + } - if (a.username > b.username) { - return 1; - } + onTeamChange() { + this.setState({teamMembers: TeamStore.getMembersInTeam(this.props.params.team)}); + } - return 0; - }); - - this.setState({ - users: memberList - }); - }, - (err) => { - this.setState({ - users: null, - serverError: err.message - }); - } - ); + nextPage(page) { + loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.props.params.team); } doPasswordReset(user) { @@ -144,20 +137,21 @@ export default class UserList extends React.Component { }); } - getTeamMemberForUser(userId) { - if (this.state.teamMembers) { - for (const index in this.state.teamMembers) { - if (this.state.teamMembers.hasOwnProperty(index)) { - var teamMember = this.state.teamMembers[index]; - - if (teamMember.user_id === userId) { - return teamMember; - } - } - } + search(term) { + if (term === '') { + this.setState({search: false, users: UserStore.getProfileListInTeam(this.props.params.team)}); + return; } - return null; + searchUsers( + term, + this.props.params.team, + {}, + (users) => { + this.setState({loading: true, search: true, users}); + loadTeamMembersForProfilesList(users, this.props.params.team, this.loadComplete); + } + ); } render() { @@ -165,41 +159,71 @@ export default class UserList extends React.Component { return null; } - if (this.state.users == null || this.state.teamMembers == null) { - return ( - <div className='wrapper--fixed'> - <h3> - <FormattedMessage - id='admin.userList.title' - defaultMessage='Users for {team}' - values={{ - team: this.state.team.name - }} - /> - </h3> - <FormError error={this.state.serverError}/> - <LoadingScreen/> - </div> - ); - } + const teamMembers = this.state.teamMembers; + const users = this.state.users; + const actionUserProps = {}; + const extraInfo = {}; + const mfaEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true' && global.window.mm_config.EnableMultifactorAuthentication === 'true'; + + let usersToDisplay; + if (this.state.loading) { + usersToDisplay = null; + } else { + usersToDisplay = []; + + for (let i = 0; i < users.length; i++) { + const user = users[i]; + + if (teamMembers[user.id]) { + usersToDisplay.push(user); + actionUserProps[user.id] = { + teamMember: teamMembers[user.id] + }; + + const info = []; + + if (user.auth_service) { + const service = (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) ? user.auth_service.toUpperCase() : Utils.toTitleCase(user.auth_service); + info.push( + <FormattedHTMLMessage + id='admin.user_item.authServiceNotEmail' + defaultMessage='<strong>Sign-in Method:</strong> {service}' + values={{ + service + }} + /> + ); + } else { + info.push( + <FormattedHTMLMessage + id='admin.user_item.authServiceEmail' + defaultMessage='<strong>Sign-in Method:</strong> Email' + /> + ); + } - var memberList = this.state.users.map((user) => { - var teamMember = this.getTeamMemberForUser(user.id); + if (mfaEnabled) { + if (user.mfa_active) { + info.push( + <FormattedHTMLMessage + id='admin.user_item.mfaYes' + defaultMessage='<strong>MFA</strong>: Yes' + /> + ); + } else { + info.push( + <FormattedHTMLMessage + id='admin.user_item.mfaNo' + defaultMessage='<strong>MFA</strong>: No' + /> + ); + } + } - if (!teamMember || teamMember.delete_at > 0) { - return null; + extraInfo[user.id] = info; + } } - - return ( - <UserItem - team={this.state.team} - key={'user_' + user.id} - user={user} - teamMember={teamMember} - refreshProfiles={this.getCurrentTeamProfiles} - doPasswordReset={this.doPasswordReset} - />); - }); + } return ( <div className='wrapper--fixed'> @@ -209,7 +233,7 @@ export default class UserList extends React.Component { defaultMessage='Users for {team} ({count})' values={{ team: this.state.team.name, - count: this.state.users.length + count: this.state.total }} /> </h3> @@ -219,7 +243,20 @@ export default class UserList extends React.Component { role='form' > <div className='more-modal__list member-list-holder'> - {memberList} + <SearchableUserList + users={usersToDisplay} + usersPerPage={USERS_PER_PAGE} + total={this.state.total} + extraInfo={extraInfo} + nextPage={this.nextPage} + search={this.search} + actions={[AdminTeamMembersDropdown]} + actionProps={{ + refreshProfiles: this.getCurrentTeamProfiles, + doPasswordReset: this.doPasswordReset + }} + actionUserProps={actionUserProps} + /> </div> </form> <ResetPasswordModal diff --git a/webapp/components/analytics/system_analytics.jsx b/webapp/components/analytics/system_analytics.jsx index 5bd8b1d28..2b4b5b48f 100644 --- a/webapp/components/analytics/system_analytics.jsx +++ b/webapp/components/analytics/system_analytics.jsx @@ -82,6 +82,7 @@ class SystemAnalytics extends React.Component { const stats = this.state.stats; let advancedCounts; + let advancedStats; let advancedGraphs; let banner; if (global.window.mm_license.IsLicensed === 'true') { @@ -130,6 +131,41 @@ class SystemAnalytics extends React.Component { </div> ); + advancedStats = ( + <div className='row'> + <StatisticCount + title={ + <FormattedMessage + id='analytics.system.totalWebsockets' + defaultMessage='Websocket Conns' + /> + } + icon='fa-user' + count={stats[StatTypes.TOTAL_WEBSOCKET_CONNECTIONS]} + /> + <StatisticCount + title={ + <FormattedMessage + id='analytics.system.totalMasterDbConnections' + defaultMessage='Master DB Conns' + /> + } + icon='fa-terminal' + count={stats[StatTypes.TOTAL_MASTER_DB_CONNECTIONS]} + /> + <StatisticCount + title={ + <FormattedMessage + id='analytics.system.totalReadDbConnections' + defaultMessage='Replica DB Conns' + /> + } + icon='fa-terminal' + count={stats[StatTypes.TOTAL_READ_DB_CONNECTIONS]} + /> + </div> + ); + const channelTypeData = formatChannelDoughtnutData(stats[StatTypes.TOTAL_PUBLIC_CHANNELS], stats[StatTypes.TOTAL_PRIVATE_GROUPS], this.props.intl); const postTypeData = formatPostDoughtnutData(stats[StatTypes.TOTAL_FILE_POSTS], stats[StatTypes.TOTAL_HASHTAG_POSTS], stats[StatTypes.TOTAL_POSTS], this.props.intl); @@ -246,6 +282,7 @@ class SystemAnalytics extends React.Component { /> </div> {advancedCounts} + {advancedStats} {advancedGraphs} <div className='row'> <LineChart diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index bd57271ed..1a8625cd2 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -63,13 +63,15 @@ export default class ChannelHeader extends React.Component { } getStateFromStores() { - const extraInfo = ChannelStore.getExtraInfo(this.props.channelId); + const stats = ChannelStore.getStats(this.props.channelId); + + const users = UserStore.getProfileListInChannel(this.props.channelId); return { channel: ChannelStore.get(this.props.channelId), - memberChannel: ChannelStore.getMember(this.props.channelId), - users: extraInfo.members, - userCount: extraInfo.member_count, + memberChannel: ChannelStore.getMyMember(this.props.channelId), + users, + userCount: stats.member_count, currentUser: UserStore.getCurrentUser(), enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true), isBusy: WebrtcStore.isBusy() @@ -89,10 +91,10 @@ export default class ChannelHeader extends React.Component { componentDidMount() { ChannelStore.addChangeListener(this.onListenerChange); - ChannelStore.addExtraInfoChangeListener(this.onListenerChange); + ChannelStore.addStatsChangeListener(this.onListenerChange); SearchStore.addSearchChangeListener(this.onListenerChange); PreferenceStore.addChangeListener(this.onListenerChange); - UserStore.addChangeListener(this.onListenerChange); + UserStore.addInChannelChangeListener(this.onListenerChange); UserStore.addStatusesChangeListener(this.onListenerChange); WebrtcStore.addChangedListener(this.onListenerChange); WebrtcStore.addBusyListener(this.onBusy); @@ -102,10 +104,10 @@ export default class ChannelHeader extends React.Component { componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); - ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); + ChannelStore.removeStatsChangeListener(this.onListenerChange); SearchStore.removeSearchChangeListener(this.onListenerChange); PreferenceStore.removeChangeListener(this.onListenerChange); - UserStore.removeChangeListener(this.onListenerChange); + UserStore.removeInChannelChangeListener(this.onListenerChange); UserStore.removeStatusesChangeListener(this.onListenerChange); WebrtcStore.removeChangedListener(this.onListenerChange); WebrtcStore.removeBusyListener(this.onBusy); @@ -117,10 +119,7 @@ export default class ChannelHeader extends React.Component { } onListenerChange() { - const newState = this.getStateFromStores(); - if (!Utils.areObjectsEqual(newState, this.state)) { - this.setState(newState); - } + this.setState(this.getStateFromStores()); } handleLeave() { @@ -265,7 +264,6 @@ export default class ChannelHeader extends React.Component { </Popover> ); let channelTitle = channel.display_name; - const currentId = this.state.currentUser.id; const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); const isDirect = (this.state.channel.type === 'D'); @@ -273,13 +271,8 @@ export default class ChannelHeader extends React.Component { if (isDirect) { const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; - let contact; - if (this.state.users.length > 1) { - if (this.state.users[0].id === currentId) { - contact = this.state.users[1]; - } else { - contact = this.state.users[0]; - } + const contact = this.state.users[0]; + if (contact) { channelTitle = Utils.displayUsername(contact.id); } diff --git a/webapp/components/channel_invite_button.jsx b/webapp/components/channel_invite_button.jsx index 59eda8e41..290c2bea4 100644 --- a/webapp/components/channel_invite_button.jsx +++ b/webapp/components/channel_invite_button.jsx @@ -1,13 +1,12 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React from 'react'; +import SpinnerButton from 'components/spinner_button.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; -import Client from 'client/web_client.jsx'; +import {addUserToChannel} from 'actions/channel_actions.jsx'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; -import SpinnerButton from 'components/spinner_button.jsx'; export default class ChannelInviteButton extends React.Component { static get propTypes() { @@ -37,7 +36,7 @@ export default class ChannelInviteButton extends React.Component { addingUser: true }); - Client.addChannelMember( + addUserToChannel( this.props.channel.id, this.props.user.id, () => { @@ -46,7 +45,6 @@ export default class ChannelInviteButton extends React.Component { }); this.props.onInviteError(null); - AsyncClient.getChannelExtraInfo(); }, (err) => { this.setState({ diff --git a/webapp/components/channel_invite_modal.jsx b/webapp/components/channel_invite_modal.jsx index c7c1906a5..99a4b9313 100644 --- a/webapp/components/channel_invite_modal.jsx +++ b/webapp/components/channel_invite_modal.jsx @@ -1,124 +1,85 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; import ChannelInviteButton from './channel_invite_button.jsx'; -import FilteredUserList from './filtered_user_list.jsx'; +import SearchableUserList from './searchable_user_list.jsx'; import LoadingScreen from './loading_screen.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import {searchUsers} from 'actions/user_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; -import {FormattedMessage} from 'react-intl'; - +import React from 'react'; import {Modal} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; -import React from 'react'; +const USERS_PER_PAGE = 50; export default class ChannelInviteModal extends React.Component { constructor(props) { super(props); - this.onListenerChange = this.onListenerChange.bind(this); - this.getStateFromStores = this.getStateFromStores.bind(this); + this.onChange = this.onChange.bind(this); this.handleInviteError = this.handleInviteError.bind(this); + this.nextPage = this.nextPage.bind(this); + this.search = this.search.bind(this); - this.state = this.getStateFromStores(); - } - shouldComponentUpdate(nextProps, nextState) { - if (!this.props.show && !nextProps.show) { - return false; - } - - if (!Utils.areObjectsEqual(this.props, nextProps)) { - return true; - } - - if (!Utils.areObjectsEqual(this.state, nextState)) { - return true; - } - - return false; - } - getStateFromStores() { - const users = UserStore.getActiveOnlyProfiles(); - - if ($.isEmptyObject(users)) { - return { - loading: true - }; - } - - // make sure we have all members of this channel before rendering - const extraInfo = ChannelStore.getCurrentExtraInfo(); - if (extraInfo.member_count !== extraInfo.members.length) { - AsyncClient.getChannelExtraInfo(this.props.channel.id, -1); - - return { - loading: true - }; - } - - const currentUser = UserStore.getCurrentUser(); - if (!currentUser) { - return { - loading: true - }; - } - - const currentMember = ChannelStore.getCurrentMember(); - if (!currentMember) { - return { - loading: true - }; - } + this.term = ''; - const memberIds = extraInfo.members.map((user) => user.id); + const channelStats = ChannelStore.getStats(props.channel.id); + const teamStats = TeamStore.getCurrentStats(); - var nonmembers = []; - for (var id in users) { - if (memberIds.indexOf(id) === -1) { - nonmembers.push(users[id]); - } - } - - nonmembers.sort((a, b) => { - return a.username.localeCompare(b.username); - }); - - return { - nonmembers, - loading: false, - currentUser, - currentMember + this.state = { + users: [], + total: teamStats.member_count - channelStats.member_count, + search: false }; } + componentWillReceiveProps(nextProps) { if (!this.props.show && nextProps.show) { - ChannelStore.addExtraInfoChangeListener(this.onListenerChange); - ChannelStore.addChangeListener(this.onListenerChange); - UserStore.addChangeListener(this.onListenerChange); - this.onListenerChange(); + TeamStore.addStatsChangeListener(this.onChange); + ChannelStore.addStatsChangeListener(this.onChange); + UserStore.addNotInChannelChangeListener(this.onChange); + UserStore.addStatusesChangeListener(this.onChange); + + this.onChange(); + AsyncClient.getProfilesNotInChannel(this.props.channel.id, 0); + AsyncClient.getTeamStats(TeamStore.getCurrentId()); } else if (this.props.show && !nextProps.show) { - ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); - ChannelStore.removeChangeListener(this.onListenerChange); - UserStore.removeChangeListener(this.onListenerChange); + TeamStore.removeStatsChangeListener(this.onChange); + ChannelStore.removeStatsChangeListener(this.onChange); + UserStore.removeNotInChannelChangeListener(this.onChange); + UserStore.removeStatusesChangeListener(this.onChange); } } + componentWillUnmount() { - ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); - ChannelStore.removeChangeListener(this.onListenerChange); - UserStore.removeChangeListener(this.onListenerChange); + ChannelStore.removeStatsChangeListener(this.onChange); + ChannelStore.removeChangeListener(this.onChange); + UserStore.removeNotInChannelChangeListener(this.onChange); } - onListenerChange() { - var newState = this.getStateFromStores(); - if (!Utils.areObjectsEqual(this.state, newState)) { - this.setState(newState); + + onChange() { + if (this.state.search) { + this.search(this.term); + return; } + + const channelStats = ChannelStore.getStats(this.props.channel.id); + const teamStats = TeamStore.getCurrentStats(); + + this.setState({ + users: UserStore.getProfileListNotInChannel(this.props.channel.id), + total: teamStats.member_count - channelStats.member_count + }); } + handleInviteError(err) { if (err) { this.setState({ @@ -130,6 +91,29 @@ export default class ChannelInviteModal extends React.Component { }); } } + + nextPage(page) { + AsyncClient.getProfilesNotInChannel(this.props.channel.id, (page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); + } + + search(term) { + this.term = term; + + if (term === '') { + this.setState({users: UserStore.getProfileListNotInChannel(), search: false}); + return; + } + + searchUsers( + term, + TeamStore.getCurrentId(), + {not_in_channel: this.props.channel.id}, + (users) => { + this.setState({search: true, users}); + } + ); + } + render() { var inviteError = null; if (this.state.inviteError) { @@ -145,9 +129,13 @@ export default class ChannelInviteModal extends React.Component { maxHeight = Utils.windowHeight() - 300; } content = ( - <FilteredUserList + <SearchableUserList style={{maxHeight}} - users={this.state.nonmembers} + users={this.state.users} + usersPerPage={USERS_PER_PAGE} + total={this.state.total} + nextPage={this.nextPage} + search={this.search} actions={[ChannelInviteButton]} actionProps={{ channel: this.props.channel, diff --git a/webapp/components/channel_members_modal.jsx b/webapp/components/channel_members_modal.jsx index d20c00623..511209b42 100644 --- a/webapp/components/channel_members_modal.jsx +++ b/webapp/components/channel_members_modal.jsx @@ -1,122 +1,89 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import FilteredUserList from './filtered_user_list.jsx'; +import SearchableUserList from './searchable_user_list.jsx'; import LoadingScreen from './loading_screen.jsx'; import ChannelInviteModal from './channel_invite_modal.jsx'; import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import {searchUsers} from 'actions/user_actions.jsx'; +import {removeUserFromChannel} from 'actions/channel_actions.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; -import Client from 'client/web_client.jsx'; import * as Utils from 'utils/utils.jsx'; -import {FormattedMessage} from 'react-intl'; - +import React from 'react'; import {Modal} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; -import React from 'react'; +const USERS_PER_PAGE = 50; export default class ChannelMembersModal extends React.Component { constructor(props) { super(props); - this.getStateFromStores = this.getStateFromStores.bind(this); this.onChange = this.onChange.bind(this); this.handleRemove = this.handleRemove.bind(this); - this.createRemoveMemberButton = this.createRemoveMemberButton.bind(this); + this.search = this.search.bind(this); + this.nextPage = this.nextPage.bind(this); - // the rest of the state gets populated when the modal is shown - this.state = { - showInviteModal: false - }; - } - shouldComponentUpdate(nextProps, nextState) { - if (!Utils.areObjectsEqual(this.props, nextProps)) { - return true; - } + this.term = ''; - if (!Utils.areObjectsEqual(this.state, nextState)) { - return true; - } - - return false; - } - getStateFromStores() { - const extraInfo = ChannelStore.getCurrentExtraInfo(); - const profiles = UserStore.getActiveOnlyProfiles(); - - if (extraInfo.member_count !== extraInfo.members.length) { - AsyncClient.getChannelExtraInfo(this.props.channel.id, -1); - - return { - loading: true - }; - } - - const memberList = extraInfo.members.map((member) => { - return profiles[member.id]; - }); - - function compareByUsername(a, b) { - if (a.username < b.username) { - return -1; - } else if (a.username > b.username) { - return 1; - } + const stats = ChannelStore.getStats(props.channel.id); - return 0; - } - - memberList.sort(compareByUsername); - - return { - memberList, - loading: false + this.state = { + users: [], + total: stats.member_count, + showInviteModal: false, + search: false }; } + componentWillReceiveProps(nextProps) { if (!this.props.show && nextProps.show) { - ChannelStore.addExtraInfoChangeListener(this.onChange); - ChannelStore.addChangeListener(this.onChange); + ChannelStore.addStatsChangeListener(this.onChange); + UserStore.addInChannelChangeListener(this.onChange); + UserStore.addStatusesChangeListener(this.onChange); this.onChange(); + AsyncClient.getProfilesInChannel(this.props.channel.id, 0); } else if (this.props.show && !nextProps.show) { - ChannelStore.removeExtraInfoChangeListener(this.onChange); - ChannelStore.removeChangeListener(this.onChange); + ChannelStore.removeStatsChangeListener(this.onChange); + UserStore.removeInChannelChangeListener(this.onChange); + UserStore.removeStatusesChangeListener(this.onChange); } } + onChange() { - const newState = this.getStateFromStores(); - if (!Utils.areObjectsEqual(this.state, newState)) { - this.setState(newState); + if (this.state.search) { + this.search(this.term); + return; } + + const stats = ChannelStore.getStats(this.props.channel.id); + this.setState({ + users: UserStore.getProfileListInChannel(this.props.channel.id), + total: stats.member_count + }); } + handleRemove(user) { const userId = user.id; - Client.removeChannelMember( - ChannelStore.getCurrentId(), + removeUserFromChannel( + this.props.channel.id, userId, - () => { - const memberList = this.state.memberList.slice(); - for (let i = 0; i < memberList.length; i++) { - if (userId === memberList[i].id) { - memberList.splice(i, 1); - break; - } - } - - this.setState({memberList}); - AsyncClient.getChannelExtraInfo(); - }, + null, (err) => { this.setState({inviteError: err.message}); } ); } + createRemoveMemberButton({user}) { if (user.id === UserStore.getCurrentId()) { return null; @@ -135,6 +102,29 @@ export default class ChannelMembersModal extends React.Component { </button> ); } + + nextPage(page) { + AsyncClient.getProfilesInChannel(this.props.channel.id, (page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); + } + + search(term) { + this.term = term; + + if (term === '') { + this.setState({users: UserStore.getProfileListInChannel(this.props.channel.id), search: false}); + return; + } + + searchUsers( + term, + TeamStore.getCurrentId(), + {in_channel: this.props.channel.id}, + (users) => { + this.setState({search: true, users}); + } + ); + } + render() { let content; if (this.state.loading) { @@ -151,9 +141,13 @@ export default class ChannelMembersModal extends React.Component { } content = ( - <FilteredUserList + <SearchableUserList style={{maxHeight}} - users={this.state.memberList} + users={this.state.users} + usersPerPage={USERS_PER_PAGE} + total={this.state.total} + nextPage={this.nextPage} + search={this.search} actions={removeButton} /> ); diff --git a/webapp/components/channel_notifications_modal.jsx b/webapp/components/channel_notifications_modal.jsx index 35a2e4087..91563a096 100644 --- a/webapp/components/channel_notifications_modal.jsx +++ b/webapp/components/channel_notifications_modal.jsx @@ -65,9 +65,9 @@ export default class ChannelNotificationsModal extends React.Component { Client.updateChannelNotifyProps(data, () => { // YUCK - var member = ChannelStore.getMember(channelId); + var member = ChannelStore.getMyMember(channelId); member.notify_props.desktop = notifyLevel; - ChannelStore.setChannelMember(member); + ChannelStore.storeMyChannelMember(member); this.updateSection(''); }, (err) => { @@ -256,13 +256,13 @@ export default class ChannelNotificationsModal extends React.Component { mark_unread: markUnreadLevel }; - //TODO: This should be fixed, moved to event_helpers + //TODO: This should be fixed, moved to actions Client.updateChannelNotifyProps(data, () => { // Yuck... - var member = ChannelStore.getMember(channelId); + var member = ChannelStore.getMyMember(channelId); member.notify_props.mark_unread = markUnreadLevel; - ChannelStore.setChannelMember(member); + ChannelStore.storeMyChannelMember(member); this.updateSection(''); }, (err) => { diff --git a/webapp/components/channel_switch_modal.jsx b/webapp/components/channel_switch_modal.jsx index ec257bab5..7d15a9c45 100644 --- a/webapp/components/channel_switch_modal.jsx +++ b/webapp/components/channel_switch_modal.jsx @@ -8,12 +8,13 @@ import SwitchChannelProvider from './suggestion/switch_channel_provider.jsx'; import {FormattedMessage} from 'react-intl'; import {Modal} from 'react-bootstrap'; +import {goToChannel, openDirectChannelToUser} from 'actions/channel_actions.jsx'; + import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; import Constants from 'utils/constants.jsx'; import * as Utils from 'utils/utils.jsx'; -import * as ChannelActions from 'actions/channel_actions.jsx'; import React from 'react'; import $ from 'jquery'; @@ -27,30 +28,14 @@ export default class SwitchChannelModal extends React.Component { this.onExited = this.onExited.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.handleSubmit = this.handleSubmit.bind(this); - this.handleDmUserChange = this.handleDmUserChange.bind(this); this.suggestionProviders = [new SwitchChannelProvider()]; this.state = { - dmUsers: UserStore.getDirectProfiles(), text: '', error: '' }; } - componentDidMount() { - UserStore.addDmListChangeListener(this.handleDmUserChange); - } - - componentWillUnmount() { - UserStore.removeDmListChangeListener(this.handleDmUserChange); - } - - handleDmUserChange() { - this.setState({ - dmUsers: UserStore.getDirectProfiles() - }); - } - componentDidUpdate(prevProps) { if (this.props.show && !prevProps.show) { const textbox = this.refs.search.getTextbox(); @@ -97,18 +82,13 @@ export default class SwitchChannelModal extends React.Component { const name = this.state.text.trim(); let channel = null; + // TODO: Replace this hack with something reasonable if (name.indexOf(Utils.localizeMessage('channel_switch_modal.dm', '(Direct Message)')) > 0) { const dmUsername = name.substr(0, name.indexOf(Utils.localizeMessage('channel_switch_modal.dm', '(Direct Message)')) - 1); - let user = null; - for (const id in this.state.dmUsers) { - if (this.state.dmUsers[id].username === dmUsername) { - user = this.state.dmUsers[id]; - break; - } - } + const user = UserStore.getProfileByUsername(dmUsername); if (user) { - Utils.openDirectChannelToUser( + openDirectChannelToUser( user, (ch) => { channel = ch; @@ -123,7 +103,7 @@ export default class SwitchChannelModal extends React.Component { } if (channel !== null) { - ChannelActions.goToChannel(channel); + goToChannel(channel); this.onHide(); } else if (this.state.text !== '') { this.setState({ diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx index 44050bb12..263fd31c2 100644 --- a/webapp/components/edit_post_modal.jsx +++ b/webapp/components/edit_post_modal.jsx @@ -1,26 +1,27 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; -import ReactDOM from 'react-dom'; -import Client from 'client/web_client.jsx'; -import * as UserAgent from 'utils/user_agent.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; -import * as GlobalActions from 'actions/global_actions.jsx'; import Textbox from './textbox.jsx'; + import BrowserStore from 'stores/browser_store.jsx'; import PostStore from 'stores/post_store.jsx'; import MessageHistoryStore from 'stores/message_history_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import Constants from 'utils/constants.jsx'; -import {FormattedMessage} from 'react-intl'; +import * as GlobalActions from 'actions/global_actions.jsx'; +import {loadPosts} from 'actions/post_actions.jsx'; -var KeyCodes = Constants.KeyCodes; +import Client from 'client/web_client.jsx'; +import * as UserAgent from 'utils/user_agent.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.jsx'; +const KeyCodes = Constants.KeyCodes; +import $ from 'jquery'; import React from 'react'; +import ReactDOM from 'react-dom'; +import {FormattedMessage} from 'react-intl'; export default class EditPostModal extends React.Component { constructor(props) { @@ -77,7 +78,7 @@ export default class EditPostModal extends React.Component { Client.updatePost( updatedPost, () => { - AsyncClient.getPosts(updatedPost.channel_id); + loadPosts(updatedPost.channel_id); window.scrollTo(0, 0); }, (err) => { diff --git a/webapp/components/emoji/components/emoji_list.jsx b/webapp/components/emoji/components/emoji_list.jsx index 340fc6afc..76c509f12 100644 --- a/webapp/components/emoji/components/emoji_list.jsx +++ b/webapp/components/emoji/components/emoji_list.jsx @@ -1,16 +1,20 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React from 'react'; +import EmojiListItem from './emoji_list_item.jsx'; +import LoadingScreen from 'components/loading_screen.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; import EmojiStore from 'stores/emoji_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import {loadEmoji} from 'actions/emoji_actions.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; import * as Utils from 'utils/utils.jsx'; -import {FormattedMessage} from 'react-intl'; -import EmojiListItem from './emoji_list_item.jsx'; +import React from 'react'; import {Link} from 'react-router'; -import LoadingScreen from 'components/loading_screen.jsx'; +import {FormattedMessage} from 'react-intl'; export default class EmojiList extends React.Component { static get propTypes() { @@ -24,28 +28,30 @@ export default class EmojiList extends React.Component { super(props); this.handleEmojiChange = this.handleEmojiChange.bind(this); - + this.handleUserChange = this.handleUserChange.bind(this); this.deleteEmoji = this.deleteEmoji.bind(this); - this.updateFilter = this.updateFilter.bind(this); this.state = { emojis: EmojiStore.getCustomEmojiMap(), loading: !EmojiStore.hasReceivedCustomEmojis(), - filter: '' + filter: '', + users: UserStore.getProfiles() }; } componentDidMount() { EmojiStore.addChangeListener(this.handleEmojiChange); + UserStore.addChangeListener(this.handleUserChange); if (window.mm_config.EnableCustomEmoji === 'true') { - AsyncClient.listEmoji(); + loadEmoji(); } } componentWillUnmount() { EmojiStore.removeChangeListener(this.handleEmojiChange); + UserStore.removeChangeListener(this.handleUserChange); } handleEmojiChange() { @@ -55,6 +61,10 @@ export default class EmojiList extends React.Component { }); } + handleUserChange() { + this.setState({users: UserStore.getProfiles()}); + } + updateFilter(e) { this.setState({ filter: e.target.value @@ -98,6 +108,7 @@ export default class EmojiList extends React.Component { emoji={emoji} onDelete={onDelete} filter={filter} + creator={this.state.users[emoji.creator_id] || {}} /> ); } diff --git a/webapp/components/emoji/components/emoji_list_item.jsx b/webapp/components/emoji/components/emoji_list_item.jsx index 0428f0286..dc27f3691 100644 --- a/webapp/components/emoji/components/emoji_list_item.jsx +++ b/webapp/components/emoji/components/emoji_list_item.jsx @@ -4,7 +4,7 @@ import React from 'react'; import EmojiStore from 'stores/emoji_store.jsx'; -import UserStore from 'stores/user_store.jsx'; + import * as Utils from 'utils/utils.jsx'; import {FormattedMessage} from 'react-intl'; @@ -14,7 +14,8 @@ export default class EmojiListItem extends React.Component { return { emoji: React.PropTypes.object.isRequired, onDelete: React.PropTypes.func.isRequired, - filter: React.PropTypes.string + filter: React.PropTypes.string, + creator: React.PropTypes.object.isRequired }; } @@ -22,10 +23,6 @@ export default class EmojiListItem extends React.Component { super(props); this.handleDelete = this.handleDelete.bind(this); - - this.state = { - creator: UserStore.getProfile(this.props.emoji.creator_id) - }; } handleDelete(e) { @@ -57,7 +54,7 @@ export default class EmojiListItem extends React.Component { render() { const emoji = this.props.emoji; - const creator = this.state.creator; + const creator = this.props.creator; const filter = this.props.filter ? this.props.filter.toLowerCase() : ''; if (!this.matchesFilter(emoji, creator, filter)) { diff --git a/webapp/components/integrations/components/installed_command.jsx b/webapp/components/integrations/components/installed_command.jsx index 658126f19..f149a21ac 100644 --- a/webapp/components/integrations/components/installed_command.jsx +++ b/webapp/components/integrations/components/installed_command.jsx @@ -2,9 +2,6 @@ // See License.txt for license information. import React from 'react'; - -import * as Utils from 'utils/utils.jsx'; - import {FormattedMessage} from 'react-intl'; export default class InstalledCommand extends React.Component { @@ -13,7 +10,8 @@ export default class InstalledCommand extends React.Component { command: React.PropTypes.object.isRequired, onRegenToken: React.PropTypes.func.isRequired, onDelete: React.PropTypes.func.isRequired, - filter: React.PropTypes.string + filter: React.PropTypes.string, + creator: React.PropTypes.object.isRequired }; } @@ -113,7 +111,7 @@ export default class InstalledCommand extends React.Component { id='installed_integrations.creation' defaultMessage='Created by {creator} on {createAt, date, full}' values={{ - creator: Utils.displayUsername(command.creator_id), + creator: this.props.creator.username, createAt: command.create_at }} /> diff --git a/webapp/components/integrations/components/installed_commands.jsx b/webapp/components/integrations/components/installed_commands.jsx index f6429c33e..1c5ef9000 100644 --- a/webapp/components/integrations/components/installed_commands.jsx +++ b/webapp/components/integrations/components/installed_commands.jsx @@ -1,16 +1,20 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React from 'react'; +import BackstageList from 'components/backstage/components/backstage_list.jsx'; +import InstalledCommand from './installed_command.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; import IntegrationStore from 'stores/integration_store.jsx'; import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import {loadTeamCommands} from 'actions/integration_actions.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; import * as Utils from 'utils/utils.jsx'; -import BackstageList from 'components/backstage/components/backstage_list.jsx'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; -import InstalledCommand from './installed_command.jsx'; export default class InstalledCommands extends React.Component { static get propTypes() { @@ -23,7 +27,7 @@ export default class InstalledCommands extends React.Component { super(props); this.handleIntegrationChange = this.handleIntegrationChange.bind(this); - + this.handleUserChange = this.handleUserChange.bind(this); this.regenCommandToken = this.regenCommandToken.bind(this); this.deleteCommand = this.deleteCommand.bind(this); @@ -31,20 +35,23 @@ export default class InstalledCommands extends React.Component { this.state = { commands: IntegrationStore.getCommands(teamId), - loading: !IntegrationStore.hasReceivedCommands(teamId) + loading: !IntegrationStore.hasReceivedCommands(teamId), + users: UserStore.getProfiles() }; } componentDidMount() { IntegrationStore.addChangeListener(this.handleIntegrationChange); + UserStore.addChangeListener(this.handleUserChange); if (window.mm_config.EnableCommands === 'true') { - AsyncClient.listTeamCommands(); + loadTeamCommands(); } } componentWillUnmount() { IntegrationStore.removeChangeListener(this.handleIntegrationChange); + UserStore.removeChangeListener(this.handleUserChange); } handleIntegrationChange() { @@ -56,6 +63,10 @@ export default class InstalledCommands extends React.Component { }); } + handleUserChange() { + this.setState({users: UserStore.getProfiles()}); + } + regenCommandToken(command) { AsyncClient.regenCommandToken(command.id); } @@ -72,6 +83,7 @@ export default class InstalledCommands extends React.Component { command={command} onRegenToken={this.regenCommandToken} onDelete={this.deleteCommand} + creator={this.state.users[command.creator_id] || {}} /> ); }); diff --git a/webapp/components/integrations/components/installed_incoming_webhook.jsx b/webapp/components/integrations/components/installed_incoming_webhook.jsx index 2b514d5ec..86274c3d6 100644 --- a/webapp/components/integrations/components/installed_incoming_webhook.jsx +++ b/webapp/components/integrations/components/installed_incoming_webhook.jsx @@ -13,7 +13,8 @@ export default class InstalledIncomingWebhook extends React.Component { return { incomingWebhook: React.PropTypes.object.isRequired, onDelete: React.PropTypes.func.isRequired, - filter: React.PropTypes.string + filter: React.PropTypes.string, + creator: React.PropTypes.object.isRequired }; } @@ -108,7 +109,7 @@ export default class InstalledIncomingWebhook extends React.Component { id='installed_integrations.creation' defaultMessage='Created by {creator} on {createAt, date, full}' values={{ - creator: Utils.displayUsername(incomingWebhook.user_id), + creator: this.props.creator.username, createAt: incomingWebhook.create_at }} /> diff --git a/webapp/components/integrations/components/installed_incoming_webhooks.jsx b/webapp/components/integrations/components/installed_incoming_webhooks.jsx index b14d1e3e8..243195b8b 100644 --- a/webapp/components/integrations/components/installed_incoming_webhooks.jsx +++ b/webapp/components/integrations/components/installed_incoming_webhooks.jsx @@ -1,16 +1,20 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React from 'react'; +import BackstageList from 'components/backstage/components/backstage_list.jsx'; +import InstalledIncomingWebhook from './installed_incoming_webhook.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; import IntegrationStore from 'stores/integration_store.jsx'; import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import {loadIncomingHooks} from 'actions/integration_actions.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; import * as Utils from 'utils/utils.jsx'; -import BackstageList from 'components/backstage/components/backstage_list.jsx'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; -import InstalledIncomingWebhook from './installed_incoming_webhook.jsx'; export default class InstalledIncomingWebhooks extends React.Component { static get propTypes() { @@ -23,27 +27,30 @@ export default class InstalledIncomingWebhooks extends React.Component { super(props); this.handleIntegrationChange = this.handleIntegrationChange.bind(this); - + this.handleUserChange = this.handleUserChange.bind(this); this.deleteIncomingWebhook = this.deleteIncomingWebhook.bind(this); const teamId = TeamStore.getCurrentId(); this.state = { incomingWebhooks: IntegrationStore.getIncomingWebhooks(teamId), - loading: !IntegrationStore.hasReceivedIncomingWebhooks(teamId) + loading: !IntegrationStore.hasReceivedIncomingWebhooks(teamId), + users: UserStore.getProfiles() }; } componentDidMount() { IntegrationStore.addChangeListener(this.handleIntegrationChange); + UserStore.addChangeListener(this.handleUserChange); if (window.mm_config.EnableIncomingWebhooks === 'true') { - AsyncClient.listIncomingHooks(); + loadIncomingHooks(); } } componentWillUnmount() { IntegrationStore.removeChangeListener(this.handleIntegrationChange); + UserStore.removeChangeListener(this.handleUserChange); } handleIntegrationChange() { @@ -55,6 +62,12 @@ export default class InstalledIncomingWebhooks extends React.Component { }); } + handleUserChange() { + this.setState({ + users: UserStore.getProfiles() + }); + } + deleteIncomingWebhook(incomingWebhook) { AsyncClient.deleteIncomingHook(incomingWebhook.id); } @@ -66,6 +79,7 @@ export default class InstalledIncomingWebhooks extends React.Component { key={incomingWebhook.id} incomingWebhook={incomingWebhook} onDelete={this.deleteIncomingWebhook} + creator={this.state.users[incomingWebhook.user_id] || {}} /> ); }); diff --git a/webapp/components/integrations/components/installed_outgoing_webhook.jsx b/webapp/components/integrations/components/installed_outgoing_webhook.jsx index 664439843..3ff2c01a4 100644 --- a/webapp/components/integrations/components/installed_outgoing_webhook.jsx +++ b/webapp/components/integrations/components/installed_outgoing_webhook.jsx @@ -4,7 +4,6 @@ import React from 'react'; import ChannelStore from 'stores/channel_store.jsx'; -import * as Utils from 'utils/utils.jsx'; import {FormattedMessage} from 'react-intl'; @@ -14,7 +13,8 @@ export default class InstalledOutgoingWebhook extends React.Component { outgoingWebhook: React.PropTypes.object.isRequired, onRegenToken: React.PropTypes.func.isRequired, onDelete: React.PropTypes.func.isRequired, - filter: React.PropTypes.string + filter: React.PropTypes.string, + creator: React.PropTypes.object.isRequired }; } @@ -195,7 +195,7 @@ export default class InstalledOutgoingWebhook extends React.Component { id='installed_integrations.creation' defaultMessage='Created by {creator} on {createAt, date, full}' values={{ - creator: Utils.displayUsername(outgoingWebhook.creator_id), + creator: this.props.creator.username, createAt: outgoingWebhook.create_at }} /> diff --git a/webapp/components/integrations/components/installed_outgoing_webhooks.jsx b/webapp/components/integrations/components/installed_outgoing_webhooks.jsx index 214e60a48..21176f8b7 100644 --- a/webapp/components/integrations/components/installed_outgoing_webhooks.jsx +++ b/webapp/components/integrations/components/installed_outgoing_webhooks.jsx @@ -1,16 +1,20 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React from 'react'; +import BackstageList from 'components/backstage/components/backstage_list.jsx'; +import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; import IntegrationStore from 'stores/integration_store.jsx'; import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import {loadOutgoingHooks} from 'actions/integration_actions.jsx'; + import * as Utils from 'utils/utils.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; -import BackstageList from 'components/backstage/components/backstage_list.jsx'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; -import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx'; export default class InstalledOutgoingWebhooks extends React.Component { static get propTypes() { @@ -23,7 +27,7 @@ export default class InstalledOutgoingWebhooks extends React.Component { super(props); this.handleIntegrationChange = this.handleIntegrationChange.bind(this); - + this.handleUserChange = this.handleUserChange.bind(this); this.regenOutgoingWebhookToken = this.regenOutgoingWebhookToken.bind(this); this.deleteOutgoingWebhook = this.deleteOutgoingWebhook.bind(this); @@ -31,20 +35,23 @@ export default class InstalledOutgoingWebhooks extends React.Component { this.state = { outgoingWebhooks: IntegrationStore.getOutgoingWebhooks(teamId), - loading: !IntegrationStore.hasReceivedOutgoingWebhooks(teamId) + loading: !IntegrationStore.hasReceivedOutgoingWebhooks(teamId), + users: UserStore.getProfiles() }; } componentDidMount() { IntegrationStore.addChangeListener(this.handleIntegrationChange); + UserStore.addChangeListener(this.handleUserChange); if (window.mm_config.EnableOutgoingWebhooks === 'true') { - AsyncClient.listOutgoingHooks(); + loadOutgoingHooks(); } } componentWillUnmount() { IntegrationStore.removeChangeListener(this.handleIntegrationChange); + UserStore.removeChangeListener(this.handleUserChange); } handleIntegrationChange() { @@ -56,6 +63,10 @@ export default class InstalledOutgoingWebhooks extends React.Component { }); } + handleUserChange() { + this.setState({users: UserStore.getProfiles()}); + } + regenOutgoingWebhookToken(outgoingWebhook) { AsyncClient.regenOutgoingHookToken(outgoingWebhook.id); } @@ -72,6 +83,7 @@ export default class InstalledOutgoingWebhooks extends React.Component { outgoingWebhook={outgoingWebhook} onRegenToken={this.regenOutgoingWebhookToken} onDelete={this.deleteOutgoingWebhook} + creator={this.state.users[outgoingWebhook.creator_id] || {}} /> ); }); diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx index 3b712ffe2..824e7b91d 100644 --- a/webapp/components/logged_in.jsx +++ b/webapp/components/logged_in.jsx @@ -1,21 +1,24 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; import LoadingScreen from 'components/loading_screen.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; + import UserStore from 'stores/user_store.jsx'; import BrowserStore from 'stores/browser_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; -import * as Utils from 'utils/utils.jsx'; + import * as GlobalActions from 'actions/global_actions.jsx'; import * as WebSocketActions from 'actions/websocket_actions.jsx'; +import {loadEmoji} from 'actions/emoji_actions.jsx'; + +import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; import {browserHistory} from 'react-router/es6'; const BACKSPACE_CHAR = 8; +import $ from 'jquery'; import React from 'react'; // import the EmojiStore so that it'll register to receive the results of the listEmojis call further down @@ -148,7 +151,7 @@ export default class LoggedIn extends React.Component { // Get custom emoji from the server if (window.mm_config.EnableCustomEmoji === 'true') { - AsyncClient.listEmoji(); + loadEmoji(false); } } diff --git a/webapp/components/member_list_team.jsx b/webapp/components/member_list_team.jsx index 9f18fba33..a3e43af28 100644 --- a/webapp/components/member_list_team.jsx +++ b/webapp/components/member_list_team.jsx @@ -1,62 +1,94 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import FilteredUserList from './filtered_user_list.jsx'; -import TeamMembersDropdown from './team_members_dropdown.jsx'; +import SearchableUserList from 'components/searchable_user_list.jsx'; +import TeamMembersDropdown from 'components/team_members_dropdown.jsx'; + import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; + +import {searchUsers, loadProfilesAndTeamMembers, loadTeamMembersForProfilesList} from 'actions/user_actions.jsx'; +import {getTeamStats} from 'utils/async_client.jsx'; + +import Constants from 'utils/constants.jsx'; import React from 'react'; +const USERS_PER_PAGE = 50; + export default class MemberListTeam extends React.Component { constructor(props) { super(props); - this.getUsers = this.getUsers.bind(this); this.onChange = this.onChange.bind(this); - this.onTeamChange = this.onTeamChange.bind(this); + this.onStatsChange = this.onStatsChange.bind(this); + this.search = this.search.bind(this); + this.loadComplete = this.loadComplete.bind(this); + + const stats = TeamStore.getCurrentStats(); this.state = { - users: this.getUsers(), - teamMembers: TeamStore.getMembersForTeam() + users: UserStore.getProfileListInTeam(), + teamMembers: Object.assign([], TeamStore.getMembersInTeam()), + total: stats.member_count, + search: false, + loading: true }; } componentDidMount() { - UserStore.addChangeListener(this.onChange); - TeamStore.addChangeListener(this.onTeamChange); - AsyncClient.getTeamMembers(TeamStore.getCurrentId()); + UserStore.addInTeamChangeListener(this.onChange); + UserStore.addStatusesChangeListener(this.onChange); + TeamStore.addChangeListener(this.onChange); + TeamStore.addStatsChangeListener(this.onStatsChange); + + loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, TeamStore.getCurrentId(), this.loadComplete); + getTeamStats(TeamStore.getCurrentId()); } componentWillUnmount() { - UserStore.removeChangeListener(this.onChange); - TeamStore.removeChangeListener(this.onTeamChange); + UserStore.removeInTeamChangeListener(this.onChange); + UserStore.removeStatusesChangeListener(this.onChange); + TeamStore.removeChangeListener(this.onChange); + TeamStore.removeStatsChangeListener(this.onStatsChange); } - getUsers() { - const profiles = UserStore.getProfiles(); - const users = []; + loadComplete() { + this.setState({loading: false}); + } - for (const id of Object.keys(profiles)) { - users.push(profiles[id]); + onChange() { + if (!this.state.search) { + this.setState({users: UserStore.getProfileListInTeam()}); } - users.sort((a, b) => a.username.localeCompare(b.username)); + this.setState({teamMembers: Object.assign([], TeamStore.getMembersInTeam())}); + } - return users; + onStatsChange() { + const stats = TeamStore.getCurrentStats(); + this.setState({total: stats.member_count}); } - onChange() { - this.setState({ - users: this.getUsers() - }); + nextPage(page) { + loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); } - onTeamChange() { - this.setState({ - teamMembers: TeamStore.getMembersForTeam() - }); + search(term) { + if (term === '') { + this.setState({search: false, users: UserStore.getProfileListInTeam()}); + return; + } + + searchUsers( + term, + TeamStore.getCurrentId(), + {}, + (users) => { + this.setState({loading: true, search: true, users}); + loadTeamMembersForProfilesList(users, TeamStore.getCurrentId(), this.loadComplete); + } + ); } render() { @@ -65,12 +97,38 @@ export default class MemberListTeam extends React.Component { teamMembersDropdown = [TeamMembersDropdown]; } + const teamMembers = this.state.teamMembers; + const users = this.state.users; + const actionUserProps = {}; + + let usersToDisplay; + if (this.state.loading) { + usersToDisplay = null; + } else { + usersToDisplay = []; + + for (let i = 0; i < users.length; i++) { + const user = users[i]; + + if (teamMembers[user.id]) { + usersToDisplay.push(user); + actionUserProps[user.id] = { + teamMember: teamMembers[user.id] + }; + } + } + } + return ( - <FilteredUserList + <SearchableUserList style={this.props.style} - users={this.state.users} - teamMembers={this.state.teamMembers} + users={usersToDisplay} + usersPerPage={USERS_PER_PAGE} + total={this.state.total} + nextPage={this.nextPage} + search={this.search} actions={teamMembersDropdown} + actionUserProps={actionUserProps} /> ); } diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx index 24718387e..11849f718 100644 --- a/webapp/components/more_direct_channels.jsx +++ b/webapp/components/more_direct_channels.jsx @@ -1,73 +1,67 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import FilteredUserList from 'components/filtered_user_list.jsx'; +import SearchableUserList from 'components/searchable_user_list.jsx'; import SpinnerButton from 'components/spinner_button.jsx'; -import LoadingScreen from 'components/loading_screen.jsx'; -import {getMoreDmList} from 'actions/user_actions.jsx'; +import {searchUsers} from 'actions/user_actions.jsx'; +import {openDirectChannelToUser} from 'actions/channel_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 * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.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; + export default class MoreDirectChannels extends React.Component { constructor(props) { super(props); this.handleHide = this.handleHide.bind(this); this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this); - this.handleUserChange = this.handleUserChange.bind(this); - this.onTeamChange = this.onTeamChange.bind(this); + this.onChange = this.onChange.bind(this); this.createJoinDirectChannelButton = this.createJoinDirectChannelButton.bind(this); + this.toggleList = this.toggleList.bind(this); + this.nextPage = this.nextPage.bind(this); + this.search = this.search.bind(this); + this.loadComplete = this.loadComplete.bind(this); this.state = { - users: UserStore.getProfilesForDmList(), - teamMembers: TeamStore.getMembersForTeam(), + users: UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true), loadingDMChannel: -1, - usersLoaded: false, - teamMembersLoaded: false + listType: 'team', + loading: false, + search: false }; } componentDidMount() { - UserStore.addDmListChangeListener(this.handleUserChange); - TeamStore.addChangeListener(this.onTeamChange); + UserStore.addChangeListener(this.onChange); + UserStore.addInTeamChangeListener(this.onChange); + UserStore.addStatusesChangeListener(this.onChange); + TeamStore.addChangeListener(this.onChange); + + AsyncClient.getProfiles(0, Constants.PROFILE_CHUNK_SIZE); + AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), 0, Constants.PROFILE_CHUNK_SIZE); } componentWillUnmount() { - UserStore.removeDmListChangeListener(this.handleUserChange); - TeamStore.removeChangeListener(this.onTeamChange); + UserStore.removeChangeListener(this.onChange); + UserStore.removeInTeamChangeListener(this.onChange); + UserStore.removeStatusesChangeListener(this.onChange); + TeamStore.removeChangeListener(this.onChange); } - shouldComponentUpdate(nextProps, nextState) { - if (nextProps.show !== this.props.show) { - return true; - } - - if (nextProps.onModalDismissed.toString() !== this.props.onModalDismissed.toString()) { - return true; - } - - if (nextState.loadingDMChannel !== this.state.loadingDMChannel) { - return true; - } - - if (!Utils.areObjectsEqual(nextState.users, this.state.users)) { - return true; - } - - if (!Utils.areObjectsEqual(nextState.teamMembers, this.state.teamMembers)) { - return true; - } - - return false; + loadComplete() { + this.setState({loading: false}); } handleHide() { @@ -84,7 +78,7 @@ export default class MoreDirectChannels extends React.Component { } this.setState({loadingDMChannel: teammate.id}); - Utils.openDirectChannelToUser( + openDirectChannelToUser( teammate, (channel) => { browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name); @@ -97,17 +91,35 @@ export default class MoreDirectChannels extends React.Component { ); } - handleUserChange() { + onChange(force) { + if (this.state.search && !force) { + return; + } + + let users; + if (this.state.listType === 'any') { + users = UserStore.getProfileList(); + } else { + users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true); + } + this.setState({ - users: UserStore.getProfilesForDmList(), - usersLoaded: true + users }); } - onTeamChange() { + toggleList(e) { + const listType = e.target.value; + let users; + if (listType === 'any') { + users = UserStore.getProfileList(); + } else { + users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true); + } + this.setState({ - teamMembers: TeamStore.getMembersForTeam(), - teamMembersLoaded: true + users, + listType }); } @@ -126,38 +138,96 @@ export default class MoreDirectChannels extends React.Component { ); } + nextPage(page) { + if (this.state.listType === 'any') { + AsyncClient.getProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); + } else { + AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), (page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); + } + } + + search(term) { + if (term === '') { + this.onChange(true); + this.setState({search: false}); + return; + } + + let teamId; + if (this.state.listType === 'any') { + teamId = ''; + } else { + teamId = TeamStore.getCurrentId(); + } + + searchUsers( + term, + teamId, + {}, + (users) => { + for (let i = 0; i < users.length; i++) { + if (users[i].id === UserStore.getCurrentId()) { + users.splice(i, 1); + break; + } + } + this.setState({search: true, users}); + } + ); + } + render() { let maxHeight = 1000; if (Utils.windowHeight() <= 1200) { maxHeight = Utils.windowHeight() - 300; } - var body = null; - if (!this.state.usersLoaded || !this.state.teamMembersLoaded) { - body = (<LoadingScreen/>); - } else { - var showTeamToggle = false; - if (global.window.mm_config.RestrictDirectMessage === 'any') { - showTeamToggle = true; - } - - body = ( - <FilteredUserList - style={{maxHeight}} - users={this.state.users} - teamMembers={this.state.teamMembers} - actions={[this.createJoinDirectChannelButton]} - showTeamToggle={showTeamToggle} - /> + let teamToggle; + if (global.window.mm_config.RestrictDirectMessage === 'any') { + teamToggle = ( + <div className='member-select__container'> + <select + className='form-control' + id='restrictList' + ref='restrictList' + defaultValue='team' + onChange={this.toggleList} + > + <option value='any'> + <FormattedMessage + id='filtered_user_list.any_team' + defaultMessage='All Users' + /> + </option> + <option value='team'> + <FormattedMessage + id='filtered_user_list.team_only' + defaultMessage='Members of this Team' + /> + </option> + </select> + <span + className='member-show' + > + <FormattedMessage + id='filtered_user_list.show' + defaultMessage='Filter:' + /> + </span> + </div> ); } + let users = this.state.users; + if (this.state.loading) { + users = null; + } + return ( <Modal dialogClassName='more-modal more-direct-channels' show={this.props.show} onHide={this.handleHide} - onEntered={getMoreDmList} > <Modal.Header closeButton={true}> <Modal.Title> @@ -168,7 +238,16 @@ export default class MoreDirectChannels extends React.Component { </Modal.Title> </Modal.Header> <Modal.Body> - {body} + {teamToggle} + <SearchableUserList + key={'moreDirectChannelsList_' + this.state.listType} + style={{maxHeight}} + users={users} + usersPerPage={USERS_PER_PAGE} + nextPage={this.nextPage} + search={this.search} + actions={[this.createJoinDirectChannelButton]} + /> </Modal.Body> <Modal.Footer> <button diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index 72066780e..865e2ac78 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -69,8 +69,8 @@ export default class Navbar extends React.Component { return { channel: ChannelStore.getCurrent(), member: ChannelStore.getCurrentMember(), - users: ChannelStore.getCurrentExtraInfo().members, - userCount: ChannelStore.getCurrentExtraInfo().member_count, + users: [], + userCount: ChannelStore.getCurrentStats().member_count, currentUser: UserStore.getCurrentUser() }; } @@ -81,7 +81,7 @@ export default class Navbar extends React.Component { componentDidMount() { ChannelStore.addChangeListener(this.onChange); - ChannelStore.addExtraInfoChangeListener(this.onChange); + ChannelStore.addStatsChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange); $('.inner-wrap').click(this.hideSidebars); document.addEventListener('keydown', this.showChannelSwitchModal); @@ -89,7 +89,7 @@ export default class Navbar extends React.Component { componentWillUnmount() { ChannelStore.removeChangeListener(this.onChange); - ChannelStore.removeExtraInfoChangeListener(this.onChange); + ChannelStore.removeStatsChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onChange); document.removeEventListener('keydown', this.showChannelSwitchModal); } diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx index f7244018d..e210fcbee 100644 --- a/webapp/components/needs_team.jsx +++ b/webapp/components/needs_team.jsx @@ -13,6 +13,7 @@ import UserStore from 'stores/user_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; +import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx'; import Constants from 'utils/constants.jsx'; const TutorialSteps = Constants.TutorialSteps; const Preferences = Constants.Preferences; @@ -80,6 +81,7 @@ export default class NeedsTeam extends React.Component { if (tutorialStep <= TutorialSteps.INTRO_SCREENS) { browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/tutorial'); } + stopPeriodicStatusUpdates(); } componentDidMount() { @@ -89,6 +91,8 @@ export default class NeedsTeam extends React.Component { // Emit view action GlobalActions.viewLoggedIn(); + startPeriodicStatusUpdates(); + // Set up tracking for whether the window is active window.isActive = true; $(window).on('focus', () => { diff --git a/webapp/components/notify_counts.jsx b/webapp/components/notify_counts.jsx index 8f9eadab7..6ccbd228b 100644 --- a/webapp/components/notify_counts.jsx +++ b/webapp/components/notify_counts.jsx @@ -7,7 +7,7 @@ import ChannelStore from 'stores/channel_store.jsx'; function getCountsStateFromStores() { var count = 0; var channels = ChannelStore.getAll(); - var members = ChannelStore.getAllMembers(); + var members = ChannelStore.getMyMembers(); channels.forEach((channel) => { var channelMember = members[channel.id]; diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx index bfbe66677..9cea3922a 100644 --- a/webapp/components/popover_list_members.jsx +++ b/webapp/components/popover_list_members.jsx @@ -6,9 +6,11 @@ import ProfilePicture from 'components/profile_picture.jsx'; import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; +import {openDirectChannelToUser} from 'actions/channel_actions.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; import Client from 'client/web_client.jsx'; import * as Utils from 'utils/utils.jsx'; -import Constants from 'utils/constants.jsx'; import $ from 'jquery'; import React from 'react'; @@ -22,20 +24,18 @@ export default class PopoverListMembers extends React.Component { this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this); this.closePopover = this.closePopover.bind(this); + + this.state = {showPopover: false}; } componentDidUpdate() { $('.member-list__popover .popover-content').perfectScrollbar(); } - componentWillMount() { - this.setState({showPopover: false}); - } - handleShowDirectChannel(teammate, e) { e.preventDefault(); - Utils.openDirectChannelToUser( + openDirectChannelToUser( teammate, (channel, channelAlreadyExisted) => { browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name); @@ -90,12 +90,6 @@ export default class PopoverListMembers extends React.Component { } if (name) { - let status; - if (m.status) { - status = m.status; - } else { - status = UserStore.getStatus(m.id); - } popoverHtml.push( <div className='more-modal__row' @@ -103,7 +97,6 @@ export default class PopoverListMembers extends React.Component { > <ProfilePicture src={`${Client.getUsersRoute()}/${m.id}/image?time=${m.update_at}`} - status={status} width='26' height='26' /> @@ -123,19 +116,27 @@ export default class PopoverListMembers extends React.Component { ); } }); - } - - let count = this.props.memberCount; - let countText = '-'; - // fall back to checking the length of the member list if the count isn't set - if (!count && members) { - count = members.length; + popoverHtml.push( + <div + className='more-modal__row' + key={'popover-member-more'} + > + <div className='col-sm-5'/> + <div className='more-modal__details'> + <div + className='more-modal__name' + > + {'...'} + </div> + </div> + </div> + ); } - if (count > Constants.MAX_CHANNEL_POPOVER_COUNT) { - countText = Constants.MAX_CHANNEL_POPOVER_COUNT + '+'; - } else if (count > 0) { + const count = this.props.memberCount; + let countText = '-'; + if (count > 0) { countText = count.toString(); } @@ -151,7 +152,10 @@ export default class PopoverListMembers extends React.Component { id='member_popover' className='member-popover__trigger' ref='member_popover_target' - onClick={(e) => this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover})} + onClick={(e) => { + this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover}); + AsyncClient.getProfilesInChannel(this.props.channel.id, 0); + }} > <div> {countText} diff --git a/webapp/components/post_view/components/pending_post_options.jsx b/webapp/components/post_view/components/pending_post_options.jsx index 711ea832c..44f4794ef 100644 --- a/webapp/components/post_view/components/pending_post_options.jsx +++ b/webapp/components/post_view/components/pending_post_options.jsx @@ -4,11 +4,10 @@ import PostStore from 'stores/post_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; -import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; +import {loadPosts} from 'actions/post_actions.jsx'; +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import Client from 'client/web_client.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; - import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; @@ -29,13 +28,13 @@ export default class PendingPostOptions extends React.Component { var post = this.props.post; Client.createPost(post, (data) => { - AsyncClient.getPosts(post.channel_id); + loadPosts(post.channel_id); var channel = ChannelStore.get(post.channel_id); - var member = ChannelStore.getMember(post.channel_id); + var member = ChannelStore.getMyMember(post.channel_id); member.msg_count = channel.total_msg_count; member.last_viewed_at = (new Date()).getTime(); - ChannelStore.setChannelMember(member); + ChannelStore.storeMyChannelMember(member); AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST, diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx index d686b28e5..46ce0ed67 100644 --- a/webapp/components/post_view/components/post_list.jsx +++ b/webapp/components/post_view/components/post_list.jsx @@ -66,6 +66,16 @@ export default class PostList extends React.Component { } } + componentWillReceiveProps(nextProps) { + // TODO: Clean-up intro text creation + if (this.props.channel && this.props.channel.type === Constants.DM_CHANNEL) { + const teammateId = Utils.getUserIdFromChannelName(this.props.channel); + if (!this.props.profiles[teammateId] && nextProps.profiles[teammateId]) { + this.introText = createChannelIntroMessage(this.props.channel, this.state.fullWidthIntro); + } + } + } + handleKeyDown(e) { if (e.which === Constants.KeyCodes.ESCAPE && $('.popover.in,.modal.in').length === 0) { e.preventDefault(); diff --git a/webapp/components/post_view/post_focus_view_controller.jsx b/webapp/components/post_view/post_focus_view_controller.jsx index 4e21cb29f..8edec6970 100644 --- a/webapp/components/post_view/post_focus_view_controller.jsx +++ b/webapp/components/post_view/post_focus_view_controller.jsx @@ -35,10 +35,7 @@ export default class PostFocusView extends React.Component { const focusedPostId = PostStore.getFocusedPostId(); const channel = ChannelStore.getCurrent(); - let profiles = UserStore.getProfiles(); - if (channel && channel.type === Constants.DM_CHANNEL) { - profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); - } + const profiles = UserStore.getProfiles(); const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true); @@ -115,12 +112,7 @@ export default class PostFocusView extends React.Component { } onUserChange() { - const channel = ChannelStore.getCurrent(); - let profiles = UserStore.getProfiles(); - if (channel && channel.type === Constants.DM_CHANNEL) { - profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); - } - this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(profiles))}); + this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))}); } onStatusChange() { diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx index 12fd5cd63..57b488b54 100644 --- a/webapp/components/post_view/post_view_controller.jsx +++ b/webapp/components/post_view/post_view_controller.jsx @@ -34,13 +34,10 @@ export default class PostViewController extends React.Component { this.onBusy = this.onBusy.bind(this); const channel = props.channel; - let profiles = UserStore.getProfiles(); - if (channel && channel.type === Constants.DM_CHANNEL) { - profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); - } + const profiles = UserStore.getProfiles(); let lastViewed = Number.MAX_VALUE; - const member = ChannelStore.getMember(channel.id); + const member = ChannelStore.getMyMember(channel.id); if (member != null) { lastViewed = member.last_viewed_at; } @@ -107,12 +104,7 @@ export default class PostViewController extends React.Component { } onUserChange() { - const channel = this.state.channel; - let profiles = UserStore.getProfiles(); - if (channel && channel.type === Constants.DM_CHANNEL) { - profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); - } - this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(profiles))}); + this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))}); } onPostsChange() { @@ -165,15 +157,12 @@ export default class PostViewController extends React.Component { const channel = nextProps.channel; let lastViewed = Number.MAX_VALUE; - const member = ChannelStore.getMember(channel.id); + const member = ChannelStore.getMyMember(channel.id); if (member != null) { lastViewed = member.last_viewed_at; } - let profiles = UserStore.getProfiles(); - if (channel && channel.type === Constants.DM_CHANNEL) { - profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); - } + const profiles = UserStore.getProfiles(); const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true); diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 7d643bd38..27446c85a 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -61,6 +61,10 @@ export default class RhsRootPost extends React.Component { return true; } + if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) { + return true; + } + if (!Utils.areObjectsEqual(nextProps.currentUser, this.props.currentUser)) { return true; } @@ -85,7 +89,7 @@ export default class RhsRootPost extends React.Component { var isOwner = this.props.currentUser.id === post.user_id; var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX); - var timestamp = UserStore.getProfile(post.user_id).update_at; + var timestamp = user.update_at; var channel = ChannelStore.get(post.channel_id); const flagIcon = Constants.FLAG_ICON_SVG; diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread.jsx index 7d0de8590..11c79d722 100644 --- a/webapp/components/rhs_thread.jsx +++ b/webapp/components/rhs_thread.jsx @@ -8,7 +8,6 @@ import RootPost from './rhs_root_post.jsx'; import Comment from './rhs_comment.jsx'; import FileUploadOverlay from './file_upload_overlay.jsx'; -import ChannelStore from 'stores/channel_store.jsx'; import PostStore from 'stores/post_store.jsx'; import UserStore from 'stores/user_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; @@ -238,12 +237,7 @@ export default class RhsThread extends React.Component { render() { const postsArray = this.state.postsArray; const selected = this.state.selected; - const channel = ChannelStore.get(this.state.selected.channel_id); - - let profiles = this.state.profiles || {}; - if (channel && channel.type === Constants.DM_CHANNEL) { - profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); - } + const profiles = this.state.profiles || {}; if (postsArray == null || selected == null) { return ( diff --git a/webapp/components/searchable_user_list.jsx b/webapp/components/searchable_user_list.jsx new file mode 100644 index 000000000..8d4f74ab3 --- /dev/null +++ b/webapp/components/searchable_user_list.jsx @@ -0,0 +1,226 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import UserList from 'components/user_list.jsx'; + +import * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.jsx'; +const KeyCodes = Constants.KeyCodes; + +import $ from 'jquery'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import {FormattedMessage} from 'react-intl'; + +const NEXT_BUTTON_TIMEOUT = 500; + +export default class SearchableUserList extends React.Component { + constructor(props) { + super(props); + + this.nextPage = this.nextPage.bind(this); + this.previousPage = this.previousPage.bind(this); + this.doSearch = this.doSearch.bind(this); + this.onSearchBoxKeyPress = this.onSearchBoxKeyPress.bind(this); + this.onSearchBoxChange = this.onSearchBoxChange.bind(this); + + this.nextTimeoutId = 0; + + this.state = { + page: 0, + search: false, + nextDisabled: false + }; + } + + componentDidUpdate(prevProps, prevState) { + if (this.state.page !== prevState.page) { + $(ReactDOM.findDOMNode(this.refs.userList)).scrollTop(0); + } + } + + componentWillUnmount() { + clearTimeout(this.nextTimeoutId); + } + + nextPage(e) { + e.preventDefault(); + this.setState({page: this.state.page + 1, nextDisabled: true}); + this.nextTimeoutId = setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT); + this.props.nextPage(this.state.page + 1); + } + + previousPage(e) { + e.preventDefault(); + this.setState({page: this.state.page - 1}); + } + + doSearch() { + const term = this.refs.filter.value; + this.props.search(term); + if (term === '') { + this.setState({page: 0, search: false}); + } else { + this.setState({search: true}); + } + } + + onSearchBoxKeyPress(e) { + if (e.charCode === KeyCodes.ENTER) { + e.preventDefault(); + this.doSearch(); + } + } + + onSearchBoxChange(e) { + if (e.target.value === '') { + this.props.search(''); // clear search + this.setState({page: 0, search: false}); + } + } + + render() { + let nextButton; + let previousButton; + let usersToDisplay; + let count; + + if (this.props.users == null) { + usersToDisplay = this.props.users; + } else if (this.state.search || this.props.users == null) { + usersToDisplay = this.props.users; + + if (this.props.total) { + count = ( + <FormattedMessage + id='filtered_user_list.countTotal' + defaultMessage='{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} total' + values={{ + count: usersToDisplay.length || 0, + total: this.props.total + }} + /> + ); + } + } else { + const pageStart = this.state.page * this.props.usersPerPage; + const pageEnd = pageStart + this.props.usersPerPage; + usersToDisplay = this.props.users.slice(pageStart, pageEnd); + + if (usersToDisplay.length >= this.props.usersPerPage) { + nextButton = ( + <button + className='btn btn-default filter-control filter-control__next' + onClick={this.nextPage} + disabled={this.state.nextDisabled} + > + {'Next'} + </button> + ); + } + + if (this.state.page > 0) { + previousButton = ( + <button + className='btn btn-default filter-control filter-control__prev' + onClick={this.previousPage} + > + {'Previous'} + </button> + ); + } + + if (this.props.total) { + const startCount = this.state.page * this.props.usersPerPage; + const endCount = startCount + usersToDisplay.length; + + count = ( + <FormattedMessage + id='filtered_user_list.countTotalPage' + defaultMessage='{startCount, number} - {endCount, number} {count, plural, =0 {0 members} one {member} other {members}} of {total} total' + values={{ + count: usersToDisplay.length, + startCount: startCount + 1, + endCount, + total: this.props.total + }} + /> + ); + } + } + + return ( + <div + className='filtered-user-list' + style={this.props.style} + > + <div className='filter-row'> + <div className='col-sm-5'> + <input + ref='filter' + className='form-control filter-textbox' + placeholder={Utils.localizeMessage('filtered_user_list.search', 'Press enter to search')} + onKeyPress={this.onSearchBoxKeyPress} + onChange={this.onSearchBoxChange} + /> + </div> + <div className='col-sm-2 filter-button'> + <button + type='button' + className='btn btn-primary' + onClick={this.doSearch} + disabled={this.props.users == null} + > + <FormattedMessage + id='filtered_user_list.searchButton' + defaultMessage='Search' + /> + </button> + </div> + <div className='col-sm-12'> + <span className='member-count pull-left'>{count}</span> + </div> + </div> + <div + ref='userList' + className='more-modal__list' + > + <UserList + users={usersToDisplay} + extraInfo={this.props.extraInfo} + actions={this.props.actions} + actionProps={this.props.actionProps} + actionUserProps={this.props.actionUserProps} + /> + </div> + <div className='filter-controls'> + {previousButton} + {nextButton} + </div> + </div> + ); + } +} + +SearchableUserList.defaultProps = { + users: [], + usersPerPage: 50, //eslint-disable-line no-magic-numbers + extraInfo: {}, + actions: [], + actionProps: {}, + actionUserProps: {}, + showTeamToggle: false +}; + +SearchableUserList.propTypes = { + users: React.PropTypes.arrayOf(React.PropTypes.object), + usersPerPage: React.PropTypes.number, + total: React.PropTypes.number, + extraInfo: React.PropTypes.object, + nextPage: React.PropTypes.func.isRequired, + search: React.PropTypes.func.isRequired, + actions: React.PropTypes.arrayOf(React.PropTypes.func), + actionProps: React.PropTypes.object, + actionUserProps: React.PropTypes.object, + style: React.PropTypes.object +}; diff --git a/webapp/components/select_team/select_team.jsx b/webapp/components/select_team/select_team.jsx index 5f8d9f463..283299b37 100644 --- a/webapp/components/select_team/select_team.jsx +++ b/webapp/components/select_team/select_team.jsx @@ -46,7 +46,7 @@ export default class SelectTeam extends React.Component { getStateFromStores(loaded) { return { teams: TeamStore.getAll(), - teamMembers: TeamStore.getTeamMembers(), + teamMembers: TeamStore.getMyTeamMembers(), teamListings: TeamStore.getTeamListings(), loaded }; diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index dc52ebb91..c8a7e1eb9 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -19,6 +19,7 @@ import LocalizationStore from 'stores/localization_store.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as Utils from 'utils/utils.jsx'; import * as ChannelActions from 'actions/channel_actions.jsx'; +import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx'; import Constants from 'utils/constants.jsx'; @@ -93,7 +94,7 @@ export default class Sidebar extends React.Component { } getStateFromStores() { - const members = ChannelStore.getAllMembers(); + const members = ChannelStore.getMyMembers(); const currentChannelId = ChannelStore.getCurrentId(); const currentUserId = UserStore.getCurrentId(); @@ -133,9 +134,9 @@ export default class Sidebar extends React.Component { directChannel.teammate_id = teammateId; directChannel.status = UserStore.getStatus(teammateId) || 'offline'; - if (UserStore.hasTeamProfile(teammateId) && TeamStore.hasActiveMemberForTeam(teammateId)) { + if (TeamStore.hasActiveMemberInTeam(TeamStore.getCurrentId(), teammateId)) { directChannels.push(directChannel); - } else { + } else if (TeamStore.hasMemberNotInTeam(TeamStore.getCurrentId(), teammateId)) { directNonTeamChannels.push(directChannel); } } @@ -164,6 +165,7 @@ export default class Sidebar extends React.Component { componentDidMount() { ChannelStore.addChangeListener(this.onChange); UserStore.addChangeListener(this.onChange); + UserStore.addInTeamChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange); TeamStore.addChangeListener(this.onChange); PreferenceStore.addChangeListener(this.onChange); @@ -173,6 +175,8 @@ export default class Sidebar extends React.Component { document.addEventListener('keydown', this.navigateChannelShortcut); document.addEventListener('keydown', this.navigateUnreadChannelShortcut); + + loadProfilesAndTeamMembersForDMSidebar(); } shouldComponentUpdate(nextProps, nextState) { @@ -205,6 +209,7 @@ export default class Sidebar extends React.Component { componentWillUnmount() { ChannelStore.removeChangeListener(this.onChange); UserStore.removeChangeListener(this.onChange); + UserStore.removeInTeamChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onChange); TeamStore.removeChangeListener(this.onChange); PreferenceStore.removeChangeListener(this.onChange); diff --git a/webapp/components/sidebar_header_dropdown.jsx b/webapp/components/sidebar_header_dropdown.jsx index dccac64b3..76ed6271a 100644 --- a/webapp/components/sidebar_header_dropdown.jsx +++ b/webapp/components/sidebar_header_dropdown.jsx @@ -54,7 +54,7 @@ export default class SidebarHeaderDropdown extends React.Component { this.state = { teams: TeamStore.getAll(), - teamMembers: TeamStore.getTeamMembers(), + teamMembers: TeamStore.getMyTeamMembers(), showDropdown: false }; } @@ -118,7 +118,7 @@ export default class SidebarHeaderDropdown extends React.Component { onTeamChange() { this.setState({ teams: TeamStore.getAll(), - teamMembers: TeamStore.getTeamMembers() + teamMembers: TeamStore.getMyTeamMembers() }); } diff --git a/webapp/components/suggestion/at_mention_provider.jsx b/webapp/components/suggestion/at_mention_provider.jsx index 9998e6357..d4f441f98 100644 --- a/webapp/components/suggestion/at_mention_provider.jsx +++ b/webapp/components/suggestion/at_mention_provider.jsx @@ -1,19 +1,19 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React from 'react'; +import Suggestion from './suggestion.jsx'; -import SuggestionStore from 'stores/suggestion_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; -import UserStore from 'stores/user_store.jsx'; + +import {autocompleteUsersInChannel} from 'actions/user_actions.jsx'; + +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import * as Utils from 'utils/utils.jsx'; import Client from 'client/web_client.jsx'; -import Constants from 'utils/constants.jsx'; +import {Constants, ActionTypes} from 'utils/constants.jsx'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; -import Suggestion from './suggestion.jsx'; - -const MaxUserSuggestions = 40; class AtMentionSuggestion extends Suggestion { render() { @@ -99,92 +99,66 @@ class AtMentionSuggestion extends Suggestion { } } -function filterUsersByPrefix(users, prefix, limit, type) { - const filtered = []; - - for (const id of Object.keys(users)) { - if (filtered.length >= limit) { - break; - } - - const user = users[id]; - - if (user.delete_at > 0) { - continue; - } - - if (user.username.startsWith(prefix) || - (user.first_name && user.first_name.toLowerCase().startsWith(prefix)) || - (user.last_name && user.last_name.toLowerCase().startsWith(prefix)) || - (user.nickname && user.nickname.toLowerCase().startsWith(prefix))) { - // create a new object here since we're mutating it by adding the type field - filtered.push(Object.assign({}, user, {type})); - } - } - - return filtered; -} - export default class AtMentionProvider { constructor(channelId) { this.channelId = channelId; + this.timeoutId = ''; + } + + componentWillUnmount() { + clearTimeout(this.timeoutId); } handlePretextChanged(suggestionId, pretext) { + clearTimeout(this.timeoutId); + const captured = (/@([a-z0-9\-\._]*)$/i).exec(pretext.toLowerCase()); if (captured) { const prefix = captured[1]; - // Group users into members and nonmembers of the channel. - const users = UserStore.getActiveOnlyProfiles(true); - const channelMembers = {}; - const channelNonmembers = users; - if (this.channelId != null) { - const extraInfo = ChannelStore.getExtraInfo(this.channelId); - for (let i = 0; i < extraInfo.members.length; i++) { - const id = extraInfo.members[i].id; - if (users[id]) { - channelMembers[id] = users[id]; - Reflect.deleteProperty(channelNonmembers, id); + function autocomplete() { + autocompleteUsersInChannel( + prefix, + this.channelId, + (data) => { + const members = data.in_channel; + for (const id of Object.keys(members)) { + members[id].type = Constants.MENTION_MEMBERS; + } + + const nonmembers = data.out_of_channel; + for (const id of Object.keys(nonmembers)) { + nonmembers[id].type = Constants.MENTION_NONMEMBERS; + } + + let specialMentions = []; + if (!pretext.startsWith('/msg')) { + specialMentions = ['here', 'channel', 'all'].filter((item) => { + return item.startsWith(prefix); + }).map((name) => { + return {username: name, type: Constants.MENTION_SPECIAL}; + }); + } + + const users = members.concat(specialMentions).concat(nonmembers); + const mentions = users.map((user) => '@' + user.username); + + AppDispatcher.handleServerAction({ + type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS, + id: suggestionId, + matchedPretext: captured[0], + terms: mentions, + items: users, + component: AtMentionSuggestion + }); } - } - } - - // Filter users by prefix. - const filteredMembers = filterUsersByPrefix( - channelMembers, prefix, MaxUserSuggestions, Constants.MENTION_MEMBERS); - const filteredNonmembers = filterUsersByPrefix( - channelNonmembers, prefix, MaxUserSuggestions - filteredMembers.length, Constants.MENTION_NONMEMBERS); - let filteredSpecialMentions = []; - if (!pretext.startsWith('/msg')) { - filteredSpecialMentions = ['here', 'channel', 'all'].filter((item) => { - return item.startsWith(prefix); - }).map((name) => { - return {username: name, type: Constants.MENTION_SPECIAL}; - }); + ); } - // Sort users by username. - [filteredMembers, filteredNonmembers].forEach((items) => { - items.sort((a, b) => { - const aPrefix = a.username.startsWith(prefix); - const bPrefix = b.username.startsWith(prefix); - - if (aPrefix === bPrefix) { - return a.username.localeCompare(b.username); - } else if (aPrefix) { - return -1; - } - - return 1; - }); - }); - - const filtered = filteredMembers.concat(filteredSpecialMentions).concat(filteredNonmembers); - - const mentions = filtered.map((user) => '@' + user.username); - - SuggestionStore.addSuggestions(suggestionId, mentions, filtered, AtMentionSuggestion, captured[0]); + this.timeoutId = setTimeout( + autocomplete.bind(this), + Constants.AUTOCOMPLETE_TIMEOUT + ); } } } diff --git a/webapp/components/suggestion/search_user_provider.jsx b/webapp/components/suggestion/search_user_provider.jsx index b5466cf39..baf91cd94 100644 --- a/webapp/components/suggestion/search_user_provider.jsx +++ b/webapp/components/suggestion/search_user_provider.jsx @@ -1,13 +1,16 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React from 'react'; +import Suggestion from './suggestion.jsx'; +import {autocompleteUsersInTeam} from 'actions/user_actions.jsx'; + +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import Client from 'client/web_client.jsx'; -import SuggestionStore from 'stores/suggestion_store.jsx'; -import UserStore from 'stores/user_store.jsx'; +import * as Utils from 'utils/utils.jsx'; +import {Constants, ActionTypes} from 'utils/constants.jsx'; -import Suggestion from './suggestion.jsx'; +import React from 'react'; class SearchUserSuggestion extends Suggestion { render() { @@ -18,6 +21,17 @@ class SearchUserSuggestion extends Suggestion { className += ' selected'; } + const username = item.username; + let description = ''; + + if ((item.first_name || item.last_name) && item.nickname) { + description = `- ${Utils.getFullName(item)} (${item.nickname})`; + } else if (item.nickname) { + description = `- (${item.nickname})`; + } else if (item.first_name || item.last_name) { + description = `- ${Utils.getFullName(item)}`; + } + return ( <div className={className} @@ -27,34 +41,60 @@ class SearchUserSuggestion extends Suggestion { className='profile-img rounded' src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.update_at} /> - <i className='fa fa fa-plus-square'/>{item.username} + <i className='fa fa fa-plus-square'/> + <div className='mention--align'> + <span> + {username} + </span> + <span className='mention__fullname'> + {' '} + {description} + </span> + </div> </div> ); } } export default class SearchUserProvider { + constructor() { + this.timeoutId = ''; + } + + componentWillUnmount() { + clearTimeout(this.timeoutId); + } + handlePretextChanged(suggestionId, pretext) { + clearTimeout(this.timeoutId); + const captured = (/\bfrom:\s*(\S*)$/i).exec(pretext.toLowerCase()); if (captured) { const usernamePrefix = captured[1]; - const users = UserStore.getProfiles(); - let filtered = []; - - for (const id of Object.keys(users)) { - const user = users[id]; + function autocomplete() { + autocompleteUsersInTeam( + usernamePrefix, + (data) => { + const users = data.in_team; + const mentions = users.map((user) => user.username); - if (user.username.startsWith(usernamePrefix)) { - filtered.push(user); - } + AppDispatcher.handleServerAction({ + type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS, + id: suggestionId, + matchedPretext: usernamePrefix, + terms: mentions, + items: users, + component: SearchUserSuggestion + }); + } + ); } - filtered = filtered.sort((a, b) => a.username.localeCompare(b.username)); - - const usernames = filtered.map((user) => user.username); - - SuggestionStore.addSuggestions(suggestionId, usernames, filtered, SearchUserSuggestion, usernamePrefix); + this.timeoutId = setTimeout( + autocomplete.bind(this), + Constants.AUTOCOMPLETE_TIMEOUT + ); } } } diff --git a/webapp/components/suggestion/suggestion_list.jsx b/webapp/components/suggestion/suggestion_list.jsx index 7d8059e1e..65311a582 100644 --- a/webapp/components/suggestion/suggestion_list.jsx +++ b/webapp/components/suggestion/suggestion_list.jsx @@ -163,4 +163,4 @@ SuggestionList.propTypes = { SuggestionList.defaultProps = { renderDividers: false -};
\ No newline at end of file +}; diff --git a/webapp/components/suggestion/switch_channel_provider.jsx b/webapp/components/suggestion/switch_channel_provider.jsx index 70e95b9b1..94622b536 100644 --- a/webapp/components/suggestion/switch_channel_provider.jsx +++ b/webapp/components/suggestion/switch_channel_provider.jsx @@ -4,7 +4,6 @@ import React from 'react'; import ChannelStore from 'stores/channel_store.jsx'; -import UserStore from 'stores/user_store.jsx'; import SuggestionStore from 'stores/suggestion_store.jsx'; import Suggestion from './suggestion.jsx'; import Constants from 'utils/constants.jsx'; @@ -58,7 +57,10 @@ export default class SwitchChannelProvider { const channel = allChannels[id]; if (channel.display_name.toLowerCase().startsWith(channelPrefix.toLowerCase())) { channels.push(channel); - } else if (channel.type === Constants.DM_CHANNEL && Utils.getDirectTeammate(channel.id).username.startsWith(channelPrefix.toLowerCase())) { + } + + // TODO: Fix with auto-complete refactor + /*else if (channel.type === Constants.DM_CHANNEL && Utils.getDirectTeammate(channel.id).username.startsWith(channelPrefix.toLowerCase())) { // New channel to not modify existing channel const otherUser = Utils.getDirectTeammate(channel.id); const newChannel = { @@ -68,7 +70,7 @@ export default class SwitchChannelProvider { status: UserStore.getStatus(otherUser.id) || 'offline' }; channels.push(newChannel); - } + }*/ } channels.sort((a, b) => { diff --git a/webapp/components/team_members_dropdown.jsx b/webapp/components/team_members_dropdown.jsx index d459d0b02..3b6bc87f3 100644 --- a/webapp/components/team_members_dropdown.jsx +++ b/webapp/components/team_members_dropdown.jsx @@ -1,17 +1,20 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import ConfirmModal from './confirm_modal.jsx'; + +import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; + +import {removeUserFromTeam} from 'actions/team_actions.jsx'; + import Client from 'client/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as Utils from 'utils/utils.jsx'; -import ConfirmModal from './confirm_modal.jsx'; -import TeamStore from 'stores/team_store.jsx'; - -import {FormattedMessage} from 'react-intl'; import React from 'react'; +import {FormattedMessage} from 'react-intl'; import {browserHistory} from 'react-router/es6'; export default class TeamMembersDropdown extends React.Component { @@ -44,8 +47,8 @@ export default class TeamMembersDropdown extends React.Component { this.props.user.id, 'team_user', () => { - AsyncClient.getTeamMembers(TeamStore.getCurrentId()); - AsyncClient.getProfiles(); + AsyncClient.getTeamMember(this.props.teamMember.team_id); + AsyncClient.getUser(this.props.user.id); }, (err) => { this.setState({serverError: err.message}); @@ -54,24 +57,23 @@ export default class TeamMembersDropdown extends React.Component { } } handleRemoveFromTeam() { - Client.removeUserFromTeam( - '', - this.props.user.id, - () => { - AsyncClient.getTeamMembers(TeamStore.getCurrentId()); - AsyncClient.getProfiles(); - }, - (err) => { - this.setState({serverError: err.message}); - } - ); + removeUserFromTeam( + this.props.teamMember.team_id, + this.props.user.id, + () => { + AsyncClient.getTeamStats(this.props.teamMember.team_id); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); } handleMakeActive() { Client.updateActive(this.props.user.id, true, () => { - AsyncClient.getTeamMembers(TeamStore.getCurrentId()); - AsyncClient.getProfiles(); - AsyncClient.getChannelExtraInfo(ChannelStore.getCurrentId()); + AsyncClient.getUser(this.props.user.id); + AsyncClient.getChannelStats(ChannelStore.getCurrentId()); + AsyncClient.getTeamStats(this.props.teamMember.team_id); }, (err) => { this.setState({serverError: err.message}); @@ -81,9 +83,9 @@ export default class TeamMembersDropdown extends React.Component { handleMakeNotActive() { Client.updateActive(this.props.user.id, false, () => { - AsyncClient.getTeamMembers(TeamStore.getCurrentId()); - AsyncClient.getProfiles(); - AsyncClient.getChannelExtraInfo(ChannelStore.getCurrentId()); + AsyncClient.getUser(this.props.user.id); + AsyncClient.getChannelStats(ChannelStore.getCurrentId()); + AsyncClient.getTeamStats(this.props.teamMember.team_id); }, (err) => { this.setState({serverError: err.message}); @@ -100,8 +102,8 @@ export default class TeamMembersDropdown extends React.Component { this.props.user.id, 'team_user team_admin', () => { - AsyncClient.getTeamMembers(TeamStore.getCurrentId()); - AsyncClient.getProfiles(); + AsyncClient.getTeamMember(this.props.teamMember.team_id, this.props.user.id); + AsyncClient.getUser(this.props.user.id); }, (err) => { this.setState({serverError: err.message}); @@ -133,8 +135,8 @@ export default class TeamMembersDropdown extends React.Component { this.props.user.id, this.state.newRole, () => { - AsyncClient.getTeamMembers(TeamStore.getCurrentId()); - AsyncClient.getProfiles(); + AsyncClient.getTeamMember(this.props.teamMember.team_id, this.props.user.id); + AsyncClient.getUser(this.props.user.id); const teamUrl = TeamStore.getCurrentTeamUrl(); if (teamUrl) { diff --git a/webapp/components/textbox.jsx b/webapp/components/textbox.jsx index fa2ffec1e..44468a67a 100644 --- a/webapp/components/textbox.jsx +++ b/webapp/components/textbox.jsx @@ -109,7 +109,7 @@ export default class Textbox extends React.Component { } componentWillReceiveProps(nextProps) { - if (nextProps.channelId !== this.channelId) { + if (nextProps.channelId !== this.props.channelId) { // Update channel id for AtMentionProvider. const providers = this.suggestionProviders; for (let i = 0; i < providers.length; i++) { diff --git a/webapp/components/user_list.jsx b/webapp/components/user_list.jsx index 626cb3cf5..d34404c89 100644 --- a/webapp/components/user_list.jsx +++ b/webapp/components/user_list.jsx @@ -1,32 +1,29 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {FormattedMessage} from 'react-intl'; import UserListRow from './user_list_row.jsx'; +import LoadingScreen from 'components/loading_screen.jsx'; import React from 'react'; +import {FormattedMessage} from 'react-intl'; export default class UserList extends React.Component { render() { const users = this.props.users; let content; - if (users.length > 0) { + if (users == null) { + return <LoadingScreen/>; + } else if (users.length > 0) { content = users.map((user) => { - var teamMember; - for (var index in this.props.teamMembers) { - if (this.props.teamMembers[index].user_id === user.id) { - teamMember = this.props.teamMembers[index]; - } - } - return ( <UserListRow key={user.id} user={user} - teamMember={teamMember} + extraInfo={this.props.extraInfo[user.id]} actions={this.props.actions} actionProps={this.props.actionProps} + actionUserProps={this.props.actionUserProps[user.id]} /> ); }); @@ -56,14 +53,15 @@ export default class UserList extends React.Component { UserList.defaultProps = { users: [], - teamMembers: [], + extraInfo: {}, actions: [], actionProps: {} }; UserList.propTypes = { users: React.PropTypes.arrayOf(React.PropTypes.object), - teamMembers: React.PropTypes.arrayOf(React.PropTypes.object), + extraInfo: React.PropTypes.object, actions: React.PropTypes.arrayOf(React.PropTypes.func), - actionProps: React.PropTypes.object + actionProps: React.PropTypes.object, + actionUserProps: React.PropTypes.object }; diff --git a/webapp/components/user_list_row.jsx b/webapp/components/user_list_row.jsx index 9f80d4caa..ff381a30b 100644 --- a/webapp/components/user_list_row.jsx +++ b/webapp/components/user_list_row.jsx @@ -11,8 +11,9 @@ import * as Utils from 'utils/utils.jsx'; import Client from 'client/web_client.jsx'; import React from 'react'; +import {FormattedHTMLMessage} from 'react-intl'; -export default function UserListRow({user, teamMember, actions, actionProps}) { +export default function UserListRow({user, extraInfo, actions, actionProps, actionUserProps}) { const nameFormat = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', ''); let name = user.username; @@ -29,15 +30,29 @@ export default function UserListRow({user, teamMember, actions, actionProps}) { <Action key={index.toString()} user={user} - teamMember={teamMember} {...actionProps} + {...actionUserProps} /> ); }); } + // QUICK HACK, NEEDS A PROP FOR TOGGLING STATUS + let email = user.email; + let emailStyle = 'more-modal__description'; let status; - if (user.status) { + if (extraInfo && extraInfo.length > 0) { + email = ( + <FormattedHTMLMessage + id='admin.user_item.emailTitle' + defaultMessage='<strong>Email:</strong> {email}' + values={{ + email: user.email + }} + /> + ); + emailStyle = ''; + } else if (user.status) { status = user.status; } else { status = UserStore.getStatus(user.id); @@ -60,9 +75,10 @@ export default function UserListRow({user, teamMember, actions, actionProps}) { <div className='more-modal__name'> {name} </div> - <div className='more-modal__description'> - {user.email} + <div className={emailStyle}> + {email} </div> + {extraInfo} </div> <div className='more-modal__actions' @@ -74,17 +90,16 @@ export default function UserListRow({user, teamMember, actions, actionProps}) { } UserListRow.defaultProps = { - teamMember: { - team_id: '', - roles: '' - }, + extraInfo: [], actions: [], - actionProps: {} + actionProps: {}, + actionUserProps: {} }; UserListRow.propTypes = { user: React.PropTypes.object.isRequired, - teamMember: React.PropTypes.object.isRequired, + extraInfo: React.PropTypes.arrayOf(React.PropTypes.object), actions: React.PropTypes.arrayOf(React.PropTypes.func), - actionProps: React.PropTypes.object + actionProps: React.PropTypes.object, + actionUserProps: React.PropTypes.object }; |