diff options
Diffstat (limited to 'webapp')
72 files changed, 3184 insertions, 1556 deletions
diff --git a/webapp/actions/channel_actions.jsx b/webapp/actions/channel_actions.jsx index ed8e00db6..8364fe9b6 100644 --- a/webapp/actions/channel_actions.jsx +++ b/webapp/actions/channel_actions.jsx @@ -1,18 +1,26 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {browserHistory} from 'react-router/es6'; -import * as Utils from 'utils/utils.jsx'; +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; + import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; +import PreferenceStore from 'stores/preference_store.jsx'; + +import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx'; + +import Client from 'client/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as UserAgent from 'utils/user_agent.jsx'; -import Client from 'client/web_client.jsx'; +import * as Utils from 'utils/utils.jsx'; +import {Preferences, ActionTypes} from 'utils/constants.jsx'; + +import {browserHistory} from 'react-router/es6'; export function goToChannel(channel) { if (channel.fake) { - Utils.openDirectChannelToUser( + openDirectChannelToUser( UserStore.getProfileByUsername(channel.display_name), () => { browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name); @@ -53,3 +61,124 @@ export function setChannelAsRead(channelIdParam) { ChannelStore.emitLastViewed(Number.MAX_VALUE, false); } } + +export function addUserToChannel(channelId, userId, success, error) { + Client.addChannelMember( + channelId, + userId, + (data) => { + UserStore.removeProfileNotInChannel(channelId, userId); + const profile = UserStore.getProfile(userId); + if (profile) { + UserStore.saveProfileInChannel(channelId, profile); + UserStore.emitInChannelChange(); + } + UserStore.emitNotInChannelChange(); + + if (success) { + success(data); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'addChannelMember'); + + if (error) { + error(err); + } + } + ); +} + +export function removeUserFromChannel(channelId, userId, success, error) { + Client.removeChannelMember( + channelId, + userId, + (data) => { + UserStore.removeProfileInChannel(channelId, userId); + const profile = UserStore.getProfile(userId); + if (profile) { + UserStore.saveProfileNotInChannel(channelId, profile); + UserStore.emitNotInChannelChange(); + } + UserStore.emitInChannelChange(); + + if (success) { + success(data); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'removeChannelMember'); + + if (error) { + error(err); + } + } + ); +} + +export function openDirectChannelToUser(user, success, error) { + const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), user.id); + let channel = ChannelStore.getByName(channelName); + + if (channel) { + PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true'); + loadProfilesAndTeamMembersForDMSidebar(); + + AsyncClient.savePreference( + Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, + user.id, + 'true' + ); + + if (success) { + success(channel, true); + } + + return; + } + + channel = { + name: channelName, + last_post_at: 0, + total_msg_count: 0, + type: 'D', + display_name: user.username, + teammate_id: user.id, + status: UserStore.getStatus(user.id) + }; + + Client.createDirectChannel( + user.id, + (data) => { + Client.getChannel( + data.id, + (data2) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_CHANNEL, + channel: data2.channel, + member: data2.member + }); + + PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true'); + loadProfilesAndTeamMembersForDMSidebar(); + + AsyncClient.savePreference( + Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, + user.id, + 'true' + ); + + if (success) { + success(data2.channel, false); + } + } + ); + }, + () => { + browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/' + channelName); + if (error) { + error(); + } + } + ); +} diff --git a/webapp/actions/emoji_actions.jsx b/webapp/actions/emoji_actions.jsx new file mode 100644 index 000000000..128a9325a --- /dev/null +++ b/webapp/actions/emoji_actions.jsx @@ -0,0 +1,46 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; + +import UserStore from 'stores/user_store.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import Client from 'client/web_client.jsx'; + +import {ActionTypes} from 'utils/constants.jsx'; + +export function loadEmoji(getProfiles = true) { + Client.listEmoji( + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_CUSTOM_EMOJIS, + emojis: data + }); + + if (getProfiles) { + loadProfilesForEmoji(data); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'listEmoji'); + } + ); +} + +function loadProfilesForEmoji(emojiList) { + const profilesToLoad = {}; + for (let i = 0; i < emojiList.length; i++) { + const emoji = emojiList[i]; + if (!UserStore.hasProfile(emoji.creator_id)) { + profilesToLoad[emoji.creator_id] = true; + } + } + + const list = Object.keys(profilesToLoad); + if (list.length === 0) { + return; + } + + AsyncClient.getProfilesByIds(list); +} diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx index 941aa34f4..23ff5a295 100644 --- a/webapp/actions/global_actions.jsx +++ b/webapp/actions/global_actions.jsx @@ -12,7 +12,8 @@ import TeamStore from 'stores/team_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import SearchStore from 'stores/search_store.jsx'; -import {handleNewPost} from 'actions/post_actions.jsx'; +import {handleNewPost, loadPosts, loadPostsBefore, loadPostsAfter} from 'actions/post_actions.jsx'; +import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx'; import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; @@ -43,9 +44,9 @@ export function emitChannelClickEvent(channel) { function switchToChannel(chan) { AsyncClient.getChannels(true); AsyncClient.getMoreChannels(true); - AsyncClient.getChannelExtraInfo(chan.id); + AsyncClient.getChannelStats(chan.id); AsyncClient.updateLastViewedAt(chan.id); - AsyncClient.getPosts(chan.id); + loadPosts(chan.id); trackPage(); AppDispatcher.handleViewAction({ @@ -108,7 +109,7 @@ export function emitInitialLoad(callback) { if (data.team_members) { AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_TEAM_MEMBERS, + type: ActionTypes.RECEIVED_MY_TEAM_MEMBERS, team_members: data.team_members }); } @@ -143,9 +144,9 @@ export function doFocusPost(channelId, postId, data) { }); AsyncClient.getChannels(true); AsyncClient.getMoreChannels(true); - AsyncClient.getChannelExtraInfo(channelId); - AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS, true); - AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS, true); + AsyncClient.getChannelStats(channelId); + loadPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS, true); + loadPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS, true); } export function emitPostFocusEvent(postId, onSuccess) { @@ -246,14 +247,14 @@ export function emitLoadMorePostsFocusedTopEvent() { export function loadMorePostsTop(id, isFocusPost) { const earliestPostId = PostStore.getEarliestPost(id).id; if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) { - AsyncClient.getPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE, isFocusPost); + loadPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE, isFocusPost); } } export function emitLoadMorePostsFocusedBottomEvent() { const id = PostStore.getFocusedPostId(); const latestPostId = PostStore.getLatestPost(id).id; - AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE, Boolean(id)); + loadPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE, Boolean(id)); } export function emitUserPostedEvent(post) { @@ -362,7 +363,7 @@ export function emitClearSuggestions(suggestionId) { export function emitPreferenceChangedEvent(preference) { if (preference.category === Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW) { - AsyncClient.getDirectProfiles(); + loadProfilesAndTeamMembersForDMSidebar(); } AppDispatcher.handleServerAction({ @@ -437,7 +438,7 @@ export function loadDefaultLocale() { export function viewLoggedIn() { AsyncClient.getChannels(); AsyncClient.getMoreChannels(); - AsyncClient.getChannelExtraInfo(); + AsyncClient.getChannelStats(); // Clear pending posts (shouldn't have pending posts if we are loading) PostStore.clearPendingPosts(); diff --git a/webapp/actions/integration_actions.jsx b/webapp/actions/integration_actions.jsx new file mode 100644 index 000000000..5fd2b024d --- /dev/null +++ b/webapp/actions/integration_actions.jsx @@ -0,0 +1,114 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; + +import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import Client from 'client/web_client.jsx'; + +import {ActionTypes} from 'utils/constants.jsx'; + +export function loadIncomingHooks() { + Client.listIncomingHooks( + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_INCOMING_WEBHOOKS, + teamId: TeamStore.getCurrentId(), + incomingWebhooks: data + }); + + loadProfilesForIncomingHooks(data); + }, + (err) => { + AsyncClient.dispatchError(err, 'listIncomingHooks'); + } + ); +} + +function loadProfilesForIncomingHooks(hooks) { + const profilesToLoad = {}; + for (let i = 0; i < hooks.length; i++) { + const hook = hooks[i]; + if (!UserStore.hasProfile(hook.user_id)) { + profilesToLoad[hook.user_id] = true; + } + } + + const list = Object.keys(profilesToLoad); + if (list.length === 0) { + return; + } + + AsyncClient.getProfilesByIds(list); +} + +export function loadOutgoingHooks() { + Client.listOutgoingHooks( + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_OUTGOING_WEBHOOKS, + teamId: TeamStore.getCurrentId(), + outgoingWebhooks: data + }); + + loadProfilesForOutgoingHooks(data); + }, + (err) => { + AsyncClient.dispatchError(err, 'listOutgoingHooks'); + } + ); +} + +function loadProfilesForOutgoingHooks(hooks) { + const profilesToLoad = {}; + for (let i = 0; i < hooks.length; i++) { + const hook = hooks[i]; + if (!UserStore.hasProfile(hook.creator_id)) { + profilesToLoad[hook.creator_id] = true; + } + } + + const list = Object.keys(profilesToLoad); + if (list.length === 0) { + return; + } + + AsyncClient.getProfilesByIds(list); +} + +export function loadTeamCommands() { + Client.listTeamCommands( + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_COMMANDS, + teamId: Client.teamId, + commands: data + }); + + loadProfilesForCommands(data); + }, + (err) => { + AsyncClient.dispatchError(err, 'loadTeamCommands'); + } + ); +} + +function loadProfilesForCommands(commands) { + const profilesToLoad = {}; + for (let i = 0; i < commands.length; i++) { + const command = commands[i]; + if (!UserStore.hasProfile(command.creator_id)) { + profilesToLoad[command.creator_id] = true; + } + } + + const list = Object.keys(profilesToLoad); + if (list.length === 0) { + return; + } + + AsyncClient.getProfilesByIds(list); +} diff --git a/webapp/actions/post_actions.jsx b/webapp/actions/post_actions.jsx index 63e3feec5..462576021 100644 --- a/webapp/actions/post_actions.jsx +++ b/webapp/actions/post_actions.jsx @@ -8,6 +8,8 @@ import PostStore from 'stores/post_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; +import {loadStatusesForChannel} from 'actions/status_actions.jsx'; + import * as PostUtils from 'utils/post_utils.jsx'; import Client from 'client/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; @@ -52,6 +54,8 @@ export function handleNewPost(post, msg) { post, websocketMessageProps }); + + loadProfilesForPosts(data.posts); }, (err) => { AsyncClient.dispatchError(err, 'getPost'); @@ -115,7 +119,7 @@ export function setUnreadPost(channelId, postId) { member.last_viewed_at = lastViewed; member.msg_count = channel.total_msg_count - unreadPosts; member.mention_count = 0; - ChannelStore.setChannelMember(member); + ChannelStore.storeMyChannelMember(member); ChannelStore.setUnreadCount(channelId); AsyncClient.setLastViewedAt(lastViewed, channelId); } @@ -153,9 +157,156 @@ export function getFlaggedPosts() { results: data, is_flagged_posts: true }); + + loadProfilesForPosts(data.posts); }, (err) => { AsyncClient.dispatchError(err, 'getFlaggedPosts'); } ); } + +export function loadPosts(channelId = ChannelStore.getCurrentId()) { + const postList = PostStore.getAllPosts(channelId); + const latestPostTime = PostStore.getLatestPostFromPageTime(channelId); + + if (!postList || Object.keys(postList).length === 0 || postList.order.length < Constants.POST_CHUNK_SIZE || latestPostTime === 0) { + loadPostsPage(channelId, Constants.POST_CHUNK_SIZE); + return; + } + + Client.getPosts( + channelId, + latestPostTime, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POSTS, + id: channelId, + before: true, + numRequested: 0, + post_list: data + }); + + loadProfilesForPosts(data.posts); + loadStatusesForChannel(channelId); + }, + (err) => { + AsyncClient.dispatchError(err, 'loadPosts'); + } + ); +} + +export function loadPostsPage(channelId = ChannelStore.getCurrentId(), max = Constants.POST_CHUNK_SIZE) { + const postList = PostStore.getAllPosts(channelId); + + // if we already have more than POST_CHUNK_SIZE posts, + // let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE, + // with a max + let numPosts = Math.min(max, Constants.POST_CHUNK_SIZE); + if (postList && postList.order.length > 0) { + numPosts = Math.min(max, Constants.POST_CHUNK_SIZE * Math.ceil(postList.order.length / Constants.POST_CHUNK_SIZE)); + } + + Client.getPostsPage( + channelId, + 0, + numPosts, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POSTS, + id: channelId, + before: true, + numRequested: numPosts, + checkLatest: true, + post_list: data + }); + + loadProfilesForPosts(data.posts); + loadStatusesForChannel(channelId); + }, + (err) => { + AsyncClient.dispatchError(err, 'loadPostsPage'); + } + ); +} + +export function loadPostsBefore(postId, offset, numPost, isPost) { + const channelId = ChannelStore.getCurrentId(); + if (channelId == null) { + return; + } + + Client.getPostsBefore( + channelId, + postId, + offset, + numPost, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POSTS, + id: channelId, + before: true, + numRequested: numPost, + post_list: data, + isPost + }); + + loadProfilesForPosts(data.posts); + loadStatusesForChannel(channelId); + }, + (err) => { + AsyncClient.dispatchError(err, 'loadPostsBefore'); + } + ); +} + +export function loadPostsAfter(postId, offset, numPost, isPost) { + const channelId = ChannelStore.getCurrentId(); + if (channelId == null) { + return; + } + + Client.getPostsAfter( + channelId, + postId, + offset, + numPost, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POSTS, + id: channelId, + before: false, + numRequested: numPost, + post_list: data, + isPost + }); + + loadProfilesForPosts(data.posts); + loadStatusesForChannel(channelId); + }, + (err) => { + AsyncClient.dispatchError(err, 'loadPostsAfter'); + } + ); +} + +function loadProfilesForPosts(posts) { + const profilesToLoad = {}; + for (const pid in posts) { + if (!posts.hasOwnProperty(pid)) { + continue; + } + + const post = posts[pid]; + if (!UserStore.hasProfile(post.user_id)) { + profilesToLoad[post.user_id] = true; + } + } + + const list = Object.keys(profilesToLoad); + if (list.length === 0) { + return; + } + + AsyncClient.getProfilesByIds(list); +} diff --git a/webapp/actions/status_actions.jsx b/webapp/actions/status_actions.jsx new file mode 100644 index 000000000..c198c52ac --- /dev/null +++ b/webapp/actions/status_actions.jsx @@ -0,0 +1,133 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; + +import ChannelStore from 'stores/channel_store.jsx'; +import PostStore from 'stores/post_store.jsx'; +import PreferenceStore from 'stores/preference_store.jsx'; + +import Client from 'client/web_client.jsx'; + +import {ActionTypes, Preferences, Constants} from 'utils/constants.jsx'; + +export function loadStatusesForChannel(channelId = ChannelStore.getCurrentId()) { + const postList = PostStore.getVisiblePosts(channelId); + if (!postList || !postList.posts) { + return; + } + + const statusesToLoad = {}; + for (const pid in postList.posts) { + if (!postList.posts.hasOwnProperty(pid)) { + continue; + } + + const post = postList.posts[pid]; + statusesToLoad[post.user_id] = true; + } + + loadStatusesByIds(Object.keys(statusesToLoad)); +} + +export function loadStatusesForDMSidebar() { + const dmPrefs = PreferenceStore.getCategory(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); + const statusesToLoad = []; + + for (const [key, value] of dmPrefs) { + if (value === 'true') { + statusesToLoad.push(key); + } + } + + loadStatusesByIds(statusesToLoad); +} + +export function loadStatusesForChannelAndSidebar() { + const statusesToLoad = {}; + + const channelId = ChannelStore.getCurrentId(); + const postList = PostStore.getVisiblePosts(channelId); + if (postList && postList.posts) { + for (const pid in postList.posts) { + if (!postList.posts.hasOwnProperty(pid)) { + continue; + } + + const post = postList.posts[pid]; + statusesToLoad[post.user_id] = true; + } + } + + const dmPrefs = PreferenceStore.getCategory(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); + + for (const [key, value] of dmPrefs) { + if (value === 'true') { + statusesToLoad[key] = true; + } + } + + loadStatusesByIds(Object.keys(statusesToLoad)); +} + +export function loadStatusesForProfilesList(users) { + if (users == null) { + return; + } + + const statusesToLoad = []; + for (let i = 0; i < users.length; i++) { + statusesToLoad.push(users[i].id); + } + + loadStatusesByIds(statusesToLoad); +} + +export function loadStatusesForProfilesMap(users) { + if (users == null) { + return; + } + + const statusesToLoad = []; + for (const userId in users) { + if (!users.hasOwnProperty(userId)) { + return; + } + statusesToLoad.push(userId); + } + + loadStatusesByIds(statusesToLoad); +} + +export function loadStatusesByIds(userIds) { + if (userIds.length === 0) { + return; + } + + Client.getStatusesByIds( + userIds, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_STATUSES, + statuses: data + }); + } + ); +} + +let intervalId = ''; + +export function startPeriodicStatusUpdates() { + clearInterval(intervalId); + + intervalId = setInterval( + () => { + loadStatusesForChannelAndSidebar(); + }, + Constants.STATUS_INTERVAL + ); +} + +export function stopPeriodicStatusUpdates() { + clearInterval(intervalId); +} diff --git a/webapp/actions/team_actions.jsx b/webapp/actions/team_actions.jsx index 3bf25c193..e0403529e 100644 --- a/webapp/actions/team_actions.jsx +++ b/webapp/actions/team_actions.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; @@ -19,8 +20,6 @@ export function checkIfTeamExists(teamName, onSuccess, onError) { export function createTeam(team, onSuccess, onError) { Client.createTeam(team, (rteam) => { - AsyncClient.getDirectProfiles(); - AppDispatcher.handleServerAction({ type: ActionTypes.CREATED_TEAM, team: rteam, @@ -36,3 +35,25 @@ export function createTeam(team, onSuccess, onError) { onError ); } + +export function removeUserFromTeam(teamId, userId, success, error) { + Client.removeUserFromTeam( + teamId, + userId, + () => { + TeamStore.removeMemberInTeam(teamId, userId); + AsyncClient.getUser(userId); + + if (success) { + success(); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'removeUserFromTeam'); + + if (error) { + error(err); + } + } + ); +} diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx index 2d5fd805c..900353701 100644 --- a/webapp/actions/user_actions.jsx +++ b/webapp/actions/user_actions.jsx @@ -2,12 +2,17 @@ // See License.txt for license information. import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; -import Client from 'client/web_client.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; + +import {loadStatusesForProfilesList, loadStatusesForProfilesMap} from 'actions/status_actions.jsx'; + +import {getDirectChannelName} from 'utils/utils.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import Client from 'client/web_client.jsx'; import {ActionTypes, Preferences} from 'utils/constants.jsx'; @@ -29,9 +34,179 @@ export function switchFromLdapToEmail(email, password, ldapPassword, onSuccess, ); } -export function getMoreDmList() { - AsyncClient.getTeamMembers(TeamStore.getCurrentId()); - AsyncClient.getProfilesForDirectMessageList(); +export function loadProfilesAndTeamMembers(offset, limit, teamId = TeamStore.getCurrentId(), success, error) { + Client.getProfilesInTeam( + teamId, + offset, + limit, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_PROFILES_IN_TEAM, + profiles: data, + team_id: teamId, + offset, + count: Object.keys(data).length + }); + + loadTeamMembersForProfilesMap(data, teamId, success, error); + loadStatusesForProfilesMap(data); + }, + (err) => { + AsyncClient.dispatchError(err, 'getProfilesInTeam'); + } + ); +} + +export function loadTeamMembersForProfilesMap(profiles, teamId = TeamStore.getCurrentId(), success, error) { + const membersToLoad = {}; + for (const pid in profiles) { + if (!profiles.hasOwnProperty(pid)) { + continue; + } + + if (!TeamStore.hasActiveMemberInTeam(teamId, pid)) { + membersToLoad[pid] = true; + } + } + + const list = Object.keys(membersToLoad); + if (list.length === 0) { + if (success) { + success({}); + } + return; + } + + loadTeamMembersForProfiles(list, teamId, success, error); +} + +export function loadTeamMembersForProfilesList(profiles, teamId = TeamStore.getCurrentId(), success, error) { + const membersToLoad = {}; + for (let i = 0; i < profiles.length; i++) { + const pid = profiles[i].id; + + if (!TeamStore.hasActiveMemberInTeam(teamId, pid)) { + membersToLoad[pid] = true; + } + } + + const list = Object.keys(membersToLoad); + if (list.length === 0) { + if (success) { + success({}); + } + return; + } + + loadTeamMembersForProfiles(list, teamId, success, error); +} + +function loadTeamMembersForProfiles(userIds, teamId, success, error) { + Client.getTeamMembersByIds( + teamId, + userIds, + (data) => { + const memberMap = {}; + for (let i = 0; i < data.length; i++) { + memberMap[data[i].user_id] = data[i]; + } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_MEMBERS_IN_TEAM, + team_id: teamId, + team_members: memberMap + }); + + if (success) { + success(data); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'getTeamMembersByIds'); + + if (error) { + error(err); + } + } + ); +} + +function populateDMChannelsWithProfiles(userIds) { + const currentUserId = UserStore.getCurrentId(); + + for (let i = 0; i < userIds.length; i++) { + const channelName = getDirectChannelName(currentUserId, userIds[i]); + const channel = ChannelStore.getByName(channelName); + if (channel) { + UserStore.saveUserIdInChannel(channel.id, userIds[i]); + } + } +} + +export function loadProfilesAndTeamMembersForDMSidebar() { + const dmPrefs = PreferenceStore.getCategory(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); + const teamId = TeamStore.getCurrentId(); + const profilesToLoad = []; + const membersToLoad = []; + + for (const [key, value] of dmPrefs) { + if (value === 'true') { + if (!UserStore.hasProfile(key)) { + profilesToLoad.push(key); + } + membersToLoad.push(key); + } + } + + if (profilesToLoad.length > 0) { + Client.getProfilesByIds( + profilesToLoad, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_PROFILES, + profiles: data + }); + + // Use membersToLoad so we get all the DM profiles even if they were already loaded + populateDMChannelsWithProfiles(membersToLoad); + }, + (err) => { + AsyncClient.dispatchError(err, 'getProfilesByIds'); + } + ); + } else { + populateDMChannelsWithProfiles(membersToLoad); + } + + if (membersToLoad.length > 0) { + Client.getTeamMembersByIds( + teamId, + membersToLoad, + (data) => { + const memberMap = {}; + for (let i = 0; i < data.length; i++) { + memberMap[data[i].user_id] = data[i]; + } + + const nonMembersMap = {}; + for (let i = 0; i < membersToLoad.length; i++) { + if (!memberMap[membersToLoad[i]]) { + nonMembersMap[membersToLoad[i]] = true; + } + } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_MEMBERS_IN_TEAM, + team_id: teamId, + team_members: memberMap, + non_team_members: nonMembersMap + }); + }, + (err) => { + AsyncClient.dispatchError(err, 'getTeamMembersByIds'); + } + ); + } } export function saveTheme(teamId, theme, onSuccess, onError) { @@ -82,3 +257,62 @@ function onThemeSaved(teamId, theme, onSuccess) { onSuccess(); } + +export function searchUsers(term, teamId = TeamStore.getCurrentId(), options = {}, success, error) { + Client.searchUsers( + term, + teamId, + options, + (data) => { + loadStatusesForProfilesList(data); + + if (success) { + success(data); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'searchUsers'); + + if (error) { + error(err); + } + } + ); +} + +export function autocompleteUsersInChannel(username, channelId, success, error) { + Client.autocompleteUsersInChannel( + username, + channelId, + (data) => { + if (success) { + success(data); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'autocompleteUsersInChannel'); + + if (error) { + error(err); + } + } + ); +} + +export function autocompleteUsersInTeam(username, success, error) { + Client.autocompleteUsersInTeam( + username, + (data) => { + if (success) { + success(data); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'autocompleteUsersInTeam'); + + if (error) { + error(err); + } + } + ); +} diff --git a/webapp/actions/websocket_actions.jsx b/webapp/actions/websocket_actions.jsx index 08449b87e..14a150692 100644 --- a/webapp/actions/websocket_actions.jsx +++ b/webapp/actions/websocket_actions.jsx @@ -3,8 +3,6 @@ import $ from 'jquery'; -import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; - import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import PostStore from 'stores/post_store.jsx'; @@ -20,10 +18,11 @@ import * as Utils from 'utils/utils.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; -import * as UserActions from 'actions/user_actions.jsx'; -import {handleNewPost} from 'actions/post_actions.jsx'; +import {handleNewPost, loadPosts} from 'actions/post_actions.jsx'; +import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx'; +import * as StatusActions from 'actions/status_actions.jsx'; -import {Constants, SocketEvents, ActionTypes} from 'utils/constants.jsx'; +import {Constants, SocketEvents, UserStatuses} from 'utils/constants.jsx'; import {browserHistory} from 'react-router/es6'; @@ -53,6 +52,7 @@ export function initialize() { connUrl += Client.getUsersRoute() + '/websocket'; WebSocketClient.setEventCallback(handleEvent); + WebSocketClient.setFirstConnectCallback(handleFirstConnect); WebSocketClient.setReconnectCallback(handleReconnect); WebSocketClient.setCloseCallback(handleClose); WebSocketClient.initialize(connUrl); @@ -64,22 +64,19 @@ export function close() { } export function getStatuses() { - WebSocketClient.getStatuses( - (resp) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_STATUSES, - statuses: resp.data - }); - } - ); + StatusActions.loadStatusesForChannelAndSidebar(); +} + +function handleFirstConnect() { + getStatuses(); + ErrorStore.clearLastError(); + ErrorStore.emitChange(); } function handleReconnect() { if (Client.teamId) { AsyncClient.getChannels(); - AsyncClient.getPosts(ChannelStore.getCurrentId()); - AsyncClient.getTeamMembers(TeamStore.getCurrentId()); - AsyncClient.getProfiles(); + loadPosts(ChannelStore.getCurrentId()); } getStatuses(); @@ -112,7 +109,7 @@ function handleEvent(msg) { break; case SocketEvents.NEW_USER: - handleNewUserEvent(); + handleNewUserEvent(msg); break; case SocketEvents.LEAVE_TEAM: @@ -170,6 +167,10 @@ function handleEvent(msg) { function handleNewPostEvent(msg) { const post = JSON.parse(msg.data.post); handleNewPost(post, msg); + + if (UserStore.getStatus(post.user_id) !== UserStatuses.ONLINE) { + StatusActions.loadStatusesByIds([post.user_id]); + } } function handlePostEditEvent(msg) { @@ -196,36 +197,33 @@ function handlePostDeleteEvent(msg) { } } -function handleNewUserEvent() { - AsyncClient.getTeamMembers(TeamStore.getCurrentId()); - AsyncClient.getProfiles(); - AsyncClient.getDirectProfiles(); - AsyncClient.getChannelExtraInfo(); +function handleNewUserEvent(msg) { + AsyncClient.getUser(msg.user_id); + AsyncClient.getChannelStats(); + loadProfilesAndTeamMembersForDMSidebar(); } function handleLeaveTeamEvent(msg) { if (UserStore.getCurrentId() === msg.data.user_id) { - TeamStore.removeTeamMember(msg.broadcast.team_id); + TeamStore.removeMyTeamMember(msg.broadcast.team_id); - // if the are on the team begin removed redirect them to the root + // if they are on the team being removed redirect them to the root if (TeamStore.getCurrentId() === msg.broadcast.team_id) { TeamStore.setCurrentId(''); Client.setTeamId(''); browserHistory.push('/'); } - } else if (TeamStore.getCurrentId() === msg.broadcast.team_id) { - UserActions.getMoreDmList(); } } function handleDirectAddedEvent(msg) { AsyncClient.getChannel(msg.broadcast.channel_id); - AsyncClient.getDirectProfiles(); + loadProfilesAndTeamMembersForDMSidebar(); } function handleUserAddedEvent(msg) { if (ChannelStore.getCurrentId() === msg.broadcast.channel_id) { - AsyncClient.getChannelExtraInfo(); + AsyncClient.getChannelStats(); } if (TeamStore.getCurrentId() === msg.data.team_id && UserStore.getCurrentId() === msg.data.user_id) { @@ -248,7 +246,7 @@ function handleUserRemovedEvent(msg) { $('#removed_from_channel').modal('show'); } } else if (ChannelStore.getCurrentId() === msg.broadcast.channel_id) { - AsyncClient.getChannelExtraInfo(); + AsyncClient.getChannelStats(); } } @@ -287,6 +285,10 @@ function handlePreferenceChangedEvent(msg) { function handleUserTypingEvent(msg) { GlobalActions.emitRemoteUserTypingEvent(msg.broadcast.channel_id, msg.data.user_id, msg.data.parent_id); + + if (UserStore.getStatus(msg.data.user_id) !== UserStatuses.ONLINE) { + StatusActions.loadStatusesByIds([msg.data.user_id]); + } } function handleStatusChangedEvent(msg) { @@ -301,4 +303,4 @@ function handleHelloEvent(msg) { function handleWebrtc(msg) { const data = msg.data; return WebrtcActions.handle(data); -}
\ No newline at end of file +} diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index 334f8374d..596242e41 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -73,11 +73,7 @@ export default class Client { return `${this.url}${this.urlVersion}/teams`; } - getTeamNeededRoute() { - return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}`; - } - - getTeamNeededManualRoute(teamId) { + getTeamNeededRoute(teamId = this.getTeamId()) { return `${this.url}${this.urlVersion}/teams/${teamId}`; } @@ -565,15 +561,43 @@ export default class Client { end(this.handleResponse.bind(this, 'getMyTeam', success, error)); } - getTeamMembers(teamId, success, error) { + getTeamMembers(teamId, offset, limit, success, error) { request. - get(`${this.getTeamsRoute()}/members/${teamId}`). + get(`${this.getTeamNeededRoute(teamId)}/members/${offset}/${limit}`). set(this.defaultHeaders). type('application/json'). accept('application/json'). end(this.handleResponse.bind(this, 'getTeamMembers', success, error)); } + getTeamMember(teamId, userId, success, error) { + request. + get(`${this.getTeamNeededRoute(teamId)}/members/${userId}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getTeamMember', success, error)); + } + + getTeamMembersByIds(teamId, userIds, success, error) { + request. + post(`${this.getTeamNeededRoute(teamId)}/members/ids`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(userIds). + end(this.handleResponse.bind(this, 'getTeamMembersByIds', success, error)); + } + + getTeamStats(teamId, success, error) { + request. + get(`${this.getTeamNeededRoute(teamId)}/stats`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getTeamStats', success, error)); + } + inviteMembers(data, success, error) { request. post(`${this.getTeamNeededRoute()}/invite_members`). @@ -740,7 +764,7 @@ export default class Client { }; request. - post(`${this.getTeamNeededManualRoute(teamId)}/update_member_roles`). + post(`${this.getTeamNeededRoute(teamId)}/update_member_roles`). set(this.defaultHeaders). type('application/json'). accept('application/json'). @@ -1003,40 +1027,78 @@ export default class Client { end(this.handleResponse.bind(this, 'getRecentlyActiveUsers', success, error)); } - getDirectProfiles(success, error) { + getProfiles(offset, limit, success, error) { request. - get(`${this.getUsersRoute()}/direct_profiles`). + get(`${this.getUsersRoute()}/${offset}/${limit}`). set(this.defaultHeaders). type('application/json'). accept('application/json'). - end(this.handleResponse.bind(this, 'getDirectProfiles', success, error)); + end(this.handleResponse.bind(this, 'getProfiles', success, error)); } - getProfiles(success, error) { + getProfilesInTeam(teamId, offset, limit, success, error) { request. - get(`${this.getUsersRoute()}/profiles/${this.getTeamId()}`). + get(`${this.getTeamNeededRoute(teamId)}/users/${offset}/${limit}`). set(this.defaultHeaders). type('application/json'). accept('application/json'). - end(this.handleResponse.bind(this, 'getProfiles', success, error)); + end(this.handleResponse.bind(this, 'getProfilesInTeam', success, error)); + } + + getProfilesInChannel(channelId, offset, limit, success, error) { + request. + get(`${this.getChannelNeededRoute(channelId)}/users/${offset}/${limit}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getProfilesInChannel', success, error)); + } + + getProfilesNotInChannel(channelId, offset, limit, success, error) { + request. + get(`${this.getChannelNeededRoute(channelId)}/users/not_in_channel/${offset}/${limit}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getProfilesNotInChannel', success, error)); + } + + getProfilesByIds(userIds, success, error) { + request. + post(`${this.getUsersRoute()}/ids`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(userIds). + end(this.handleResponse.bind(this, 'getProfilesByIds', success, error)); + } + + searchUsers(term, teamId, options, success, error) { + request. + post(`${this.getUsersRoute()}/search`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({term, team_id: teamId, ...options}). + end(this.handleResponse.bind(this, 'searchUsers', success, error)); } - getProfilesForTeam(teamId, success, error) { + autocompleteUsersInChannel(term, channelId, success, error) { request. - get(`${this.getUsersRoute()}/profiles/${teamId}`). + get(`${this.getChannelNeededRoute(channelId)}/users/autocomplete?term=${encodeURIComponent(term)}`). set(this.defaultHeaders). type('application/json'). accept('application/json'). - end(this.handleResponse.bind(this, 'getProfilesForTeam', success, error)); + end(this.handleResponse.bind(this, 'autocompleteUsers', success, error)); } - getProfilesForDirectMessageList(success, error) { + autocompleteUsersInTeam(term, success, error) { request. - get(`${this.getUsersRoute()}/profiles_for_dm_list/${this.getTeamId()}`). + get(`${this.getTeamNeededRoute()}/users/autocomplete?term=${encodeURIComponent(term)}`). set(this.defaultHeaders). type('application/json'). accept('application/json'). - end(this.handleResponse.bind(this, 'getProfilesForDirectMessageList', success, error)); + end(this.handleResponse.bind(this, 'autocompleteUsers', success, error)); } getStatuses(success, error) { @@ -1048,6 +1110,16 @@ export default class Client { end(this.handleResponse.bind(this, 'getStatuses', success, error)); } + getStatusesByIds(userIds, success, error) { + request. + post(`${this.getUsersRoute()}/status/ids`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(userIds). + end(this.handleResponse.bind(this, 'getStatuses', success, error)); + } + setActiveChannel(id, success, error) { request. post(`${this.getUsersRoute()}/status/set_active_channel`). @@ -1285,18 +1357,22 @@ export default class Client { end(this.handleResponse.bind(this, 'getChannelCounts', success, error)); } - getChannelExtraInfo(channelId, memberLimit, success, error) { - var url = `${this.getChannelNeededRoute(channelId)}/extra_info`; - if (memberLimit) { - url += '/' + memberLimit; - } + getChannelStats(channelId, success, error) { + request. + get(`${this.getChannelNeededRoute(channelId)}/stats`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getChannelStats', success, error)); + } + getChannelMember(channelId, userId, success, error) { request. - get(url). + get(`${this.getChannelNeededRoute(channelId)}/members/${userId}`). set(this.defaultHeaders). type('application/json'). accept('application/json'). - end(this.handleResponse.bind(this, 'getChannelExtraInfo', success, error)); + end(this.handleResponse.bind(this, 'getChannelMember', success, error)); } addChannelMember(channelId, userId, success, error) { diff --git a/webapp/client/websocket_client.jsx b/webapp/client/websocket_client.jsx index aa78d8d98..035e30be5 100644 --- a/webapp/client/websocket_client.jsx +++ b/webapp/client/websocket_client.jsx @@ -12,6 +12,7 @@ export default class WebSocketClient { this.connectFailCount = 0; this.eventCallback = null; this.responseCallbacks = {}; + this.firstConnectCallback = null; this.reconnectCallback = null; this.errorCallback = null; this.closeCallback = null; @@ -29,12 +30,13 @@ export default class WebSocketClient { this.conn = new WebSocket(connectionUrl); this.conn.onopen = () => { - if (this.reconnectCallback) { - this.reconnectCallback(); - } - if (this.connectFailCount > 0) { console.log('websocket re-established connection'); //eslint-disable-line no-console + if (this.reconnectCallback) { + this.reconnectCallback(); + } + } else if (this.firstConnectCallback) { + this.firstConnectCallback(); } this.connectFailCount = 0; @@ -104,6 +106,10 @@ export default class WebSocketClient { this.eventCallback = callback; } + setFirstConnectCallback(callback) { + this.firstConnectCallback = callback; + } + setReconnectCallback(callback) { this.reconnectCallback = callback; } @@ -157,4 +163,10 @@ export default class WebSocketClient { getStatuses(callback) { this.sendMessage('get_statuses', null, callback); } + + getStatusesByIds(userIds, callback) { + const data = {}; + data.user_ids = userIds; + this.sendMessage('get_statuses_by_ids', data, callback); + } } 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 }; diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index b8b5e1249..8493c335a 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -808,8 +808,8 @@ "admin.true": "true", "admin.userList.title": "Users for {team}", "admin.userList.title2": "Users for {team} ({count})", - "admin.user_item.authServiceEmail": ", <strong>Sign-in Method:</strong> Email", - "admin.user_item.authServiceNotEmail": ", <strong>Sign-in Method:</strong> {service}", + "admin.user_item.authServiceEmail": "<strong>Sign-in Method:</strong> Email", + "admin.user_item.authServiceNotEmail": "<strong>Sign-in Method:</strong> {service}", "admin.user_item.confirmDemoteDescription": "If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you'll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command.", "admin.user_item.confirmDemoteRoleTitle": "Confirm demotion from System Admin role", "admin.user_item.confirmDemotion": "Confirm Demotion", @@ -822,8 +822,8 @@ "admin.user_item.makeSysAdmin": "Make System Admin", "admin.user_item.makeTeamAdmin": "Make Team Admin", "admin.user_item.member": "Member", - "admin.user_item.mfaNo": ", <strong>MFA</strong>: No", - "admin.user_item.mfaYes": ", <strong>MFA</strong>: Yes", + "admin.user_item.mfaNo": "<strong>MFA</strong>: No", + "admin.user_item.mfaYes": "<strong>MFA</strong>: Yes", "admin.user_item.resetMfa": "Remove MFA", "admin.user_item.resetPwd": "Reset Password", "admin.user_item.switchToEmail": "Switch to Email/Password", @@ -881,6 +881,9 @@ "analytics.system.totalSessions": "Total Sessions", "analytics.system.totalTeams": "Total Teams", "analytics.system.totalUsers": "Total Users", + "analytics.system.totalWebsockets" : "Websocket Conns", + "analytics.system.totalMasterDbConnections": "Master DB Conns", + "analytics.system.totalReadDbConnections": "Replica DB Conns", "analytics.team.activeUsers": "Active Users With Posts", "analytics.team.newlyCreated": "Newly Created Users", "analytics.team.privateGroups": "Private Groups", @@ -1192,13 +1195,14 @@ "file_upload.limited": "Uploads limited to {count} files maximum. Please use additional posts for more files.", "file_upload.pasted": "Image Pasted at ", "filtered_channels_list.count": "{count} {count, plural, =0 {0 channels} one {channel} other {channels}}", - "filtered_channels_list.countTotal": "{count} {count, plural, =0 {0 channels} one {channel} other {channels}} of {total} Total", + "filtered_channels_list.countTotal": "{count} {count, plural, =0 {0 channels} one {channel} other {channels}} of {total} total", "filtered_channels_list.search": "Search channels", "filtered_user_list.any_team": "All Users", "filtered_user_list.count": "{count} {count, plural, =0 {0 members} one {member} other {members}}", - "filtered_user_list.countTotal": "{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} Total", + "filtered_user_list.countTotal": "{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} total", + "filtered_user_list.countTotalPage": "{startCount, number} - {endCount, number} {count, plural, =0 {0 members} one {member} other {members}} of {total} total", "filtered_user_list.member": "Member", - "filtered_user_list.search": "Search members", + "filtered_user_list.search": "Press enter to search", "filtered_user_list.show": "Filter:", "filtered_user_list.team_only": "Members of this Team", "find_team.email": "Email", diff --git a/webapp/routes/route_team.jsx b/webapp/routes/route_team.jsx index 1b4e48a51..e63be5a5e 100644 --- a/webapp/routes/route_team.jsx +++ b/webapp/routes/route_team.jsx @@ -58,7 +58,6 @@ function preNeedsTeam(nextState, replace, callback) { // for the current url. const teamName = nextState.params.team; var team = TeamStore.getByName(teamName); - const oldTeamId = TeamStore.getCurrentId(); if (!team) { browserHistory.push('/'); @@ -70,15 +69,7 @@ function preNeedsTeam(nextState, replace, callback) { TeamStore.saveMyTeam(team); TeamStore.emitChange(); - // If the old team id is null then we will already have the direct - // profiles from initial load - if (oldTeamId != null) { - AsyncClient.getDirectProfiles(); - } - var d1 = $.Deferred(); //eslint-disable-line new-cap - var d2 = $.Deferred(); //eslint-disable-line new-cap - var d3 = $.Deferred(); //eslint-disable-line new-cap Client.getChannels( (data) => { @@ -96,38 +87,7 @@ function preNeedsTeam(nextState, replace, callback) { } ); - Client.getProfiles( - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES, - profiles: data - }); - - d2.resolve(); - }, - (err) => { - AsyncClient.dispatchError(err, 'getProfiles'); - d2.resolve(); - } - ); - - Client.getTeamMembers( - TeamStore.getCurrentId(), - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_MEMBERS_FOR_TEAM, - team_members: data - }); - - d3.resolve(); - }, - (err) => { - AsyncClient.dispatchError(err, 'getTeamMembers'); - d3.resolve(); - } - ); - - $.when(d1, d2, d3).done(() => { + $.when(d1).done(() => { callback(); }); } diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss index 00fd838f0..53c52fdf1 100644 --- a/webapp/sass/components/_modal.scss +++ b/webapp/sass/components/_modal.scss @@ -236,7 +236,7 @@ &.more-channel__modal { .modal-body { overflow-x: hidden; - padding: 10px 0 20px; + padding: 10px 0 15px; } .channel-count { @@ -457,12 +457,12 @@ .modal-body { overflow-x: hidden; - padding: 10px 0 20px; + padding: 10px 0 15px; } .filter-row { @include clearfix; - margin: 10px 0; + margin: 5px 0 10px; } .member-count { @@ -602,9 +602,16 @@ } } +.member-select__container { + position: absolute; + right: 15px; + top: 15px; +} + .filtered-user-list { display: flex; flex-direction: column; + width: 100%; .filter-row { flex-grow: 0; @@ -615,4 +622,20 @@ flex-grow: 1; flex-shrink: 1; } + + .filter-controls { + @include clearfix; + flex-grow: 0; + flex-shrink: 0; + padding: 1em 1.5em 0; + + .filter-control__next { + float: right; + } + } + + .filter-button { + margin-left: 0; + padding-left: 0; + } } diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss index 9a0190ebd..a3e7ab5f5 100644 --- a/webapp/sass/responsive/_mobile.scss +++ b/webapp/sass/responsive/_mobile.scss @@ -96,7 +96,13 @@ } .member-select__container { + margin-bottom: 10px; margin-top: 10px; + overflow: hidden; + position: relative; + right: 10px; + top: 0; + width: 100%; } .user-popover { @@ -844,6 +850,10 @@ @include translate3d(0, 0, 0); } + .nav-pills__container { + height: 100%; + } + > div { padding-bottom: 70px; } diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss index cbed38a8b..83c68dc6b 100644 --- a/webapp/sass/routes/_admin-console.scss +++ b/webapp/sass/routes/_admin-console.scss @@ -239,6 +239,19 @@ } } + .more-modal__list { + .filtered-user-list { + .filter-controls { + padding-bottom: 1em; + } + } + + .filter-row { + margin: 10px 0; + overflow: hidden; + } + } + .member-list-holder { background: $white; margin-bottom: 4em; @@ -451,4 +464,3 @@ overflow: hidden; text-overflow: ellipsis; } - diff --git a/webapp/stores/channel_store.jsx b/webapp/stores/channel_store.jsx index 1870ad15b..4d3042be7 100644 --- a/webapp/stores/channel_store.jsx +++ b/webapp/stores/channel_store.jsx @@ -12,7 +12,7 @@ const NotificationPrefs = Constants.NotificationPrefs; const CHANGE_EVENT = 'change'; const LEAVE_EVENT = 'leave'; const MORE_CHANGE_EVENT = 'change'; -const EXTRA_INFO_EVENT = 'extra_info'; +const STATS_EVENT = 'stats'; const LAST_VIEVED_EVENT = 'last_viewed'; class ChannelStoreClass extends EventEmitter { @@ -21,41 +21,13 @@ class ChannelStoreClass extends EventEmitter { this.setMaxListeners(15); - this.emitChange = this.emitChange.bind(this); - this.addChangeListener = this.addChangeListener.bind(this); - this.removeChangeListener = this.removeChangeListener.bind(this); - this.emitMoreChange = this.emitMoreChange.bind(this); - this.addMoreChangeListener = this.addMoreChangeListener.bind(this); - this.removeMoreChangeListener = this.removeMoreChangeListener.bind(this); - this.emitExtraInfoChange = this.emitExtraInfoChange.bind(this); - this.addExtraInfoChangeListener = this.addExtraInfoChangeListener.bind(this); - this.removeExtraInfoChangeListener = this.removeExtraInfoChangeListener.bind(this); - this.emitLeave = this.emitLeave.bind(this); - this.addLeaveListener = this.addLeaveListener.bind(this); - this.removeLeaveListener = this.removeLeaveListener.bind(this); - this.emitLastViewed = this.emitLastViewed.bind(this); - this.addLastViewedListener = this.addLastViewedListener.bind(this); - this.removeLastViewedListener = this.removeLastViewedListener.bind(this); - this.findFirstBy = this.findFirstBy.bind(this); - this.get = this.get.bind(this); - this.getMember = this.getMember.bind(this); - this.getByName = this.getByName.bind(this); - this.getByDisplayName = this.getByDisplayName.bind(this); - this.setPostMode = this.setPostMode.bind(this); - this.getPostMode = this.getPostMode.bind(this); - this.setUnreadCount = this.setUnreadCount.bind(this); - this.setUnreadCounts = this.setUnreadCounts.bind(this); - this.getUnreadCount = this.getUnreadCount.bind(this); - this.getUnreadCounts = this.getUnreadCounts.bind(this); - this.getChannelNamesMap = this.getChannelNamesMap.bind(this); - this.currentId = null; this.postMode = this.POST_MODE_CHANNEL; this.channels = []; - this.channelMembers = {}; + this.myChannelMembers = {}; this.moreChannels = {}; this.moreChannels.loading = true; - this.extraInfos = {}; + this.stats = {}; this.unreadCounts = {}; } @@ -91,16 +63,16 @@ class ChannelStoreClass extends EventEmitter { this.removeListener(MORE_CHANGE_EVENT, callback); } - emitExtraInfoChange() { - this.emit(EXTRA_INFO_EVENT); + emitStatsChange() { + this.emit(STATS_EVENT); } - addExtraInfoChangeListener(callback) { - this.on(EXTRA_INFO_EVENT, callback); + addStatsChangeListener(callback) { + this.on(STATS_EVENT, callback); } - removeExtraInfoChangeListener(callback) { - this.removeListener(EXTRA_INFO_EVENT, callback); + removeStatsChangeListener(callback) { + this.removeListener(STATS_EVENT, callback); } emitLeave(id) { this.emit(LEAVE_EVENT, id); @@ -148,8 +120,8 @@ class ChannelStoreClass extends EventEmitter { return this.findFirstBy('id', id); } - getMember(id) { - return this.getAllMembers()[id]; + getMyMember(id) { + return this.getMyMembers()[id]; } getByName(name) { @@ -168,10 +140,6 @@ class ChannelStoreClass extends EventEmitter { return this.getChannels(); } - getAllMembers() { - return this.getChannelMembers(); - } - getMoreAll() { return this.getMoreChannels(); } @@ -181,7 +149,7 @@ class ChannelStoreClass extends EventEmitter { } resetCounts(id) { - const cm = this.channelMembers; + const cm = this.myChannelMembers; for (var cmid in cm) { if (cm[cmid].channel_id === id) { var c = this.get(id); @@ -213,41 +181,34 @@ class ChannelStoreClass extends EventEmitter { var currentId = this.getCurrentId(); if (currentId) { - return this.getAllMembers()[currentId]; + return this.getMyMembers()[currentId]; } return null; } - setChannelMember(member) { - var members = this.getChannelMembers(); - members[member.channel_id] = member; - this.storeChannelMembers(members); - this.emitChange(); + getCurrentStats() { + return this.getStats(this.getCurrentId()); } - getCurrentExtraInfo() { - return this.getExtraInfo(this.getCurrentId()); - } - - getExtraInfo(channelId) { - var extra = null; + getStats(channelId) { + let stats; if (channelId) { - extra = this.getExtraInfos()[channelId]; + stats = this.stats[channelId]; } - if (extra) { + if (stats) { // create a defensive copy - extra = JSON.parse(JSON.stringify(extra)); + stats = Object.assign({}, stats); } else { - extra = {members: []}; + stats = {member_count: 0}; } - return extra; + return stats; } - pStoreChannel(channel) { + storeChannel(channel) { var channels = this.getChannels(); var found; @@ -279,18 +240,18 @@ class ChannelStoreClass extends EventEmitter { return this.channels; } - pStoreChannelMember(channelMember) { - var members = this.getChannelMembers(); + storeMyChannelMember(channelMember) { + const members = Object.assign({}, this.getMyMembers()); members[channelMember.channel_id] = channelMember; - this.storeChannelMembers(members); + this.storeMyChannelMembers(members); } - storeChannelMembers(channelMembers) { - this.channelMembers = channelMembers; + storeMyChannelMembers(channelMembers) { + this.myChannelMembers = channelMembers; } - getChannelMembers() { - return this.channelMembers; + getMyMembers() { + return this.myChannelMembers; } storeMoreChannels(channels) { @@ -301,12 +262,8 @@ class ChannelStoreClass extends EventEmitter { return this.moreChannels; } - storeExtraInfos(extraInfos) { - this.extraInfos = extraInfos; - } - - getExtraInfos() { - return this.extraInfos; + storeStats(stats) { + this.stats = stats; } isDefault(channel) { @@ -323,7 +280,7 @@ class ChannelStoreClass extends EventEmitter { setUnreadCount(id) { const ch = this.get(id); - const chMember = this.getMember(id); + const chMember = this.getMyMember(id); const chMentionCount = chMember.mention_count; let chUnreadCount = ch.total_msg_count - chMember.msg_count; @@ -351,7 +308,7 @@ class ChannelStoreClass extends EventEmitter { } leaveChannel(id) { - Reflect.deleteProperty(this.channelMembers, id); + Reflect.deleteProperty(this.myChannelMembers, id); const element = this.channels.indexOf(id); if (element > -1) { this.channels.splice(element, 1); @@ -405,7 +362,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { case ActionTypes.RECEIVED_CHANNELS: ChannelStore.storeChannels(action.channels); - ChannelStore.storeChannelMembers(action.members); + ChannelStore.storeMyChannelMembers(action.members); currentId = ChannelStore.getCurrentId(); if (currentId && window.isActive) { ChannelStore.resetCounts(currentId); @@ -415,9 +372,9 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { break; case ActionTypes.RECEIVED_CHANNEL: - ChannelStore.pStoreChannel(action.channel); + ChannelStore.storeChannel(action.channel); if (action.member) { - ChannelStore.pStoreChannelMember(action.member); + ChannelStore.storeMyChannelMember(action.member); } currentId = ChannelStore.getCurrentId(); if (currentId && window.isActive) { @@ -432,11 +389,11 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { ChannelStore.emitMoreChange(); break; - case ActionTypes.RECEIVED_CHANNEL_EXTRA_INFO: - var extraInfos = ChannelStore.getExtraInfos(); - extraInfos[action.extra_info.id] = action.extra_info; - ChannelStore.storeExtraInfos(extraInfos); - ChannelStore.emitExtraInfoChange(); + case ActionTypes.RECEIVED_CHANNEL_STATS: + var stats = Object.assign({}, ChannelStore.getStats()); + stats[action.stats.channel_id] = action.stats; + ChannelStore.storeStats(stats); + ChannelStore.emitStatsChange(); break; case ActionTypes.LEAVE_CHANNEL: diff --git a/webapp/stores/notification_store.jsx b/webapp/stores/notification_store.jsx index 02826d586..dc707b50e 100644 --- a/webapp/stores/notification_store.jsx +++ b/webapp/stores/notification_store.jsx @@ -44,7 +44,7 @@ class NotificationStoreClass extends EventEmitter { const channel = ChannelStore.get(post.channel_id); const user = UserStore.getCurrentUser(); - const member = ChannelStore.getMember(post.channel_id); + const member = ChannelStore.getMyMember(post.channel_id); let notifyLevel = member && member.notify_props ? member.notify_props.desktop : 'default'; if (notifyLevel === 'default') { diff --git a/webapp/stores/post_store.jsx b/webapp/stores/post_store.jsx index 2d0d7a674..cdd3f5860 100644 --- a/webapp/stores/post_store.jsx +++ b/webapp/stores/post_store.jsx @@ -178,15 +178,15 @@ class PostStoreClass extends EventEmitter { } // Returns true if posts need to be fetched - requestVisibilityIncrease(id, ammount) { + requestVisibilityIncrease(id, amount) { const endVisible = this.postsInfo[id].endVisible; const postList = this.postsInfo[id].postList; if (this.getVisibilityAtTop(id)) { return false; } - this.postsInfo[id].endVisible += ammount; + this.postsInfo[id].endVisible += amount; this.emitChange(); - return endVisible + ammount > postList.order.length; + return endVisible + amount > postList.order.length; } getFocusedPostId() { diff --git a/webapp/stores/suggestion_store.jsx b/webapp/stores/suggestion_store.jsx index c59c26a66..c528f7360 100644 --- a/webapp/stores/suggestion_store.jsx +++ b/webapp/stores/suggestion_store.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import Constants from 'utils/constants.jsx'; import EventEmitter from 'events'; @@ -222,7 +222,9 @@ class SuggestionStore extends EventEmitter { switch (type) { case ActionTypes.SUGGESTION_PRETEXT_CHANGED: - this.clearSuggestions(id); + if (other.pretext === '') { + this.clearSuggestions(id); + } this.setPretext(id, other.pretext); this.emitPretextChanged(id, other.pretext); @@ -231,6 +233,8 @@ class SuggestionStore extends EventEmitter { this.emitSuggestionsChanged(id); break; case ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS: + this.clearSuggestions(id); + // ensure the matched pretext hasn't changed so that we don't receive suggestions for outdated pretext this.addSuggestions(id, other.terms, other.items, other.component, other.matchedPretext); diff --git a/webapp/stores/team_store.jsx b/webapp/stores/team_store.jsx index c71cc685b..3a4ae73b9 100644 --- a/webapp/stores/team_store.jsx +++ b/webapp/stores/team_store.jsx @@ -9,6 +9,7 @@ import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; const CHANGE_EVENT = 'change'; +const STATS_EVENT = 'stats'; var Utils; @@ -20,8 +21,10 @@ class TeamStoreClass extends EventEmitter { clear() { this.teams = {}; - this.team_members = []; - this.members_for_team = []; + this.my_team_members = []; + this.members_in_team = {}; + this.members_not_in_team = {}; + this.stats = {}; this.teamListings = {}; this.currentTeamId = ''; } @@ -38,6 +41,18 @@ class TeamStoreClass extends EventEmitter { this.removeListener(CHANGE_EVENT, callback); } + emitStatsChange() { + this.emit(STATS_EVENT); + } + + addStatsChangeListener(callback) { + this.on(STATS_EVENT, callback); + } + + removeStatsChangeListener(callback) { + this.removeListener(STATS_EVENT, callback); + } + get(id) { var c = this.getAll(); return c[id]; @@ -114,6 +129,27 @@ class TeamStoreClass extends EventEmitter { return origin + '/' + team.name; } + getCurrentStats() { + return this.getStats(this.getCurrentId()); + } + + getStats(teamId) { + let stats; + + if (teamId) { + stats = this.stats[teamId]; + } + + if (stats) { + // create a defensive copy + stats = Object.assign({}, stats); + } else { + stats = {member_count: 0}; + } + + return stats; + } + saveTeam(team) { this.teams[team.id] = team; } @@ -127,44 +163,62 @@ class TeamStoreClass extends EventEmitter { this.currentTeamId = team.id; } - saveTeamMembers(members) { - this.team_members = members; + saveStats(teamId, stats) { + this.stats[teamId] = stats; } - appendTeamMember(member) { - this.team_members.push(member); + saveMyTeamMembers(members) { + this.my_team_members = members; } - removeTeamMember(teamId) { - for (var index in this.team_members) { - if (this.team_members.hasOwnProperty(index)) { - if (this.team_members[index].team_id === teamId) { - this.team_members.splice(index, 1); + appendMyTeamMember(member) { + this.my_team_members.push(member); + } + + removeMyTeamMember(teamId) { + for (var index in this.my_team_members) { + if (this.my_team_members.hasOwnProperty(index)) { + if (this.my_team_members[index].team_id === teamId) { + Reflect.deleteProperty(this.my_team_members, index); } } } } - getTeamMembers() { - return this.team_members; + getMyTeamMembers() { + return this.my_team_members; } - saveMembersForTeam(members) { - this.members_for_team = members; + saveMembersInTeam(teamId = this.getCurrentId(), members) { + const oldMembers = this.members_in_team[teamId] || {}; + this.members_in_team[teamId] = Object.assign({}, oldMembers, members); } - getMembersForTeam() { - return this.members_for_team; + saveMembersNotInTeam(teamId = this.getCurrentId(), nonmembers) { + this.members_not_in_team[teamId] = nonmembers; } - hasActiveMemberForTeam(userId) { - for (var index in this.members_for_team) { - if (this.members_for_team.hasOwnProperty(index)) { - if (this.members_for_team[index].user_id === userId && - this.members_for_team[index].team_id === this.currentTeamId) { - return this.members_for_team[index].delete_at === 0; - } - } + removeMemberInTeam(teamId = this.getCurrentId(), userId) { + if (this.members_in_team[teamId]) { + Reflect.deleteProperty(this.members_in_team[teamId], userId); + } + } + + getMembersInTeam(teamId = this.getCurrentId()) { + return this.members_in_team[teamId] || {}; + } + + hasActiveMemberInTeam(teamId = this.getCurrentId(), userId) { + if (this.members_in_team[teamId] && this.members_in_team[teamId][userId]) { + return true; + } + + return false; + } + + hasMemberNotInTeam(teamId = this.getCurrentId(), userId) { + if (this.members_not_in_team[teamId] && this.members_not_in_team[teamId][userId]) { + return true; } return false; @@ -187,7 +241,7 @@ class TeamStoreClass extends EventEmitter { Utils = require('utils/utils.jsx'); //eslint-disable-line global-require } - var teamMembers = this.getTeamMembers(); + var teamMembers = this.getMyTeamMembers(); const teamMember = teamMembers.find((m) => m.user_id === userId && m.team_id === teamId); if (teamMember) { @@ -210,25 +264,32 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => { break; case ActionTypes.CREATED_TEAM: TeamStore.saveTeam(action.team); - TeamStore.appendTeamMember(action.member); + TeamStore.appendMyTeamMember(action.member); TeamStore.emitChange(); break; case ActionTypes.RECEIVED_ALL_TEAMS: TeamStore.saveTeams(action.teams); TeamStore.emitChange(); break; - case ActionTypes.RECEIVED_TEAM_MEMBERS: - TeamStore.saveTeamMembers(action.team_members); + case ActionTypes.RECEIVED_MY_TEAM_MEMBERS: + TeamStore.saveMyTeamMembers(action.team_members); TeamStore.emitChange(); break; case ActionTypes.RECEIVED_ALL_TEAM_LISTINGS: TeamStore.saveTeamListings(action.teams); TeamStore.emitChange(); break; - case ActionTypes.RECEIVED_MEMBERS_FOR_TEAM: - TeamStore.saveMembersForTeam(action.team_members); + case ActionTypes.RECEIVED_MEMBERS_IN_TEAM: + TeamStore.saveMembersInTeam(action.team_id, action.team_members); + if (action.non_team_members) { + TeamStore.saveMembersNotInTeam(action.team_id, action.non_team_members); + } TeamStore.emitChange(); break; + case ActionTypes.RECEIVED_TEAM_STATS: + TeamStore.saveStats(action.team_id, action.stats); + TeamStore.emitStatsChange(); + break; default: } }); diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx index 859f385c0..d93848670 100644 --- a/webapp/stores/user_store.jsx +++ b/webapp/stores/user_store.jsx @@ -6,12 +6,16 @@ import EventEmitter from 'events'; import * as GlobalActions from 'actions/global_actions.jsx'; import LocalizationStore from './localization_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; const UserStatuses = Constants.UserStatuses; -const CHANGE_EVENT_DM_LIST = 'change_dm_list'; +const CHANGE_EVENT_NOT_IN_CHANNEL = 'change_not_in_channel'; +const CHANGE_EVENT_IN_CHANNEL = 'change_in_channel'; +const CHANGE_EVENT_IN_TEAM = 'change_in_team'; const CHANGE_EVENT = 'change'; const CHANGE_EVENT_SESSIONS = 'change_sessions'; const CHANGE_EVENT_AUDITS = 'change_audits'; @@ -26,9 +30,26 @@ class UserStoreClass extends EventEmitter { } clear() { - this.profiles_for_dm_list = {}; + // All the profiles, regardless of where they came from this.profiles = {}; - this.direct_profiles = {}; + this.paging_offset = 0; + this.paging_count = 0; + + // Lists of sorted IDs for users in a team + this.profiles_in_team = {}; + this.in_team_offset = 0; + this.in_team_count = 0; + + // Lists of sorted IDs for users in a channel + this.profiles_in_channel = {}; + this.in_channel_offset = {}; + this.in_channel_count = {}; + + // Lists of sorted IDs for users not in a channel + this.profiles_not_in_channel = {}; + this.not_in_channel_offset = {}; + this.not_in_channel_count = {}; + this.statuses = {}; this.sessions = {}; this.audits = {}; @@ -48,16 +69,40 @@ class UserStoreClass extends EventEmitter { this.removeListener(CHANGE_EVENT, callback); } - emitDmListChange() { - this.emit(CHANGE_EVENT_DM_LIST); + emitInTeamChange() { + this.emit(CHANGE_EVENT_IN_TEAM); + } + + addInTeamChangeListener(callback) { + this.on(CHANGE_EVENT_IN_TEAM, callback); } - addDmListChangeListener(callback) { - this.on(CHANGE_EVENT_DM_LIST, callback); + removeInTeamChangeListener(callback) { + this.removeListener(CHANGE_EVENT_IN_TEAM, callback); } - removeDmListChangeListener(callback) { - this.removeListener(CHANGE_EVENT_DM_LIST, callback); + emitInChannelChange() { + this.emit(CHANGE_EVENT_IN_CHANNEL); + } + + addInChannelChangeListener(callback) { + this.on(CHANGE_EVENT_IN_CHANNEL, callback); + } + + removeInChannelChangeListener(callback) { + this.removeListener(CHANGE_EVENT_IN_CHANNEL, callback); + } + + emitNotInChannelChange() { + this.emit(CHANGE_EVENT_NOT_IN_CHANNEL); + } + + addNotInChannelChangeListener(callback) { + this.on(CHANGE_EVENT_NOT_IN_CHANNEL, callback); + } + + removeNotInChannelChangeListener(callback) { + this.removeListener(CHANGE_EVENT_NOT_IN_CHANNEL, callback); } emitSessionsChange() { @@ -96,6 +141,8 @@ class UserStoreClass extends EventEmitter { this.removeListener(CHANGE_EVENT_STATUSES, callback); } + // General + getCurrentUser() { return this.getProfiles()[this.currentUserId]; } @@ -119,29 +166,30 @@ class UserStoreClass extends EventEmitter { return null; } - hasProfile(userId) { - return this.getProfile(userId) != null; - } + // System-Wide Profiles - hasTeamProfile(userId) { - return this.getProfiles()[userId]; + saveProfiles(profiles) { + const currentId = this.getCurrentId(); + if (profiles[currentId]) { + Reflect.deleteProperty(profiles, currentId); + } + this.profiles = Object.assign({}, this.profiles, profiles); } - hasDirectProfile(userId) { - return this.getDirectProfiles()[userId]; + getProfiles() { + return this.profiles; } getProfile(userId) { - if (userId === this.getCurrentId()) { - return this.getCurrentUser(); + if (this.profiles[userId]) { + return Object.assign({}, this.profiles[userId]); } - const user = this.getProfiles()[userId]; - if (user) { - return user; - } + return null; + } - return this.getDirectProfiles()[userId]; + hasProfile(userId) { + return this.getProfile(userId) != null; } getProfileByUsername(username) { @@ -162,22 +210,6 @@ class UserStoreClass extends EventEmitter { return profileUsernameMap; } - getDirectProfiles() { - return this.direct_profiles; - } - - saveDirectProfile(profile) { - this.direct_profiles[profile.id] = profile; - } - - saveDirectProfiles(profiles) { - this.direct_profiles = profiles; - } - - getProfiles() { - return this.profiles; - } - getActiveOnlyProfiles(skipCurrent) { const active = {}; const profiles = this.getProfiles(); @@ -195,14 +227,54 @@ class UserStoreClass extends EventEmitter { getActiveOnlyProfileList() { const profileMap = this.getActiveOnlyProfiles(); const profiles = []; - const currentId = this.getCurrentId(); for (const id in profileMap) { - if (profileMap.hasOwnProperty(id) && id !== currentId) { + if (profileMap.hasOwnProperty(id)) { profiles.push(profileMap[id]); } } + profiles.sort((a, b) => { + if (a.username < b.username) { + return -1; + } + if (a.username > b.username) { + return 1; + } + return 0; + }); + + return profiles; + } + + getProfileList(skipCurrent) { + const profiles = []; + const currentId = this.getCurrentId(); + + for (const id in this.profiles) { + if (this.profiles.hasOwnProperty(id)) { + var profile = this.profiles[id]; + + if (skipCurrent && id === currentId) { + continue; + } + + if (profile.delete_at === 0) { + profiles.push(profile); + } + } + } + + profiles.sort((a, b) => { + if (a.username < b.username) { + return -1; + } + if (a.username > b.username) { + return 1; + } + return 0; + }); + return profiles; } @@ -210,44 +282,194 @@ class UserStoreClass extends EventEmitter { this.profiles[profile.id] = profile; } - saveProfiles(profiles) { + // Team-Wide Profiles + + saveProfilesInTeam(teamId, profiles) { + const oldProfileList = this.profiles_in_team[teamId] || []; + const oldProfileMap = {}; + for (let i = 0; i < oldProfileList.length; i++) { + oldProfileMap[oldProfileList[i]] = this.getProfile(oldProfileList[i]); + } + + const newProfileMap = Object.assign({}, oldProfileMap, profiles); + const newProfileList = Object.keys(newProfileMap); + + newProfileList.sort((a, b) => { + const aProfile = newProfileMap[a]; + const bProfile = newProfileMap[b]; + + if (aProfile.username < bProfile.username) { + return -1; + } + if (aProfile.username > bProfile.username) { + return 1; + } + return 0; + }); + + this.profiles_in_team[teamId] = newProfileList; + this.saveProfiles(profiles); + } + + getProfileListInTeam(teamId = TeamStore.getCurrentId(), skipCurrent) { + const userIds = this.profiles_in_team[teamId] || []; + const profiles = []; const currentId = this.getCurrentId(); - const currentUser = this.profiles[currentId]; - if (currentUser) { - if (currentId in this.profiles) { - Reflect.deleteProperty(this.profiles, currentId); + + for (let i = 0; i < userIds.length; i++) { + const profile = this.getProfile(userIds[i]); + + if (skipCurrent && profile.id === currentId) { + continue; } - this.profiles = profiles; - this.profiles[currentId] = currentUser; - } else { - this.profiles = profiles; + if (profile) { + profiles.push(profile); + } } + + return profiles; } - getProfilesForDmList() { - const currentId = this.getCurrentId(); - const profiles = []; + // Channel-Wide Profiles - for (const id in this.profiles_for_dm_list) { - if (this.profiles_for_dm_list.hasOwnProperty(id) && id !== currentId) { - var profile = this.profiles_for_dm_list[id]; + saveProfilesInChannel(channelId = ChannelStore.getCurrentId(), profiles) { + const oldProfileList = this.profiles_in_channel[channelId] || []; + const oldProfileMap = {}; + for (let i = 0; i < oldProfileList.length; i++) { + oldProfileMap[oldProfileList[i]] = this.getProfile(oldProfileList[i]); + } - if (profile.delete_at === 0) { - profiles.push(profile); - } + const newProfileMap = Object.assign({}, oldProfileMap, profiles); + const newProfileList = Object.keys(newProfileMap); + + newProfileList.sort((a, b) => { + const aProfile = newProfileMap[a]; + const bProfile = newProfileMap[b]; + + if (aProfile.username < bProfile.username) { + return -1; } + if (aProfile.username > bProfile.username) { + return 1; + } + return 0; + }); + + this.profiles_in_channel[channelId] = newProfileList; + this.saveProfiles(profiles); + } + + saveProfileInChannel(channelId = ChannelStore.getCurrentId(), profile) { + const profileMap = {}; + profileMap[profile.id] = profile; + this.saveProfilesInChannel(channelId, profileMap); + } + + saveUserIdInChannel(channelId = ChannelStore.getCurrentId(), userId) { + const profile = this.getProfile(userId); + + // Must have profile or we can't sort the list + if (!profile) { + return false; + } + + this.saveProfileInChannel(channelId, profile); + + return true; + } + + removeProfileInChannel(channelId, userId) { + const userIds = this.profiles_in_channel[channelId]; + if (!userIds) { + return; } - profiles.sort((a, b) => a.username.localeCompare(b.username)); + const index = userIds.indexOf(userId); + if (index === -1) { + return; + } + + userIds.splice(index, 1); + } + + getProfileListInChannel(channelId = ChannelStore.getCurrentId()) { + const userIds = this.profiles_in_channel[channelId] || []; + const profiles = []; + + for (let i = 0; i < userIds.length; i++) { + const profile = this.getProfile(userIds[i]); + if (profile) { + profiles.push(profile); + } + } return profiles; } - saveProfilesForDmList(profiles) { - this.profiles_for_dm_list = profiles; + saveProfilesNotInChannel(channelId = ChannelStore.getCurrentId(), profiles) { + const oldProfileList = this.profiles_not_in_channel[channelId] || []; + const oldProfileMap = {}; + for (let i = 0; i < oldProfileList.length; i++) { + oldProfileMap[oldProfileList[i]] = this.getProfile(oldProfileList[i]); + } + + const newProfileMap = Object.assign({}, oldProfileMap, profiles); + const newProfileList = Object.keys(newProfileMap); + + newProfileList.sort((a, b) => { + const aProfile = newProfileMap[a]; + const bProfile = newProfileMap[b]; + + if (aProfile.username < bProfile.username) { + return -1; + } + if (aProfile.username > bProfile.username) { + return 1; + } + return 0; + }); + + this.profiles_not_in_channel[channelId] = newProfileList; + this.saveProfiles(profiles); + } + + saveProfileNotInChannel(channelId = ChannelStore.getCurrentId(), profile) { + const profileMap = {}; + profileMap[profile.id] = profile; + this.saveProfilesNotInChannel(channelId, profileMap); + } + + removeProfileNotInChannel(channelId, userId) { + const userIds = this.profiles_not_in_channel[channelId]; + if (!userIds) { + return; + } + + const index = userIds.indexOf(userId); + if (index === -1) { + return; + } + + userIds.splice(index, 1); + } + + getProfileListNotInChannel(channelId = ChannelStore.getCurrentId()) { + const userIds = this.profiles_not_in_channel[channelId] || []; + const profiles = []; + + for (let i = 0; i < userIds.length; i++) { + const profile = this.getProfile(userIds[i]); + if (profile) { + profiles.push(profile); + } + } + + return profiles; } + // Other + setSessions(sessions) { this.sessions = sessions; } @@ -331,6 +553,58 @@ class UserStoreClass extends EventEmitter { return false; } + + setPage(offset, count) { + this.paging_offset = offset + count; + this.paging_count = this.paging_count + count; + } + + getPagingOffset() { + return this.paging_offset; + } + + getPagingCount() { + return this.paging_count; + } + + setInTeamPage(offset, count) { + this.in_team_offset = offset + count; + this.in_team_count = this.in_team_count + count; + } + + getInTeamPagingOffset() { + return this.in_team_offset; + } + + getInTeamPagingCount() { + return this.in_team_count; + } + + setInChannelPage(channelId, offset, count) { + this.in_channel_offset[channelId] = offset + count; + this.in_channel_count[channelId] = this.dm_paging_count + count; + } + + getInChannelPagingOffset(channelId) { + return this.in_channel_offset[channelId] | 0; + } + + getInChannelPagingCount(channelId) { + return this.in_channel_count[channelId] | 0; + } + + setNotInChannelPage(channelId, offset, count) { + this.not_in_channel_offset[channelId] = offset + count; + this.not_in_channel_count[channelId] = this.dm_paging_count + count; + } + + getNotInChannelPagingOffset(channelId) { + return this.not_in_channel_offset[channelId] | 0; + } + + getNotInChannelPagingCount(channelId) { + return this.not_in_channel_count[channelId] | 0; + } } var UserStore = new UserStoreClass(); @@ -340,16 +614,36 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { - case ActionTypes.RECEIVED_PROFILES_FOR_DM_LIST: - UserStore.saveProfilesForDmList(action.profiles); - UserStore.emitDmListChange(); - break; case ActionTypes.RECEIVED_PROFILES: UserStore.saveProfiles(action.profiles); + if (action.offset != null && action.count != null) { + UserStore.setPage(action.offset, action.count); + } UserStore.emitChange(); break; - case ActionTypes.RECEIVED_DIRECT_PROFILES: - UserStore.saveDirectProfiles(action.profiles); + case ActionTypes.RECEIVED_PROFILES_IN_TEAM: + UserStore.saveProfilesInTeam(action.team_id, action.profiles); + if (action.offset != null && action.count != null) { + UserStore.setInTeamPage(action.offset, action.count); + } + UserStore.emitInTeamChange(); + break; + case ActionTypes.RECEIVED_PROFILES_IN_CHANNEL: + UserStore.saveProfilesInChannel(action.channel_id, action.profiles); + if (action.offset != null && action.count != null) { + UserStore.setInChannelPage(action.offset, action.count); + } + UserStore.emitInChannelChange(); + break; + case ActionTypes.RECEIVED_PROFILES_NOT_IN_CHANNEL: + UserStore.saveProfilesNotInChannel(action.channel_id, action.profiles); + if (action.offset != null && action.count != null) { + UserStore.setNotInChannelPage(action.offset, action.count); + } + UserStore.emitNotInChannelChange(); + break; + case ActionTypes.RECEIVED_PROFILE: + UserStore.saveProfile(action.profile); UserStore.emitChange(); break; case ActionTypes.RECEIVED_ME: diff --git a/webapp/tests/client_channel.test.jsx b/webapp/tests/client_channel.test.jsx index ccfcb32a4..92145f6e1 100644 --- a/webapp/tests/client_channel.test.jsx +++ b/webapp/tests/client_channel.test.jsx @@ -285,11 +285,10 @@ describe('Client.Channels', function() { }); }); - it('getChannelExtraInfo', function(done) { + it('getChannelStats', function(done) { TestHelper.initBasic(() => { - TestHelper.basicClient().getChannelExtraInfo( + TestHelper.basicClient().getChannelStats( TestHelper.basicChannel().id, - 5, function(data) { assert.equal(data.member_count, 1); done(); @@ -301,6 +300,23 @@ describe('Client.Channels', function() { }); }); + it('getChannelMember', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getChannelMember( + TestHelper.basicChannel().id, + TestHelper.basicUser().id, + function(data) { + assert.equal(data.channel_id, TestHelper.basicChannel().id); + assert.equal(data.user_id, TestHelper.basicUser().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + /* TODO FIX THIS TEST it('addChannelMember', function(done) { TestHelper.initBasic(() => { diff --git a/webapp/tests/client_team.test.jsx b/webapp/tests/client_team.test.jsx index 157a2f4a5..642307986 100644 --- a/webapp/tests/client_team.test.jsx +++ b/webapp/tests/client_team.test.jsx @@ -130,10 +130,12 @@ describe('Client.Team', function() { }); }); - it('GetTeamMembers', function(done) { + it('getTeamMembers', function(done) { TestHelper.initBasic(() => { TestHelper.basicClient().getTeamMembers( TestHelper.basicTeam().id, + 0, + 100, function(data) { assert.equal(data.length > 0, true); done(); @@ -145,6 +147,55 @@ describe('Client.Team', function() { }); }); + it('getTeamMember', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getTeamMember( + TestHelper.basicTeam().id, + TestHelper.basicUser().id, + function(data) { + assert.equal(data.user_id, TestHelper.basicUser().id); + assert.equal(data.team_id, TestHelper.basicTeam().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getTeamStats', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getTeamStats( + TestHelper.basicTeam().id, + function(data) { + assert.equal(data.member_count > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getTeamMembersByIds', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getTeamMembersByIds( + TestHelper.basicTeam().id, + [TestHelper.basicUser().id], + function(data) { + assert.equal(data[0].user_id, TestHelper.basicUser().id); + assert.equal(data[0].team_id, TestHelper.basicTeam().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + it('inviteMembers', function(done) { TestHelper.initBasic(() => { var data = {}; diff --git a/webapp/tests/client_user.test.jsx b/webapp/tests/client_user.test.jsx index 48a0150d4..8c6f0f970 100644 --- a/webapp/tests/client_user.test.jsx +++ b/webapp/tests/client_user.test.jsx @@ -444,23 +444,28 @@ describe('Client.User', function() { }); }); - it('getDirectProfiles', function(done) { + it('getProfiles', function(done) { TestHelper.initBasic(() => { - TestHelper.basicClient().getDirectProfiles( + TestHelper.basicClient().getProfiles( + 0, + 100, function(data) { - assert.equal(Object.keys(data).length === 0, true); + assert.equal(Object.keys(data).length > 0, true); done(); }, function(err) { - done(new Error(err.getDirectProfiles)); + done(new Error(err.message)); } ); }); }); - it('getProfiles', function(done) { + it('getProfilesInTeam', function(done) { TestHelper.initBasic(() => { - TestHelper.basicClient().getProfiles( + TestHelper.basicClient().getProfilesInTeam( + TestHelper.basicTeam().id, + 0, + 100, function(data) { assert.equal(data[TestHelper.basicUser().id].id, TestHelper.basicUser().id); done(); @@ -472,10 +477,10 @@ describe('Client.User', function() { }); }); - it('getProfilesForTeam', function(done) { + it('getProfilesByIds', function(done) { TestHelper.initBasic(() => { - TestHelper.basicClient().getProfilesForTeam( - TestHelper.basicTeam().id, + TestHelper.basicClient().getProfilesByIds( + [TestHelper.basicUser().id], function(data) { assert.equal(data[TestHelper.basicUser().id].id, TestHelper.basicUser().id); done(); @@ -487,9 +492,12 @@ describe('Client.User', function() { }); }); - it('getProfilesForDirectMessageList', function(done) { + it('getProfilesInChannel', function(done) { TestHelper.initBasic(() => { - TestHelper.basicClient().getProfilesForDirectMessageList( + TestHelper.basicClient().getProfilesInChannel( + TestHelper.basicChannel().id, + 0, + 100, function(data) { assert.equal(Object.keys(data).length > 0, true); done(); @@ -501,16 +509,80 @@ describe('Client.User', function() { }); }); - /* TODO: FIX THIS TEST - it('getStatuses', function(done) { + it('getProfilesNotInChannel', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getProfilesNotInChannel( + TestHelper.basicChannel().id, + 0, + 100, + function(data) { + assert.equal(Object.keys(data).length > 0, false); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('searchUsers', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().searchUsers( + 'uid', + TestHelper.basicTeam().id, + {}, + function(data) { + assert.equal(data.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('autocompleteUsersInChannel', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().autocompleteUsersInChannel( + 'uid', + TestHelper.basicChannel().id, + function(data) { + assert.equal(data != null, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('autocompleteUsersInTeam', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().autocompleteUsersInTeam( + 'uid', + function(data) { + assert.equal(data != null, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + + it('getStatusesByIds', function(done) { TestHelper.initBasic(() => { var ids = []; ids.push(TestHelper.basicUser().id); - TestHelper.basicClient().getStatuses( + TestHelper.basicClient().getStatusesByIds( ids, function(data) { - assert.equal(data[TestHelper.basicUser().id], 'online'); + assert.equal(data[TestHelper.basicUser().id] != null, true); done(); }, function(err) { @@ -519,7 +591,6 @@ describe('Client.User', function() { ); }); }); - */ it('setActiveChannel', function(done) { TestHelper.initBasic(() => { diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index 30bc474f8..24d540929 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -1,19 +1,21 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; -import Client from 'client/web_client.jsx'; -import * as GlobalActions from 'actions/global_actions.jsx'; -import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import BrowserStore from 'stores/browser_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; -import PostStore from 'stores/post_store.jsx'; import UserStore from 'stores/user_store.jsx'; -import * as utils from './utils.jsx'; -import * as UserAgent from './user_agent.jsx'; +import TeamStore from 'stores/team_store.jsx'; import ErrorStore from 'stores/error_store.jsx'; -import Constants from './constants.jsx'; +import * as GlobalActions from 'actions/global_actions.jsx'; +import {loadStatusesForProfilesMap} from 'actions/status_actions.jsx'; + +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; +import Client from 'client/web_client.jsx'; +import * as utils from 'utils/utils.jsx'; +import * as UserAgent from 'utils/user_agent.jsx'; + +import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; const StatTypes = Constants.StatTypes; @@ -215,94 +217,89 @@ export function getMoreChannels(force) { } } -export function getChannelExtraInfo(id, memberLimit) { - let channelId; - if (id) { - channelId = id; - } else { - channelId = ChannelStore.getCurrentId(); +export function getChannelStats(channelId = ChannelStore.getCurrentId()) { + if (isCallInProgress('getChannelStats' + channelId)) { + return; } - if (channelId != null) { - if (isCallInProgress('getChannelExtraInfo_' + channelId)) { - return; - } - - callTracker['getChannelExtraInfo_' + channelId] = utils.getTimestamp(); + callTracker['getChannelStats' + channelId] = utils.getTimestamp(); - Client.getChannelExtraInfo( - channelId, - memberLimit, - (data) => { - callTracker['getChannelExtraInfo_' + channelId] = 0; + Client.getChannelStats( + channelId, + (data) => { + callTracker['getChannelStats' + channelId] = 0; - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_CHANNEL_EXTRA_INFO, - extra_info: data - }); - }, - (err) => { - callTracker['getChannelExtraInfo_' + channelId] = 0; - dispatchError(err, 'getChannelExtraInfo'); - } - ); - } + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_CHANNEL_STATS, + stats: data + }); + }, + (err) => { + callTracker['getChannelStats' + channelId] = 0; + dispatchError(err, 'getChannelStats'); + } + ); } -export function getTeamMembers(teamId) { - if (isCallInProgress('getTeamMembers')) { +export function getChannelMember(channelId, userId) { + if (isCallInProgress(`getChannelMember${channelId}${userId}`)) { return; } - callTracker.getTeamMembers = utils.getTimestamp(); - Client.getTeamMembers( - teamId, + callTracker[`getChannelMember${channelId}${userId}`] = utils.getTimestamp(); + + Client.getChannelMember( + channelId, + userId, (data) => { - callTracker.getTeamMembers = 0; + callTracker[`getChannelMember${channelId}${userId}`] = 0; AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_MEMBERS_FOR_TEAM, - team_members: data + type: ActionTypes.RECEIVED_CHANNEL_MEMBER, + member: data }); }, (err) => { - callTracker.getTeamMembers = 0; - dispatchError(err, 'getTeamMembers'); + callTracker[`getChannelMember${channelId}${userId}`] = 0; + dispatchError(err, 'getChannelMember'); } ); } -export function getProfilesForDirectMessageList() { - if (isCallInProgress('getProfilesForDirectMessageList')) { +export function getUser(userId) { + if (isCallInProgress(`getUser${userId}`)) { return; } - callTracker.getProfilesForDirectMessageList = utils.getTimestamp(); - Client.getProfilesForDirectMessageList( + callTracker[`getUser${userId}`] = utils.getTimestamp(); + Client.getUser( + userId, (data) => { - callTracker.getProfilesForDirectMessageList = 0; + callTracker[`getUser${userId}`] = 0; AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES_FOR_DM_LIST, - profiles: data + type: ActionTypes.RECEIVED_PROFILE, + profile: data }); }, (err) => { - callTracker.getProfilesForDirectMessageList = 0; - dispatchError(err, 'getProfilesForDirectMessageList'); + callTracker[`getUser${userId}`] = 0; + dispatchError(err, 'getUser'); } ); } -export function getProfiles() { - if (isCallInProgress('getProfiles')) { +export function getProfiles(offset = UserStore.getPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { + if (isCallInProgress(`getProfiles${offset}${limit}`)) { return; } - callTracker.getProfiles = utils.getTimestamp(); + callTracker[`getProfiles${offset}${limit}`] = utils.getTimestamp(); Client.getProfiles( + offset, + limit, (data) => { - callTracker.getProfiles = 0; + callTracker[`getProfiles${offset}${limit}`] = 0; AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_PROFILES, @@ -310,30 +307,123 @@ export function getProfiles() { }); }, (err) => { - callTracker.getProfiles = 0; + callTracker[`getProfiles${offset}${limit}`] = 0; dispatchError(err, 'getProfiles'); } ); } -export function getDirectProfiles() { - if (isCallInProgress('getDirectProfiles')) { +export function getProfilesInTeam(teamId = TeamStore.getCurrentId(), offset = UserStore.getInTeamPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { + if (isCallInProgress(`getProfilesInTeam${offset}${limit}`)) { + return; + } + + callTracker[`getProfilesInTeam${offset}${limit}`] = utils.getTimestamp(); + Client.getProfilesInTeam( + teamId, + offset, + limit, + (data) => { + callTracker[`getProfilesInTeam${offset}${limit}`] = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_PROFILES_IN_TEAM, + profiles: data, + team_id: teamId, + offset, + count: Object.keys(data).length + }); + }, + (err) => { + callTracker[`getProfilesInTeam${offset}${limit}`] = 0; + dispatchError(err, 'getProfilesInTeam'); + } + ); +} + +export function getProfilesInChannel(channelId = ChannelStore.getCurrentId(), offset = UserStore.getInChannelPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { + if (isCallInProgress(`getProfilesInChannel${offset}${limit}`)) { + return; + } + + callTracker[`getProfilesInChannel${offset}${limit}`] = utils.getTimestamp(); + Client.getProfilesInChannel( + channelId, + offset, + limit, + (data) => { + callTracker[`getProfilesInChannel${offset}${limit}`] = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_PROFILES_IN_CHANNEL, + channel_id: channelId, + profiles: data, + offset, + count: Object.keys(data).length + }); + + loadStatusesForProfilesMap(data); + }, + (err) => { + callTracker[`getProfilesInChannel${offset}${limit}`] = 0; + dispatchError(err, 'getProfilesInChannel'); + } + ); +} + +export function getProfilesNotInChannel(channelId = ChannelStore.getCurrentId(), offset = UserStore.getNotInChannelPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE) { + if (isCallInProgress(`getProfilesNotInChannel${offset}${limit}`)) { + return; + } + + callTracker[`getProfilesNotInChannel${offset}${limit}`] = utils.getTimestamp(); + Client.getProfilesNotInChannel( + channelId, + offset, + limit, + (data) => { + callTracker[`getProfilesNotInChannel${offset}${limit}`] = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_PROFILES_NOT_IN_CHANNEL, + channel_id: channelId, + profiles: data, + offset, + count: Object.keys(data).length + }); + + loadStatusesForProfilesMap(data); + }, + (err) => { + callTracker[`getProfilesNotInChannel${offset}${limit}`] = 0; + dispatchError(err, 'getProfilesNotInChannel'); + } + ); +} + +export function getProfilesByIds(userIds) { + if (isCallInProgress('getProfilesByIds')) { return; } - callTracker.getDirectProfiles = utils.getTimestamp(); - Client.getDirectProfiles( + if (!userIds || userIds.length === 0) { + return; + } + + callTracker.getProfilesByIds = utils.getTimestamp(); + Client.getProfilesByIds( + userIds, (data) => { - callTracker.getDirectProfiles = 0; + callTracker.getProfilesByIds = 0; AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_DIRECT_PROFILES, + type: ActionTypes.RECEIVED_PROFILES, profiles: data }); }, (err) => { - callTracker.getDirectProfiles = 0; - dispatchError(err, 'getDirectProfiles'); + callTracker.getProfilesByIds = 0; + dispatchError(err, 'getProfilesByIds'); } ); } @@ -548,173 +638,6 @@ export function search(terms, isOrSearch) { ); } -export function getPostsPage(id, maxPosts) { - let channelId = id; - if (channelId == null) { - channelId = ChannelStore.getCurrentId(); - if (channelId == null) { - return; - } - } - - if (isCallInProgress('getPostsPage_' + channelId)) { - return; - } - - var postList = PostStore.getAllPosts(id); - - var max = maxPosts; - if (max == null) { - max = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS; - } - - // if we already have more than POST_CHUNK_SIZE posts, - // let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE, - // with a max at maxPosts - var numPosts = Math.min(max, Constants.POST_CHUNK_SIZE); - if (postList && postList.order.length > 0) { - numPosts = Math.min(max, Constants.POST_CHUNK_SIZE * Math.ceil(postList.order.length / Constants.POST_CHUNK_SIZE)); - } - - if (channelId != null) { - callTracker['getPostsPage_' + channelId] = utils.getTimestamp(); - - Client.getPostsPage( - channelId, - 0, - numPosts, - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POSTS, - id: channelId, - before: true, - numRequested: numPosts, - checkLatest: true, - post_list: data - }); - }, - (err) => { - dispatchError(err, 'getPostsPage'); - }, - () => { - callTracker['getPostsPage_' + channelId] = 0; - } - ); - } -} - -export function getPosts(id) { - let channelId = id; - if (channelId == null) { - channelId = ChannelStore.getCurrentId(); - if (channelId == null) { - return; - } - } - - if (isCallInProgress('getPosts_' + channelId)) { - return; - } - - const postList = PostStore.getAllPosts(channelId); - const latestPostTime = PostStore.getLatestPostFromPageTime(id); - - if ($.isEmptyObject(postList) || postList.order.length < Constants.POST_CHUNK_SIZE || latestPostTime === 0) { - getPostsPage(channelId, Constants.POST_CHUNK_SIZE); - return; - } - - callTracker['getPosts_' + channelId] = utils.getTimestamp(); - - Client.getPosts( - channelId, - latestPostTime, - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POSTS, - id: channelId, - before: true, - numRequested: 0, - post_list: data - }); - }, - (err) => { - dispatchError(err, 'getPosts'); - }, - () => { - callTracker['getPosts_' + channelId] = 0; - } - ); -} - -export function getPostsBefore(postId, offset, numPost, isPost) { - const channelId = ChannelStore.getCurrentId(); - if (channelId == null) { - return; - } - - if (isCallInProgress('getPostsBefore_' + channelId)) { - return; - } - - Client.getPostsBefore( - channelId, - postId, - offset, - numPost, - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POSTS, - id: channelId, - before: true, - numRequested: numPost, - post_list: data, - isPost - }); - }, - (err) => { - dispatchError(err, 'getPostsBefore'); - }, - () => { - callTracker['getPostsBefore_' + channelId] = 0; - } - ); -} - -export function getPostsAfter(postId, offset, numPost, isPost) { - const channelId = ChannelStore.getCurrentId(); - if (channelId == null) { - return; - } - - if (isCallInProgress('getPostsAfter_' + channelId)) { - return; - } - - Client.getPostsAfter( - channelId, - postId, - offset, - numPost, - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POSTS, - id: channelId, - before: false, - numRequested: numPost, - post_list: data, - isPost - }); - }, - (err) => { - dispatchError(err, 'getPostsAfter'); - }, - () => { - callTracker['getPostsAfter_' + channelId] = 0; - } - ); -} - export function getFileInfosForPost(channelId, postId) { const callName = 'getFileInfosForPost' + postId; @@ -828,6 +751,58 @@ export function getMyTeam() { ); } +export function getTeamMember(teamId, userId) { + const callName = `getTeamMember${teamId}${userId}`; + if (isCallInProgress(callName)) { + return; + } + + callTracker[callName] = utils.getTimestamp(); + Client.getTeamMember( + (data) => { + callTracker[callName] = 0; + + const memberMap = {}; + memberMap[userId] = data; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_MEMBERS_IN_TEAM, + team_id: teamId, + team_members: memberMap + }); + }, + (err) => { + callTracker[callName] = 0; + dispatchError(err, 'getTeamMember'); + } + ); +} + +export function getTeamStats(teamId) { + const callName = `getTeamStats${teamId}`; + if (isCallInProgress(callName)) { + return; + } + + callTracker[callName] = utils.getTimestamp(); + Client.getTeamStats( + teamId, + (data) => { + callTracker[callName] = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_TEAM_STATS, + team_id: teamId, + stats: data + }); + }, + (err) => { + callTracker[callName] = 0; + dispatchError(err, 'getTeamStats'); + } + ); +} + export function getAllPreferences() { if (isCallInProgress('getAllPreferences')) { return; @@ -987,6 +962,18 @@ export function getStandardAnalytics(teamId) { if (data[index].name === 'team_count' && teamId == null) { stats[StatTypes.TOTAL_TEAMS] = data[index].value; } + + if (data[index].name === 'total_websocket_connections') { + stats[StatTypes.TOTAL_WEBSOCKET_CONNECTIONS] = data[index].value; + } + + if (data[index].name === 'total_master_db_connections') { + stats[StatTypes.TOTAL_MASTER_DB_CONNECTIONS] = data[index].value; + } + + if (data[index].name === 'total_read_db_connections') { + stats[StatTypes.TOTAL_READ_DB_CONNECTIONS] = data[index].value; + } } AppDispatcher.handleServerAction({ @@ -1212,54 +1199,6 @@ export function getRecentAndNewUsersAnalytics(teamId) { ); } -export function listIncomingHooks() { - if (isCallInProgress('listIncomingHooks')) { - return; - } - - callTracker.listIncomingHooks = utils.getTimestamp(); - - Client.listIncomingHooks( - (data) => { - callTracker.listIncomingHooks = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_INCOMING_WEBHOOKS, - teamId: Client.teamId, - incomingWebhooks: data - }); - }, - (err) => { - callTracker.listIncomingHooks = 0; - dispatchError(err, 'getIncomingHooks'); - } - ); -} - -export function listOutgoingHooks() { - if (isCallInProgress('listOutgoingHooks')) { - return; - } - - callTracker.listOutgoingHooks = utils.getTimestamp(); - - Client.listOutgoingHooks( - (data) => { - callTracker.listOutgoingHooks = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_OUTGOING_WEBHOOKS, - teamId: Client.teamId, - outgoingWebhooks: data - }); - }, - (err) => { - callTracker.listOutgoingHooks = 0; - dispatchError(err, 'getOutgoingHooks'); - } - ); -} - export function addIncomingHook(hook, success, error) { Client.addIncomingHook( hook, @@ -1353,30 +1292,6 @@ export function regenOutgoingHookToken(id) { ); } -export function listTeamCommands() { - if (isCallInProgress('listTeamCommands')) { - return; - } - - callTracker.listTeamCommands = utils.getTimestamp(); - - Client.listTeamCommands( - (data) => { - callTracker.listTeamCommands = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_COMMANDS, - teamId: Client.teamId, - commands: data - }); - }, - (err) => { - callTracker.listTeamCommands = 0; - dispatchError(err, 'listTeamCommands'); - } - ); -} - export function addCommand(command, success, error) { Client.addCommand( command, @@ -1459,29 +1374,6 @@ export function getPublicLink(fileId, success, error) { ); } -export function listEmoji() { - if (isCallInProgress('listEmoji')) { - return; - } - - callTracker.listEmoji = utils.getTimestamp(); - - Client.listEmoji( - (data) => { - callTracker.listEmoji = 0; - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_CUSTOM_EMOJIS, - emojis: data - }); - }, - (err) => { - callTracker.listEmoji = 0; - dispatchError(err, 'listEmoji'); - } - ); -} - export function addEmoji(emoji, image, success, error) { const callName = 'addEmoji' + emoji.name; diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 10f1a2879..83d64358c 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -71,7 +71,7 @@ export const ActionTypes = keyMirror({ RECEIVED_CHANNELS: null, RECEIVED_CHANNEL: null, RECEIVED_MORE_CHANNELS: null, - RECEIVED_CHANNEL_EXTRA_INFO: null, + RECEIVED_CHANNEL_STATS: null, FOCUS_POST: null, RECEIVED_POSTS: null, @@ -84,9 +84,11 @@ export const ActionTypes = keyMirror({ RECEIVED_MENTION_DATA: null, RECEIVED_ADD_MENTION: null, - RECEIVED_PROFILES_FOR_DM_LIST: null, RECEIVED_PROFILES: null, - RECEIVED_DIRECT_PROFILES: null, + RECEIVED_PROFILES_IN_TEAM: null, + RECEIVED_PROFILE: null, + RECEIVED_PROFILES_IN_CHANNEL: null, + RECEIVED_PROFILE_NOT_IN_CHANNEL: null, RECEIVED_ME: null, RECEIVED_SESSIONS: null, RECEIVED_AUDITS: null, @@ -129,8 +131,9 @@ export const ActionTypes = keyMirror({ RECEIVED_SERVER_COMPLIANCE_REPORTS: null, RECEIVED_ALL_TEAMS: null, RECEIVED_ALL_TEAM_LISTINGS: null, - RECEIVED_TEAM_MEMBERS: null, - RECEIVED_MEMBERS_FOR_TEAM: null, + RECEIVED_MY_TEAM_MEMBERS: null, + RECEIVED_MEMBERS_IN_TEAM: null, + RECEIVED_TEAM_STATS: null, RECEIVED_LOCALE: null, @@ -232,7 +235,10 @@ export const Constants = { POST_PER_DAY: null, USERS_WITH_POSTS_PER_DAY: null, RECENTLY_ACTIVE_USERS: null, - NEWLY_CREATED_USERS: null + NEWLY_CREATED_USERS: null, + TOTAL_WEBSOCKET_CONNECTIONS: null, + TOTAL_MASTER_DB_CONNECTIONS: null, + TOTAL_READ_DB_CONNECTIONS: null }), STAT_MAX_ACTIVE_USERS: 20, STAT_MAX_NEW_USERS: 20, @@ -313,7 +319,7 @@ export const Constants = { SIGNIN_VERIFIED: 'verified', SESSION_EXPIRED: 'expired', POST_CHUNK_SIZE: 60, - MAX_POST_CHUNKS: 3, + PROFILE_CHUNK_SIZE: 100, POST_FOCUS_CONTEXT_RADIUS: 10, POST_LOADING: 'loading', POST_FAILED: 'failed', @@ -843,7 +849,9 @@ export const Constants = { MENTION_MEMBERS: 'mention.members', MENTION_NONMEMBERS: 'mention.nonmembers', MENTION_SPECIAL: 'mention.special', - DEFAULT_NOTIFICATION_DURATION: 5000 + DEFAULT_NOTIFICATION_DURATION: 5000, + STATUS_INTERVAL: 60000, + AUTOCOMPLETE_TIMEOUT: 200 }; export default Constants; diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 10a59c25c..fcfec3592 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -10,7 +10,6 @@ import PreferenceStore from 'stores/preference_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import Constants from 'utils/constants.jsx'; var ActionTypes = Constants.ActionTypes; -import * as AsyncClient from './async_client.jsx'; import Client from 'client/web_client.jsx'; import * as UserAgent from 'utils/user_agent.jsx'; @@ -1074,66 +1073,6 @@ export function windowHeight() { return $(window).height(); } -export function openDirectChannelToUser(user, successCb, errorCb) { - AsyncClient.savePreference( - Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, - user.id, - 'true' - ); - - // if the user in another team and isn't already in the direct message - // list then we should add him so his name shows up correctly. - var profileUser = UserStore.getProfile(user.id); - if (!profileUser) { - UserStore.getDirectProfiles()[user.id] = user; - } - - const channelName = this.getDirectChannelName(UserStore.getCurrentId(), user.id); - let channel = ChannelStore.getByName(channelName); - - if (channel) { - if ($.isFunction(successCb)) { - successCb(channel, true); - } - } else { - channel = { - name: channelName, - last_post_at: 0, - total_msg_count: 0, - type: 'D', - display_name: user.username, - teammate_id: user.id, - status: UserStore.getStatus(user.id) - }; - - Client.createDirectChannel( - user.id, - (data) => { - Client.getChannel( - data.id, - (data2) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_CHANNEL, - channel: data2.channel, - member: data2.member - }); - - if ($.isFunction(successCb)) { - successCb(data2.channel, false); - } - } - ); - }, - () => { - browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/' + channelName); - if ($.isFunction(errorCb)) { - errorCb(); - } - } - ); - } -} - // Use when sorting multiple channels or teams by their `display_name` field export function sortByDisplayName(a, b) { let aDisplayName = ''; |