summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoram Wilander <jwawilander@gmail.com>2016-10-19 14:49:25 -0400
committerGitHub <noreply@github.com>2016-10-19 14:49:25 -0400
commit365b8b465e8a53ebb2da2bf3aef659ac81a2bc6a (patch)
tree643b2dd52b478c2c0b049ac28798d870b9dfd397
parent0512bd26ee85473aa47206d5f207a9a506019138 (diff)
downloadchat-365b8b465e8a53ebb2da2bf3aef659ac81a2bc6a.tar.gz
chat-365b8b465e8a53ebb2da2bf3aef659ac81a2bc6a.tar.bz2
chat-365b8b465e8a53ebb2da2bf3aef659ac81a2bc6a.zip
Merging performance branch into master (#4268)
* improve performance on sendNotifications * Fix SQL queries * Remove get direct profiles, not needed anymore * Add raw data to error details if AppError fails to decode * men * Fix decode (#4052) * Fixing json decode * Adding unit test * Initial work for client scaling (#4051) * Begin adding paging to profiles API * Added more paging functionality * Finish hooking up admin console user lists * Add API for searching users and add searching to all user lists * Add lazy loading of profiles * Revert config.json * Fix unit tests and some style issues * Add GetProfilesFromList to Go driver and fix web unit test * Update etag for GetProfiles * Updating ui for filters and pagination (#4044) * Updating UI for pagination * Adjusting margins for filter row * Adjusting margin for specific modals * Adding relative padding to system console * Adjusting responsive view * Update client user tests * Minor fixes for direct messages modal (#4056) * Remove some unneeded initial load calls (#4057) * UX updates to user lists, added smart counts and bug fixes (#4059) * Improved getExplicitMentions and unit tests (#4064) * Refactor getting posts to lazy load profiles correctly (#4062) * Comment out SetActiveChannel test (#4066) * Profiler cpu, block, and memory profiler. (#4081) * Fix TestSetActiveChannel unit test (#4071) * Fixing build failure caused by dependancies updating (#4076) * Adding profiler * Fix admin_team_member_dropdown eslint errors * Bumping session cache size (#4077) * Bumping session cache size * Bumping status cache * Refactor how the client handles channel members to be large team friendly (#4106) * Refactor how the client handles channel members to be large team friendly * Change Id to ChannelId in ChannelStats model * Updated getChannelMember and getProfilesByIds routes to match proposal * Performance improvements (#4100) * Performance improvements * Fixing re-connect issue * Fixing error message * Some other minor perf tweaks * Some other minor perf tweaks * Fixing config file * Fixing buffer size * Fixing web socket send message * adding some error logging * fix getMe to be user required * Fix websocket event for new user * Fixing shutting down * Reverting web socket changes * Fixing logging lvl * Adding caching to GetMember * Adding some logging * Fixing caching * Fixing caching invalidate * Fixing direct message caching * Fixing caching * Fixing caching * Remove GetDirectProfiles from initial load * Adding logging and fixing websocket client * Adding back caching from bad merge. * Explicitly close go driver requests (#4162) * Refactored how the client handles team members to be more large team friendly (#4159) * Refactor getProfilesForDirectMessageList API into getAllProfiles API * Refactored how the client handles team members to be more large team friendly * Fix js error when receiving a notification * Fix JS error caused by current user being overwritten with sanitized version (#4165) * Adding error message to status failure (#4167) * Fix a few bugs caused by client scaling refactoring (#4170) * When there is no read replica, don't open a second set of connections to the master database (#4173) * Adding connection tacking to stats (#4174) * Reduce DB writes for statuses and other status related changes (#4175) * Fix bug preventing opening of DM channels from more modal (#4181) * Fixing socket timing error (#4183) * Fixing ping/pong handler * Fixing socket timing error * Commenting out status broadcasting * Removing user status changes * Removing user status changes * Removing user status changes * Removing user status changes * Adding DoPreComputeJson() * Performance improvements (#4194) * * Fix System Console Analytics queries * Add db.SetConnMaxLifetime to 15 minutes * Add "net/http/pprof" for profiling * Add FreeOSMemory() to manually release memory on reload config * Add flag to enable http profiler * Fix memory leak (#4197) * Fix memory leak * removed unneeded nil assignment * Fixing go routine leak (#4208) * Merge fixes * Merge fix * Refactored statuses to be queried by the client rather than broadcast by the server (#4212) * Refactored server code to reduce status broadcasts and to allow getting statuses by IDs * Refactor client code to periodically fetch statuses * Add store unit test for getting statuses by ids * Fix status unit test * Add getStatusesByIds REST API and move the client over to use that instead of the WebSocket * Adding multiple threads to websocket hub (#4230) * Adding multiple threads to websocket hub * Fixing unit tests * Fixing so websocket connections from the same user end up in the sameā€¦ (#4240) * Fixing so websocket connections from the same user end up in the same list * Removing old comment * Refactor user autocomplete to query the server (#4239) * Add API for autocompleting users * Converted at mention autocomplete to query server * Converted user search autocomplete to query server * Switch autocomplete API naming to use term instead of username * Split autocomplete API into two, one for channels and for teams * Fix copy/paste error * Some final client scaling fixes (#4246) * Add lazy loading of profiles to integration pages * Add lazy loading of profiles to emoji page * Fix JS error when receiving post in select team menu and also clean up channel store
-rw-r--r--Makefile2
-rw-r--r--api/admin.go37
-rw-r--r--api/admin_test.go10
-rw-r--r--api/apitestlib.go4
-rw-r--r--api/authorization.go14
-rw-r--r--api/auto_users.go7
-rw-r--r--api/channel.go92
-rw-r--r--api/channel_test.go149
-rw-r--r--api/cli_test.go10
-rw-r--r--api/command_loadtest.go2
-rw-r--r--api/command_msg.go10
-rw-r--r--api/command_statuses_test.go2
-rw-r--r--api/context.go6
-rw-r--r--api/post.go145
-rw-r--r--api/post_test.go67
-rw-r--r--api/server.go22
-rw-r--r--api/status.go131
-rw-r--r--api/status_test.go106
-rw-r--r--api/team.go100
-rw-r--r--api/team_test.go109
-rw-r--r--api/user.go301
-rw-r--r--api/user_test.go438
-rw-r--r--api/web_conn.go149
-rw-r--r--api/web_hub.go152
-rw-r--r--api/websocket.go12
-rw-r--r--api/websocket_handler.go10
-rw-r--r--api/websocket_router.go1
-rw-r--r--api/websocket_test.go3
-rw-r--r--config/config.json4
-rw-r--r--i18n/en.json18
-rw-r--r--mattermost.go61
-rw-r--r--model/autocomplete.go58
-rw-r--r--model/channel.go4
-rw-r--r--model/channel_extra.go49
-rw-r--r--model/channel_stats.go34
-rw-r--r--model/client.go164
-rw-r--r--model/initial_load.go15
-rw-r--r--model/session.go2
-rw-r--r--model/status.go5
-rw-r--r--model/team_stats.go34
-rw-r--r--model/user.go20
-rw-r--r--model/utils.go30
-rw-r--r--model/utils_test.go7
-rw-r--r--model/websocket_client.go21
-rw-r--r--model/websocket_message.go44
-rw-r--r--store/sql_channel_store.go126
-rw-r--r--store/sql_channel_store_test.go65
-rw-r--r--store/sql_post_store.go70
-rw-r--r--store/sql_status_store.go41
-rw-r--r--store/sql_status_store_test.go9
-rw-r--r--store/sql_store.go26
-rw-r--r--store/sql_team_store.go69
-rw-r--r--store/sql_team_store_test.go79
-rw-r--r--store/sql_user_store.go359
-rw-r--r--store/sql_user_store_test.go428
-rw-r--r--store/store.go26
-rw-r--r--web/web_test.go10
-rw-r--r--webapp/actions/channel_actions.jsx137
-rw-r--r--webapp/actions/emoji_actions.jsx46
-rw-r--r--webapp/actions/global_actions.jsx23
-rw-r--r--webapp/actions/integration_actions.jsx114
-rw-r--r--webapp/actions/post_actions.jsx153
-rw-r--r--webapp/actions/status_actions.jsx133
-rw-r--r--webapp/actions/team_actions.jsx25
-rw-r--r--webapp/actions/user_actions.jsx244
-rw-r--r--webapp/actions/websocket_actions.jsx62
-rw-r--r--webapp/client/client.jsx130
-rw-r--r--webapp/client/websocket_client.jsx20
-rw-r--r--webapp/components/admin_console/admin_navbar_dropdown.jsx4
-rw-r--r--webapp/components/admin_console/admin_team_members_dropdown.jsx (renamed from webapp/components/admin_console/user_item.jsx)126
-rw-r--r--webapp/components/admin_console/team_users.jsx255
-rw-r--r--webapp/components/analytics/system_analytics.jsx37
-rw-r--r--webapp/components/channel_header.jsx33
-rw-r--r--webapp/components/channel_invite_button.jsx10
-rw-r--r--webapp/components/channel_invite_modal.jsx166
-rw-r--r--webapp/components/channel_members_modal.jsx144
-rw-r--r--webapp/components/channel_notifications_modal.jsx10
-rw-r--r--webapp/components/channel_switch_modal.jsx32
-rw-r--r--webapp/components/edit_post_modal.jsx25
-rw-r--r--webapp/components/emoji/components/emoji_list.jsx29
-rw-r--r--webapp/components/emoji/components/emoji_list_item.jsx11
-rw-r--r--webapp/components/integrations/components/installed_command.jsx8
-rw-r--r--webapp/components/integrations/components/installed_commands.jsx26
-rw-r--r--webapp/components/integrations/components/installed_incoming_webhook.jsx5
-rw-r--r--webapp/components/integrations/components/installed_incoming_webhooks.jsx28
-rw-r--r--webapp/components/integrations/components/installed_outgoing_webhook.jsx6
-rw-r--r--webapp/components/integrations/components/installed_outgoing_webhooks.jsx26
-rw-r--r--webapp/components/logged_in.jsx11
-rw-r--r--webapp/components/member_list_team.jsx118
-rw-r--r--webapp/components/more_direct_channels.jsx201
-rw-r--r--webapp/components/navbar.jsx8
-rw-r--r--webapp/components/needs_team.jsx4
-rw-r--r--webapp/components/notify_counts.jsx2
-rw-r--r--webapp/components/popover_list_members.jsx52
-rw-r--r--webapp/components/post_view/components/pending_post_options.jsx11
-rw-r--r--webapp/components/post_view/components/post_list.jsx10
-rw-r--r--webapp/components/post_view/post_focus_view_controller.jsx12
-rw-r--r--webapp/components/post_view/post_view_controller.jsx21
-rw-r--r--webapp/components/rhs_root_post.jsx6
-rw-r--r--webapp/components/rhs_thread.jsx8
-rw-r--r--webapp/components/searchable_user_list.jsx226
-rw-r--r--webapp/components/select_team/select_team.jsx2
-rw-r--r--webapp/components/sidebar.jsx11
-rw-r--r--webapp/components/sidebar_header_dropdown.jsx4
-rw-r--r--webapp/components/suggestion/at_mention_provider.jsx134
-rw-r--r--webapp/components/suggestion/search_user_provider.jsx76
-rw-r--r--webapp/components/suggestion/suggestion_list.jsx2
-rw-r--r--webapp/components/suggestion/switch_channel_provider.jsx8
-rw-r--r--webapp/components/team_members_dropdown.jsx56
-rw-r--r--webapp/components/textbox.jsx2
-rw-r--r--webapp/components/user_list.jsx24
-rw-r--r--webapp/components/user_list_row.jsx39
-rw-r--r--webapp/i18n/en.json18
-rw-r--r--webapp/routes/route_team.jsx42
-rw-r--r--webapp/sass/components/_modal.scss29
-rw-r--r--webapp/sass/responsive/_mobile.scss10
-rw-r--r--webapp/sass/routes/_admin-console.scss14
-rw-r--r--webapp/stores/channel_store.jsx127
-rw-r--r--webapp/stores/notification_store.jsx2
-rw-r--r--webapp/stores/post_store.jsx6
-rw-r--r--webapp/stores/suggestion_store.jsx8
-rw-r--r--webapp/stores/team_store.jsx123
-rw-r--r--webapp/stores/user_store.jsx430
-rw-r--r--webapp/tests/client_channel.test.jsx22
-rw-r--r--webapp/tests/client_team.test.jsx53
-rw-r--r--webapp/tests/client_user.test.jsx103
-rw-r--r--webapp/utils/async_client.jsx552
-rw-r--r--webapp/utils/constants.jsx24
-rw-r--r--webapp/utils/utils.jsx61
129 files changed, 6295 insertions, 2414 deletions
diff --git a/Makefile b/Makefile
index ab507dcee..bdf0e3a68 100644
--- a/Makefile
+++ b/Makefile
@@ -361,7 +361,7 @@ run-server: prepare-enterprise start-docker
run-cli: prepare-enterprise start-docker
@echo Running mattermost for development
- @echo Example should be like >'make ARGS="-version" run-cli'
+ @echo Example should be like 'make ARGS="-version" run-cli'
$(GO) run $(GOFLAGS) $(GO_LINKER_FLAGS) *.go ${ARGS}
diff --git a/api/admin.go b/api/admin.go
index 9ac071e6d..0edfb246b 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -20,6 +20,7 @@ import (
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
"github.com/mssola/user_agent"
+ "runtime/debug"
)
func InitAdmin() {
@@ -48,7 +49,7 @@ func InitAdmin() {
BaseRoutes.Admin.Handle("/remove_certificate", ApiAdminSystemRequired(removeCertificate)).Methods("POST")
BaseRoutes.Admin.Handle("/saml_cert_status", ApiAdminSystemRequired(samlCertificateStatus)).Methods("GET")
BaseRoutes.Admin.Handle("/cluster_status", ApiAdminSystemRequired(getClusterStatus)).Methods("GET")
- BaseRoutes.Admin.Handle("/recently_active_users/{team_id:[A-Za-z0-9]+}", ApiUserRequiredActivity(getRecentlyActiveUsers, false)).Methods("GET")
+ BaseRoutes.Admin.Handle("/recently_active_users/{team_id:[A-Za-z0-9]+}", ApiUserRequired(getRecentlyActiveUsers)).Methods("GET")
}
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -134,6 +135,7 @@ func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
}
func reloadConfig(c *Context, w http.ResponseWriter, r *http.Request) {
+ debug.FreeOSMemory()
utils.LoadConfig(utils.CfgFileName)
// start/restart email batching job if necessary
@@ -338,12 +340,15 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
name := params["name"]
if name == "standard" {
- var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 5)
+ var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 8)
rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
rows[2] = &model.AnalyticsRow{"post_count", 0}
rows[3] = &model.AnalyticsRow{"unique_user_count", 0}
rows[4] = &model.AnalyticsRow{"team_count", 0}
+ rows[5] = &model.AnalyticsRow{"total_websocket_connections", 0}
+ rows[6] = &model.AnalyticsRow{"total_master_db_connections", 0}
+ rows[7] = &model.AnalyticsRow{"total_read_db_connections", 0}
openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
@@ -386,6 +391,10 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
rows[4].Value = float64(r.Data.(int64))
}
+ rows[5].Value = float64(TotalWebsocketConnections())
+ rows[6].Value = float64(Srv.Store.TotalMasterDbConnections())
+ rows[7].Value = float64(Srv.Store.TotalReadDbConnections())
+
w.Write([]byte(rows.ToJson()))
} else if name == "post_counts_day" {
if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil {
@@ -706,32 +715,14 @@ func samlCertificateStatus(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getRecentlyActiveUsers(c *Context, w http.ResponseWriter, r *http.Request) {
- statusMap := map[string]interface{}{}
-
- if result := <-Srv.Store.Status().GetAllFromTeam(c.TeamId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- statuses := result.Data.([]*model.Status)
- for _, s := range statuses {
- statusMap[s.UserId] = s.LastActivityAt
- }
- }
-
- if result := <-Srv.Store.User().GetProfiles(c.TeamId); result.Err != nil {
+ if result := <-Srv.Store.User().GetRecentlyActiveUsersForTeam(c.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
profiles := result.Data.(map[string]*model.User)
- for k, p := range profiles {
- p = sanitizeProfile(c, p)
-
- if lastActivityAt, ok := statusMap[p.Id].(int64); ok {
- p.LastActivityAt = lastActivityAt
- }
-
- profiles[k] = p
+ for _, p := range profiles {
+ sanitizeProfile(c, p)
}
w.Write([]byte(model.UserMapToJson(profiles)))
diff --git a/api/admin_test.go b/api/admin_test.go
index 445d2de38..e1520877c 100644
--- a/api/admin_test.go
+++ b/api/admin_test.go
@@ -527,17 +527,13 @@ func TestAdminLdapSyncNow(t *testing.T) {
}
}
+// Needs more work
func TestGetRecentlyActiveUsers(t *testing.T) {
th := Setup().InitBasic()
- user1Id := th.BasicUser.Id
- user2Id := th.BasicUser2.Id
-
if userMap, err := th.BasicClient.GetRecentlyActiveUsers(th.BasicTeam.Id); err != nil {
t.Fatal(err)
- } else if len(userMap.Data.(map[string]*model.User)) != 2 {
- t.Fatal("should have been 2")
- } else if userMap.Data.(map[string]*model.User)[user1Id].Id != user1Id || userMap.Data.(map[string]*model.User)[user2Id].Id != user2Id {
- t.Fatal("should have been valid")
+ } else if len(userMap.Data.(map[string]*model.User)) >= 2 {
+ t.Fatal("should have been at least 2")
}
}
diff --git a/api/apitestlib.go b/api/apitestlib.go
index 9345d3fc4..37367b71d 100644
--- a/api/apitestlib.go
+++ b/api/apitestlib.go
@@ -36,7 +36,7 @@ func SetupEnterprise() *TestHelper {
*utils.Cfg.RateLimitSettings.Enable = false
utils.DisableDebugLogForTest()
utils.License.Features.SetDefaults()
- NewServer()
+ NewServer(false)
StartServer()
utils.InitHTML()
InitApi()
@@ -57,7 +57,7 @@ func Setup() *TestHelper {
utils.Cfg.TeamSettings.MaxUsersPerTeam = 50
*utils.Cfg.RateLimitSettings.Enable = false
utils.DisableDebugLogForTest()
- NewServer()
+ NewServer(false)
StartServer()
InitApi()
utils.EnableDebugLogForTest()
diff --git a/api/authorization.go b/api/authorization.go
index 5badf244b..8b3140b0f 100644
--- a/api/authorization.go
+++ b/api/authorization.go
@@ -5,6 +5,7 @@ package api
import (
"net/http"
+ "strings"
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
@@ -65,15 +66,16 @@ func HasPermissionToTeam(user *model.User, teamMember *model.TeamMember, permiss
}
func HasPermissionToChannelContext(c *Context, channelId string, permission *model.Permission) bool {
- cmc := Srv.Store.Channel().GetMember(channelId, c.Session.UserId)
+ cmc := Srv.Store.Channel().GetAllChannelMembersForUser(c.Session.UserId, true)
var channelRoles []string
if cmcresult := <-cmc; cmcresult.Err == nil {
- channelMember := cmcresult.Data.(model.ChannelMember)
- channelRoles = channelMember.GetRoles()
-
- if CheckIfRolesGrantPermission(channelRoles, permission.Id) {
- return true
+ ids := cmcresult.Data.(map[string]string)
+ if roles, ok := ids[channelId]; ok {
+ channelRoles = strings.Fields(roles)
+ if CheckIfRolesGrantPermission(channelRoles, permission.Id) {
+ return true
+ }
}
}
diff --git a/api/auto_users.go b/api/auto_users.go
index a23b76246..7439de96e 100644
--- a/api/auto_users.go
+++ b/api/auto_users.go
@@ -80,6 +80,13 @@ func (cfg *AutoUserCreator) createRandomUser() (*model.User, bool) {
ruser := result.Data.(*model.User)
+ status := &model.Status{ruser.Id, model.STATUS_ONLINE, false, model.GetMillis(), ""}
+ if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
+ result.Err.Translate(utils.T)
+ l4g.Error(result.Err.Error())
+ return nil, false
+ }
+
// We need to cheat to verify the user's email
store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
diff --git a/api/channel.go b/api/channel.go
index f40c979ca..9cc8976c2 100644
--- a/api/channel.go
+++ b/api/channel.go
@@ -6,7 +6,6 @@ package api
import (
"fmt"
"net/http"
- "strconv"
"strings"
l4g "github.com/alecthomas/log4go"
@@ -16,16 +15,12 @@ import (
"github.com/mattermost/platform/utils"
)
-const (
- defaultExtraMemberLimit = 100
-)
-
func InitChannel() {
l4g.Debug(utils.T("api.channel.init.debug"))
- BaseRoutes.Channels.Handle("/", ApiUserRequiredActivity(getChannels, false)).Methods("GET")
+ BaseRoutes.Channels.Handle("/", ApiUserRequired(getChannels)).Methods("GET")
BaseRoutes.Channels.Handle("/more", ApiUserRequired(getMoreChannels)).Methods("GET")
- BaseRoutes.Channels.Handle("/counts", ApiUserRequiredActivity(getChannelCounts, false)).Methods("GET")
+ BaseRoutes.Channels.Handle("/counts", ApiUserRequired(getChannelCounts)).Methods("GET")
BaseRoutes.Channels.Handle("/create", ApiUserRequired(createChannel)).Methods("POST")
BaseRoutes.Channels.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST")
BaseRoutes.Channels.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST")
@@ -35,9 +30,9 @@ func InitChannel() {
BaseRoutes.NeedChannelName.Handle("/join", ApiUserRequired(join)).Methods("POST")
- BaseRoutes.NeedChannel.Handle("/", ApiUserRequiredActivity(getChannel, false)).Methods("GET")
- BaseRoutes.NeedChannel.Handle("/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET")
- BaseRoutes.NeedChannel.Handle("/extra_info/{member_limit:-?[0-9]+}", ApiUserRequired(getChannelExtraInfo)).Methods("GET")
+ BaseRoutes.NeedChannel.Handle("/", ApiUserRequired(getChannel)).Methods("GET")
+ BaseRoutes.NeedChannel.Handle("/stats", ApiUserRequired(getChannelStats)).Methods("GET")
+ BaseRoutes.NeedChannel.Handle("/members/{user_id:[A-Za-z0-9]+}", ApiUserRequired(getChannelMember)).Methods("GET")
BaseRoutes.NeedChannel.Handle("/join", ApiUserRequired(join)).Methods("POST")
BaseRoutes.NeedChannel.Handle("/leave", ApiUserRequired(leave)).Methods("POST")
BaseRoutes.NeedChannel.Handle("/delete", ApiUserRequired(deleteChannel)).Methods("POST")
@@ -150,11 +145,14 @@ func CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *mo
} else {
channel := result.Data.(*model.Channel)
+ InvalidateCacheForUser(userId)
+ InvalidateCacheForUser(otherUserId)
+
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil)
message.Add("teammate_id", otherUserId)
go Publish(message)
- return result.Data.(*model.Channel), nil
+ return channel, nil
}
}
@@ -566,6 +564,7 @@ func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelM
go func() {
InvalidateCacheForUser(user.Id)
+ Srv.Store.User().InvalidateProfilesInChannelCache(channel.Id)
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_ADDED, "", channel.Id, "", nil)
message.Add("user_id", user.Id)
@@ -609,6 +608,8 @@ func JoinDefaultChannels(teamId string, user *model.User, channelRole string) *m
if _, err := CreatePost(fakeContext, post, false); err != nil {
l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err)
}
+
+ Srv.Store.User().InvalidateProfilesInChannelCache(result.Data.(*model.Channel).Id)
}
if result := <-Srv.Store.Channel().GetByName(teamId, "off-topic"); result.Err != nil {
@@ -631,6 +632,8 @@ func JoinDefaultChannels(teamId string, user *model.User, channelRole string) *m
if _, err := CreatePost(fakeContext, post, false); err != nil {
l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err)
}
+
+ Srv.Store.User().InvalidateProfilesInChannelCache(result.Data.(*model.Channel).Id)
}
return err
@@ -778,9 +781,9 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("name=" + channel.Name)
go func() {
- InvalidateCacheForChannel(channel.Id)
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_DELETED, c.TeamId, "", "", nil)
message.Add("channel_id", channel.Id)
+
go Publish(message)
post := &model.Post{
@@ -917,54 +920,27 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) {
}
-func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) {
+func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["channel_id"]
- var memberLimit int
- if memberLimitString, ok := params["member_limit"]; !ok {
- memberLimit = defaultExtraMemberLimit
- } else if memberLimitInt64, err := strconv.ParseInt(memberLimitString, 10, 0); err != nil {
- c.Err = model.NewLocAppError("getChannelExtraInfo", "api.channel.get_channel_extra_info.member_limit.app_error", nil, err.Error())
- return
- } else {
- memberLimit = int(memberLimitInt64)
- }
-
sc := Srv.Store.Channel().Get(id)
var channel *model.Channel
- if cresult := <-sc; cresult.Err != nil {
- c.Err = cresult.Err
+ if result := <-sc; result.Err != nil {
+ c.Err = result.Err
return
} else {
- channel = cresult.Data.(*model.Channel)
+ channel = result.Data.(*model.Channel)
}
- extraEtag := channel.ExtraEtag(memberLimit)
- if HandleEtag(extraEtag, w, r) {
- return
- }
-
- scm := Srv.Store.Channel().GetMember(id, c.Session.UserId)
- ecm := Srv.Store.Channel().GetExtraMembers(id, memberLimit)
- ccm := Srv.Store.Channel().GetMemberCount(id)
-
- if cmresult := <-scm; cmresult.Err != nil {
- c.Err = cmresult.Err
- return
- } else if ecmresult := <-ecm; ecmresult.Err != nil {
- c.Err = ecmresult.Err
- return
- } else if ccmresult := <-ccm; ccmresult.Err != nil {
- c.Err = ccmresult.Err
+ if result := <-Srv.Store.Channel().GetMemberCount(id); result.Err != nil {
+ c.Err = result.Err
return
} else {
- //member := cmresult.Data.(model.ChannelMember)
- extraMembers := ecmresult.Data.([]model.ExtraMember)
- memberCount := ccmresult.Data.(int64)
+ memberCount := result.Data.(int64)
if channel.DeleteAt > 0 {
- c.Err = model.NewLocAppError("getChannelExtraInfo", "api.channel.get_channel_extra_info.deleted.app_error", nil, "")
+ c.Err = model.NewLocAppError("getChannelStats", "api.channel.get_channel_extra_info.deleted.app_error", nil, "")
c.Err.StatusCode = http.StatusBadRequest
return
}
@@ -973,12 +949,29 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- data := model.ChannelExtra{Id: channel.Id, Members: extraMembers, MemberCount: memberCount}
- w.Header().Set(model.HEADER_ETAG_SERVER, extraEtag)
+ data := model.ChannelStats{ChannelId: channel.Id, MemberCount: memberCount}
w.Write([]byte(data.ToJson()))
}
}
+func getChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ channelId := params["channel_id"]
+ userId := params["user_id"]
+
+ if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ return
+ }
+
+ if result := <-Srv.Store.Channel().GetMember(channelId, userId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ member := result.Data.(model.ChannelMember)
+ w.Write([]byte(member.ToJson()))
+ }
+}
+
func addMember(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["channel_id"]
@@ -1101,6 +1094,7 @@ func RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel
}
InvalidateCacheForUser(userIdToRemove)
+ Srv.Store.User().InvalidateProfilesInChannelCache(channel.Id)
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", channel.Id, "", nil)
message.Add("user_id", userIdToRemove)
diff --git a/api/channel_test.go b/api/channel_test.go
index 1d0f0270d..4835ee9b7 100644
--- a/api/channel_test.go
+++ b/api/channel_test.go
@@ -10,7 +10,6 @@ import (
"time"
"github.com/mattermost/platform/model"
- "github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
@@ -1106,7 +1105,7 @@ func TestDeleteChannel(t *testing.T) {
}
}
-func TestGetChannelExtraInfo(t *testing.T) {
+func TestGetChannelStats(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
team := th.BasicTeam
@@ -1114,115 +1113,13 @@ func TestGetChannelExtraInfo(t *testing.T) {
channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
- rget := Client.Must(Client.GetChannelExtraInfo(channel1.Id, -1, ""))
- data := rget.Data.(*model.ChannelExtra)
- if data.Id != channel1.Id {
+ rget := Client.Must(Client.GetChannelStats(channel1.Id, ""))
+ data := rget.Data.(*model.ChannelStats)
+ if data.ChannelId != channel1.Id {
t.Fatal("couldnt't get extra info")
- } else if len(data.Members) != 1 {
- t.Fatal("got incorrect members")
} else if data.MemberCount != 1 {
t.Fatal("got incorrect member count")
}
-
- //
- // Testing etag caching
- //
-
- currentEtag := rget.Etag
-
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
- t.Fatal(err)
- } else if cache_result.Data.(*model.ChannelExtra) != nil {
- t.Log(cache_result.Data)
- t.Fatal("response should be empty")
- } else {
- currentEtag = cache_result.Etag
- }
-
- Client2 := model.NewClient("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress)
-
- user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Tester 2", Password: "passwd1"}
- user2 = Client2.Must(Client2.CreateUser(user2, "")).Data.(*model.User)
- LinkUserToTeam(user2, team)
- Client2.SetTeamId(team.Id)
- store.Must(Srv.Store.User().VerifyEmail(user2.Id))
-
- Client2.Login(user2.Email, "passwd1")
- Client2.Must(Client2.JoinChannel(channel1.Id))
-
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
- t.Fatal(err)
- } else if cache_result.Data.(*model.ChannelExtra) == nil {
- t.Log(cache_result.Data)
- t.Fatal("response should not be empty")
- } else {
- currentEtag = cache_result.Etag
- }
-
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
- t.Fatal(err)
- } else if cache_result.Data.(*model.ChannelExtra) != nil {
- t.Log(cache_result.Data)
- t.Fatal("response should be empty")
- } else {
- currentEtag = cache_result.Etag
- }
-
- Client2.Must(Client2.LeaveChannel(channel1.Id))
-
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
- t.Fatal(err)
- } else if cache_result.Data.(*model.ChannelExtra) == nil {
- t.Log(cache_result.Data)
- t.Fatal("response should not be empty")
- } else {
- currentEtag = cache_result.Etag
- }
-
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
- t.Fatal(err)
- } else if cache_result.Data.(*model.ChannelExtra) != nil {
- t.Log(cache_result.Data)
- t.Fatal("response should be empty")
- } else {
- currentEtag = cache_result.Etag
- }
-
- Client2.Must(Client2.JoinChannel(channel1.Id))
-
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, 2, currentEtag); err != nil {
- t.Fatal(err)
- } else if extra := cache_result.Data.(*model.ChannelExtra); extra == nil {
- t.Fatal("response should not be empty")
- } else if len(extra.Members) != 2 {
- t.Fatal("should've returned 2 members")
- } else if extra.MemberCount != 2 {
- t.Fatal("should've returned member count of 2")
- } else {
- currentEtag = cache_result.Etag
- }
-
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, 1, currentEtag); err != nil {
- t.Fatal(err)
- } else if extra := cache_result.Data.(*model.ChannelExtra); extra == nil {
- t.Fatal("response should not be empty")
- } else if len(extra.Members) != 1 {
- t.Fatal("should've returned only 1 member")
- } else if extra.MemberCount != 2 {
- t.Fatal("should've returned member count of 2")
- } else {
- currentEtag = cache_result.Etag
- }
-
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, 1, currentEtag); err != nil {
- t.Fatal(err)
- } else if cache_result.Data.(*model.ChannelExtra) != nil {
- t.Log(cache_result.Data)
- t.Fatal("response should be empty")
- } else {
- currentEtag = cache_result.Etag
- }
-
}
func TestAddChannelMember(t *testing.T) {
@@ -1495,3 +1392,41 @@ func TestFuzzyChannel(t *testing.T) {
}
}
}
+
+func TestGetChannelMember(t *testing.T) {
+ th := Setup().InitBasic()
+ Client := th.BasicClient
+ team := th.BasicTeam
+
+ channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ if result, err := Client.GetChannelMember(channel1.Id, th.BasicUser.Id); err != nil {
+ t.Fatal(err)
+ } else {
+ cm := result.Data.(*model.ChannelMember)
+
+ if cm.UserId != th.BasicUser.Id {
+ t.Fatal("user ids didn't match")
+ }
+ if cm.ChannelId != channel1.Id {
+ t.Fatal("channel ids didn't match")
+ }
+ }
+
+ if _, err := Client.GetChannelMember(channel1.Id, th.BasicUser2.Id); err == nil {
+ t.Fatal("should have failed - user not in channel")
+ }
+
+ if _, err := Client.GetChannelMember("junk", th.BasicUser2.Id); err == nil {
+ t.Fatal("should have failed - bad channel id")
+ }
+
+ if _, err := Client.GetChannelMember(channel1.Id, "junk"); err == nil {
+ t.Fatal("should have failed - bad user id")
+ }
+
+ if _, err := Client.GetChannelMember("junk", "junk"); err == nil {
+ t.Fatal("should have failed - bad channel and user id")
+ }
+}
diff --git a/api/cli_test.go b/api/cli_test.go
index de2347058..4613988f3 100644
--- a/api/cli_test.go
+++ b/api/cli_test.go
@@ -68,7 +68,7 @@ func TestCliCreateUserWithTeam(t *testing.T) {
t.Fatal(err)
}
- profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfiles(th.SystemAdminTeam.Id, "")).Data.(map[string]*model.User)
+ profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfilesInTeam(th.SystemAdminTeam.Id, 0, 1000, "")).Data.(map[string]*model.User)
found := false
@@ -318,7 +318,7 @@ func TestCliJoinTeam(t *testing.T) {
t.Fatal(err)
}
- profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfiles(th.SystemAdminTeam.Id, "")).Data.(map[string]*model.User)
+ profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfilesInTeam(th.SystemAdminTeam.Id, 0, 1000, "")).Data.(map[string]*model.User)
found := false
@@ -348,7 +348,7 @@ func TestCliLeaveTeam(t *testing.T) {
t.Fatal(err)
}
- profiles := th.BasicClient.Must(th.BasicClient.GetProfiles(th.BasicTeam.Id, "")).Data.(map[string]*model.User)
+ profiles := th.BasicClient.Must(th.BasicClient.GetProfilesInTeam(th.BasicTeam.Id, 0, 1000, "")).Data.(map[string]*model.User)
found := false
@@ -359,8 +359,8 @@ func TestCliLeaveTeam(t *testing.T) {
}
- if !found {
- t.Fatal("profile still should be in team even if deleted")
+ if found {
+ t.Fatal("profile should not be on team")
}
if result := <-Srv.Store.Team().GetTeamsByUserId(th.BasicUser.Id); result.Err != nil {
diff --git a/api/command_loadtest.go b/api/command_loadtest.go
index beb831b22..8f5163a66 100644
--- a/api/command_loadtest.go
+++ b/api/command_loadtest.go
@@ -288,7 +288,7 @@ func (me *LoadTestProvider) PostsCommand(c *Context, channelId string, message s
}
var usernames []string
- if result := <-Srv.Store.User().GetProfiles(c.TeamId); result.Err == nil {
+ if result := <-Srv.Store.User().GetProfiles(c.TeamId, 0, 1000); result.Err == nil {
profileUsers := result.Data.(map[string]*model.User)
usernames = make([]string, len(profileUsers))
i := 0
diff --git a/api/command_msg.go b/api/command_msg.go
index aac657385..2e0e25397 100644
--- a/api/command_msg.go
+++ b/api/command_msg.go
@@ -47,20 +47,22 @@ func (me *msgProvider) DoCommand(c *Context, channelId string, message string) *
targetUser = strings.SplitN(message, " ", 2)[0]
targetUser = strings.TrimPrefix(targetUser, "@")
- if profileList := <-Srv.Store.User().GetAllProfiles(); profileList.Err != nil {
+ // FIX ME
+ // Why isn't this selecting by username since we have that?
+ if profileList := <-Srv.Store.User().GetAll(); profileList.Err != nil {
c.Err = profileList.Err
return &model.CommandResponse{Text: c.T("api.command_msg.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
} else {
- profileUsers := profileList.Data.(map[string]*model.User)
+ profileUsers := profileList.Data.([]*model.User)
for _, userProfile := range profileUsers {
- //Don't let users open DMs with themselves. It probably won't work out well.
+ // Don't let users open DMs with themselves. It probably won't work out well.
if userProfile.Id == c.Session.UserId {
continue
}
if userProfile.Username == targetUser {
targetChannelId := ""
- //Find the channel based on this user
+ // Find the channel based on this user
channelName := model.GetDMNameFromIds(c.Session.UserId, userProfile.Id)
if channel := <-Srv.Store.Channel().GetByName(c.TeamId, channelName); channel.Err != nil {
diff --git a/api/command_statuses_test.go b/api/command_statuses_test.go
index 1c8026a9f..73c6354f4 100644
--- a/api/command_statuses_test.go
+++ b/api/command_statuses_test.go
@@ -27,7 +27,7 @@ func commandAndTest(t *testing.T, th *TestHelper, status string) {
t.Fatal("Command failed to execute")
}
- time.Sleep(300 * time.Millisecond)
+ time.Sleep(500 * time.Millisecond)
statuses := Client.Must(Client.GetStatuses()).Data.(map[string]string)
diff --git a/api/context.go b/api/context.go
index 524ccf402..257f43174 100644
--- a/api/context.go
+++ b/api/context.go
@@ -57,7 +57,7 @@ func AppHandlerIndependent(h func(*Context, http.ResponseWriter, *http.Request))
}
func ApiUserRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
- return &handler{h, true, false, true, true, false, false}
+ return &handler{h, true, false, true, false, false, false}
}
func ApiUserRequiredActivity(h func(*Context, http.ResponseWriter, *http.Request), isUserActivity bool) http.Handler {
@@ -85,7 +85,7 @@ func ApiAppHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Req
}
func ApiUserRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
- return &handler{h, true, false, true, true, false, true}
+ return &handler{h, true, false, true, false, false, true}
}
func ApiAppHandlerTrustRequesterIndependent(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
@@ -220,7 +220,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.LogError(c.Err)
c.Err.Where = r.URL.Path
- // Block out detailed error whenn not in developer mode
+ // Block out detailed error when not in developer mode
if !*utils.Cfg.ServiceSettings.EnableDeveloper {
c.Err.DetailedError = ""
}
diff --git a/api/post.go b/api/post.go
index 498f5b363..5ae5f60db 100644
--- a/api/post.go
+++ b/api/post.go
@@ -35,18 +35,18 @@ const (
func InitPost() {
l4g.Debug(utils.T("api.post.init.debug"))
- BaseRoutes.NeedTeam.Handle("/posts/search", ApiUserRequired(searchPosts)).Methods("POST")
- BaseRoutes.NeedTeam.Handle("/posts/flagged/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getFlaggedPosts, false)).Methods("GET")
+ BaseRoutes.NeedTeam.Handle("/posts/search", ApiUserRequiredActivity(searchPosts, true)).Methods("POST")
+ BaseRoutes.NeedTeam.Handle("/posts/flagged/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getFlaggedPosts)).Methods("GET")
BaseRoutes.NeedTeam.Handle("/posts/{post_id}", ApiUserRequired(getPostById)).Methods("GET")
BaseRoutes.NeedTeam.Handle("/pltmp/{post_id}", ApiUserRequired(getPermalinkTmp)).Methods("GET")
- BaseRoutes.Posts.Handle("/create", ApiUserRequired(createPost)).Methods("POST")
- BaseRoutes.Posts.Handle("/update", ApiUserRequired(updatePost)).Methods("POST")
- BaseRoutes.Posts.Handle("/page/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getPosts, false)).Methods("GET")
- BaseRoutes.Posts.Handle("/since/{time:[0-9]+}", ApiUserRequiredActivity(getPostsSince, false)).Methods("GET")
+ BaseRoutes.Posts.Handle("/create", ApiUserRequiredActivity(createPost, true)).Methods("POST")
+ BaseRoutes.Posts.Handle("/update", ApiUserRequiredActivity(updatePost, true)).Methods("POST")
+ BaseRoutes.Posts.Handle("/page/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getPosts)).Methods("GET")
+ BaseRoutes.Posts.Handle("/since/{time:[0-9]+}", ApiUserRequired(getPostsSince)).Methods("GET")
BaseRoutes.NeedPost.Handle("/get", ApiUserRequired(getPost)).Methods("GET")
- BaseRoutes.NeedPost.Handle("/delete", ApiUserRequired(deletePost)).Methods("POST")
+ BaseRoutes.NeedPost.Handle("/delete", ApiUserRequiredActivity(deletePost, true)).Methods("POST")
BaseRoutes.NeedPost.Handle("/before/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsBefore)).Methods("GET")
BaseRoutes.NeedPost.Handle("/after/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsAfter)).Methods("GET")
BaseRoutes.NeedPost.Handle("/get_file_infos", ApiUserRequired(getFileInfosForPost)).Methods("GET")
@@ -154,7 +154,7 @@ func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post
}
}
- go handlePostEvents(c, rpost, triggerWebhooks)
+ handlePostEvents(c, rpost, triggerWebhooks)
return rpost, nil
}
@@ -250,7 +250,7 @@ func handlePostEvents(c *Context, post *model.Post, triggerWebhooks bool) {
channel = result.Data.(*model.Channel)
}
- go sendNotifications(c, post, team, channel)
+ sendNotifications(c, post, team, channel)
var user *model.User
if result := <-uchan; result.Err != nil {
@@ -441,40 +441,31 @@ func handleWebhookEvents(c *Context, post *model.Post, team *model.Team, channel
}
}
-// Given a map of user IDs to profiles and a map of user IDs of channel members, returns a list of mention
-// keywords for all users on the team. Users that are members of the channel will have all their mention
-// keywords returned while users that aren't in the channel will only have their @mentions returned.
-func getMentionKeywords(profiles map[string]*model.User, members map[string]string) map[string][]string {
+// Given a map of user IDs to profiles, returns a list of mention
+// keywords for all users in the channel.
+func getMentionKeywordsInChannel(profiles map[string]*model.User) map[string][]string {
keywords := make(map[string][]string)
for id, profile := range profiles {
- _, inChannel := members[id]
-
- if inChannel {
- if len(profile.NotifyProps["mention_keys"]) > 0 {
- // Add all the user's mention keys
- splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",")
- for _, k := range splitKeys {
- // note that these are made lower case so that we can do a case insensitive check for them
- key := strings.ToLower(k)
- keywords[key] = append(keywords[key], id)
- }
+ if len(profile.NotifyProps["mention_keys"]) > 0 {
+ // Add all the user's mention keys
+ splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",")
+ for _, k := range splitKeys {
+ // note that these are made lower case so that we can do a case insensitive check for them
+ key := strings.ToLower(k)
+ keywords[key] = append(keywords[key], id)
}
+ }
- // If turned on, add the user's case sensitive first name
- if profile.NotifyProps["first_name"] == "true" {
- keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
- }
+ // If turned on, add the user's case sensitive first name
+ if profile.NotifyProps["first_name"] == "true" {
+ keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
+ }
- // Add @channel and @all to keywords if user has them turned on
- if profile.NotifyProps["channel"] == "true" {
- keywords["@channel"] = append(keywords["@channel"], profile.Id)
- keywords["@all"] = append(keywords["@all"], profile.Id)
- }
- } else {
- // user isn't in channel, so just look for @mentions
- key := "@" + strings.ToLower(profile.Username)
- keywords[key] = append(keywords[key], id)
+ // Add @channel and @all to keywords if user has them turned on
+ if profile.NotifyProps["channel"] == "true" {
+ keywords["@channel"] = append(keywords["@channel"], profile.Id)
+ keywords["@all"] = append(keywords["@all"], profile.Id)
}
}
@@ -482,9 +473,11 @@ func getMentionKeywords(profiles map[string]*model.User, members map[string]stri
}
// Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
-// users and whether or not @here was mentioned.
-func getExplicitMentions(message string, keywords map[string][]string) (map[string]bool, bool) {
+// users and a slice of potencial mention users not in the channel and whether or not @here was mentioned.
+func getExplicitMentions(message string, keywords map[string][]string) (map[string]bool, []string, bool) {
mentioned := make(map[string]bool)
+ potentialOthersMentioned := make([]string, 0)
+ systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
hereMentioned := false
addMentionedUsers := func(ids []string) {
@@ -510,6 +503,9 @@ func getExplicitMentions(message string, keywords map[string][]string) (map[stri
if ids, match := keywords[word]; match {
addMentionedUsers(ids)
isMention = true
+ } else if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
+ potentialOthersMentioned = append(potentialOthersMentioned, word[1:])
+ continue
}
if !isMention {
@@ -532,19 +528,19 @@ func getExplicitMentions(message string, keywords map[string][]string) (map[stri
// Case-sensitive check for first name
if ids, match := keywords[splitWord]; match {
addMentionedUsers(ids)
+ } else if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
+ username := word[1:len(splitWord)]
+ potentialOthersMentioned = append(potentialOthersMentioned, username)
}
}
}
}
- return mentioned, hereMentioned
+ return mentioned, potentialOthersMentioned, hereMentioned
}
func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) {
- // get profiles for all users we could be mentioning
- pchan := Srv.Store.User().GetProfiles(c.TeamId)
- dpchan := Srv.Store.User().GetDirectProfiles(c.Session.UserId)
- mchan := Srv.Store.Channel().GetMembers(post.ChannelId)
+ pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true)
fchan := Srv.Store.FileInfo().GetForPost(post.Id)
var profileMap map[string]*model.User
@@ -555,30 +551,11 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
profileMap = result.Data.(map[string]*model.User)
}
- if result := <-dpchan; result.Err != nil {
- l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err)
- return
- } else {
- dps := result.Data.(map[string]*model.User)
- for k, v := range dps {
- profileMap[k] = v
- }
- }
-
+ // If the user who made the post is mention don't send a notification
if _, ok := profileMap[post.UserId]; !ok {
l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId)
return
}
- // using a map as a pseudo-set since we're checking for containment a lot
- members := make(map[string]string)
- if result := <-mchan; result.Err != nil {
- l4g.Error(utils.T("api.post.handle_post_events_and_forget.members.error"), post.ChannelId, result.Err)
- return
- } else {
- for _, member := range result.Data.([]model.ChannelMember) {
- members[member.UserId] = member.UserId
- }
- }
mentionedUserIds := make(map[string]bool)
allActivityPushUserIds := []string{}
@@ -595,11 +572,11 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
mentionedUserIds[otherUserId] = true
} else {
- keywords := getMentionKeywords(profileMap, members)
+ keywords := getMentionKeywordsInChannel(profileMap)
- // get users that are explicitly mentioned
var mentioned map[string]bool
- mentioned, hereNotification = getExplicitMentions(post.Message, keywords)
+ var potentialOtherMentions []string
+ mentioned, potentialOtherMentions, hereNotification = getExplicitMentions(post.Message, keywords)
// get users that have comment thread mentions enabled
if len(post.RootId) > 0 {
@@ -623,25 +600,15 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
delete(mentioned, post.UserId)
}
- outOfChannelMentions := make(map[string]bool)
- for id := range mentioned {
- if _, inChannel := members[id]; inChannel {
- mentionedUserIds[id] = true
- } else {
- outOfChannelMentions[id] = true
+ if len(potentialOtherMentions) > 0 {
+ if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil {
+ outOfChannelMentions := result.Data.(map[string]*model.User)
+ go sendOutOfChannelMentions(c, post, outOfChannelMentions)
}
}
- go sendOutOfChannelMentions(c, post, profileMap, outOfChannelMentions)
-
// find which users in the channel are set up to always receive mobile notifications
- for id := range members {
- profile := profileMap[id]
- if profile == nil {
- l4g.Warn(utils.T("api.post.notification.member_profile.warn"), id)
- continue
- }
-
+ for _, profile := range profileMap {
if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL &&
(post.UserId != profile.Id || post.Props["from_webhook"] == "true") &&
!post.IsSystemMessage() {
@@ -699,10 +666,9 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
}
_, profileFound := profileMap[status.UserId]
- _, isChannelMember := members[status.UserId]
_, alreadyMentioned := mentionedUserIds[status.UserId]
- if status.Status == model.STATUS_ONLINE && profileFound && isChannelMember && !alreadyMentioned {
+ if status.Status == model.STATUS_ONLINE && profileFound && !alreadyMentioned {
mentionedUsersList = append(mentionedUsersList, status.UserId)
updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId))
}
@@ -787,7 +753,8 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
message.Add("mentions", model.ArrayToJson(mentionedUsersList))
}
- go Publish(message)
+ Publish(message)
+ return
}
func sendNotificationEmail(c *Context, post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) {
@@ -1045,14 +1012,14 @@ func getMobileAppSession(userId string) *model.Session {
return nil
}
-func sendOutOfChannelMentions(c *Context, post *model.Post, profiles map[string]*model.User, outOfChannelMentions map[string]bool) {
- if len(outOfChannelMentions) == 0 {
+func sendOutOfChannelMentions(c *Context, post *model.Post, profiles map[string]*model.User) {
+ if len(profiles) == 0 {
return
}
var usernames []string
- for id := range outOfChannelMentions {
- usernames = append(usernames, profiles[id].Username)
+ for _, user := range profiles {
+ usernames = append(usernames, user.Username)
}
sort.Strings(usernames)
diff --git a/api/post_test.go b/api/post_test.go
index bdc5278e4..3c917aec3 100644
--- a/api/post_test.go
+++ b/api/post_test.go
@@ -883,10 +883,9 @@ func TestGetMentionKeywords(t *testing.T) {
}
profiles := map[string]*model.User{user1.Id: user1}
- members := map[string]string{user1.Id: user1.Id}
- mentions := getMentionKeywords(profiles, members)
+ mentions := getMentionKeywordsInChannel(profiles)
if len(mentions) != 3 {
- t.Fatal("should've returned two mention keywords")
+ t.Fatal("should've returned three mention keywords")
} else if ids, ok := mentions["user"]; !ok || ids[0] != user1.Id {
t.Fatal("should've returned mention key of user")
} else if ids, ok := mentions["@user"]; !ok || ids[0] != user1.Id {
@@ -906,8 +905,7 @@ func TestGetMentionKeywords(t *testing.T) {
}
profiles = map[string]*model.User{user2.Id: user2}
- members = map[string]string{user2.Id: user2.Id}
- mentions = getMentionKeywords(profiles, members)
+ mentions = getMentionKeywordsInChannel(profiles)
if len(mentions) != 1 {
t.Fatal("should've returned one mention keyword")
} else if ids, ok := mentions["First"]; !ok || ids[0] != user2.Id {
@@ -925,8 +923,7 @@ func TestGetMentionKeywords(t *testing.T) {
}
profiles = map[string]*model.User{user3.Id: user3}
- members = map[string]string{user3.Id: user3.Id}
- mentions = getMentionKeywords(profiles, members)
+ mentions = getMentionKeywordsInChannel(profiles)
if len(mentions) != 2 {
t.Fatal("should've returned two mention keywords")
} else if ids, ok := mentions["@channel"]; !ok || ids[0] != user3.Id {
@@ -948,8 +945,7 @@ func TestGetMentionKeywords(t *testing.T) {
}
profiles = map[string]*model.User{user4.Id: user4}
- members = map[string]string{user4.Id: user4.Id}
- mentions = getMentionKeywords(profiles, members)
+ mentions = getMentionKeywordsInChannel(profiles)
if len(mentions) != 6 {
t.Fatal("should've returned six mention keywords")
} else if ids, ok := mentions["user"]; !ok || ids[0] != user4.Id {
@@ -973,13 +969,7 @@ func TestGetMentionKeywords(t *testing.T) {
user3.Id: user3,
user4.Id: user4,
}
- members = map[string]string{
- user1.Id: user1.Id,
- user2.Id: user2.Id,
- user3.Id: user3.Id,
- user4.Id: user4.Id,
- }
- mentions = getMentionKeywords(profiles, members)
+ mentions = getMentionKeywordsInChannel(profiles)
if len(mentions) != 6 {
t.Fatal("should've returned six mention keywords")
} else if ids, ok := mentions["user"]; !ok || len(ids) != 2 || (ids[0] != user1.Id && ids[1] != user1.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) {
@@ -995,16 +985,6 @@ func TestGetMentionKeywords(t *testing.T) {
} else if ids, ok := mentions["@all"]; !ok || len(ids) != 2 || (ids[0] != user3.Id && ids[1] != user3.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) {
t.Fatal("should've mentioned user3 and user4 with @all")
}
-
- // a user that's not in the channel
- profiles = map[string]*model.User{user4.Id: user4}
- members = map[string]string{}
- mentions = getMentionKeywords(profiles, members)
- if len(mentions) != 1 {
- t.Fatal("should've returned one mention keyword")
- } else if ids, ok := mentions["@user"]; !ok || len(ids) != 1 || ids[0] != user4.Id {
- t.Fatal("should've returned mention key of @user")
- }
}
func TestGetExplicitMentionsAtHere(t *testing.T) {
@@ -1051,7 +1031,7 @@ func TestGetExplicitMentionsAtHere(t *testing.T) {
}
for message, shouldMention := range cases {
- if _, hereMentioned := getExplicitMentions(message, nil); hereMentioned && !shouldMention {
+ if _, _, hereMentioned := getExplicitMentions(message, nil); hereMentioned && !shouldMention {
t.Fatalf("shouldn't have mentioned @here with \"%v\"", message)
} else if !hereMentioned && shouldMention {
t.Fatalf("should've have mentioned @here with \"%v\"", message)
@@ -1060,10 +1040,12 @@ func TestGetExplicitMentionsAtHere(t *testing.T) {
// mentioning @here and someone
id := model.NewId()
- if mentions, hereMentioned := getExplicitMentions("@here @user", map[string][]string{"@user": {id}}); !hereMentioned {
+ if mentions, potential, hereMentioned := getExplicitMentions("@here @user @potential", map[string][]string{"@user": {id}}); !hereMentioned {
t.Fatal("should've mentioned @here with \"@here @user\"")
} else if len(mentions) != 1 || !mentions[id] {
t.Fatal("should've mentioned @user with \"@here @user\"")
+ } else if len(potential) > 1 {
+ t.Fatal("should've potential mentions for @potential")
}
}
@@ -1074,69 +1056,76 @@ func TestGetExplicitMentions(t *testing.T) {
// not mentioning anybody
message := "this is a message"
keywords := map[string][]string{}
- if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 0 {
- t.Fatal("shouldn't have mentioned anybody")
+ if mentions, potential, _ := getExplicitMentions(message, keywords); len(mentions) != 0 || len(potential) != 0 {
+ t.Fatal("shouldn't have mentioned anybody or have any potencial mentions")
}
// mentioning a user that doesn't exist
message = "this is a message for @user"
- if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 0 {
+ if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 0 {
t.Fatal("shouldn't have mentioned user that doesn't exist")
}
// mentioning one person
keywords = map[string][]string{"@user": {id1}}
- if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] {
+ if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] {
t.Fatal("should've mentioned @user")
}
// mentioning one person without an @mention
message = "this is a message for @user"
keywords = map[string][]string{"this": {id1}}
- if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] {
+ if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] {
t.Fatal("should've mentioned this")
}
// mentioning multiple people with one word
message = "this is a message for @user"
keywords = map[string][]string{"@user": {id1, id2}}
- if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
+ if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
t.Fatal("should've mentioned two users with @user")
}
// mentioning only one of multiple people
keywords = map[string][]string{"@user": {id1}, "@mention": {id2}}
- if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] {
+ if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] {
t.Fatal("should've mentioned @user and not @mention")
}
// mentioning multiple people with multiple words
message = "this is an @mention for @user"
keywords = map[string][]string{"@user": {id1}, "@mention": {id2}}
- if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
+ if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
t.Fatal("should've mentioned two users with @user and @mention")
}
// mentioning @channel (not a special case, but it's good to double check)
message = "this is an message for @channel"
keywords = map[string][]string{"@channel": {id1, id2}}
- if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
+ if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
t.Fatal("should've mentioned two users with @channel")
}
// mentioning @all (not a special case, but it's good to double check)
message = "this is an message for @all"
keywords = map[string][]string{"@all": {id1, id2}}
- if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
+ if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
t.Fatal("should've mentioned two users with @all")
}
// mentioning user.period without mentioning user (PLT-3222)
message = "user.period doesn't complicate things at all by including periods in their username"
keywords = map[string][]string{"user.period": {id1}, "user": {id2}}
- if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] {
+ if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] {
t.Fatal("should've mentioned user.period and not user")
}
+
+ // mentioning a potential out of channel user
+ message = "this is an message for @potential and @user"
+ keywords = map[string][]string{"@user": {id1}}
+ if mentions, potential, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || len(potential) != 1 {
+ t.Fatal("should've mentioned user and have a potential not in channel")
+ }
}
func TestGetFlaggedPosts(t *testing.T) {
diff --git a/api/server.go b/api/server.go
index a7b9716c4..fee74e373 100644
--- a/api/server.go
+++ b/api/server.go
@@ -7,6 +7,7 @@ import (
"crypto/tls"
"net"
"net/http"
+ "net/http/pprof"
"strings"
"time"
@@ -36,7 +37,20 @@ const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second
var Srv *Server
-func NewServer() {
+func AttachProfiler(router *mux.Router) {
+ router.HandleFunc("/debug/pprof/", pprof.Index)
+ router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
+ router.HandleFunc("/debug/pprof/profile", pprof.Profile)
+ router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
+
+ // Manually add support for paths linked to by index page at /debug/pprof/
+ router.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
+ router.Handle("/debug/pprof/heap", pprof.Handler("heap"))
+ router.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
+ router.Handle("/debug/pprof/block", pprof.Handler("block"))
+}
+
+func NewServer(enableProfiler bool) {
l4g.Info(utils.T("api.server.new_server.init.info"))
@@ -44,6 +58,10 @@ func NewServer() {
Srv.Store = store.NewSqlStore()
Srv.Router = mux.NewRouter()
+ if enableProfiler {
+ AttachProfiler(Srv.Router)
+ l4g.Info("Enabled HTTP Profiler")
+ }
Srv.Router.NotFoundHandler = http.HandlerFunc(Handle404)
}
@@ -177,7 +195,7 @@ func StopServer() {
Srv.GracefulServer.Stop(TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN)
Srv.Store.Close()
- hub.Stop()
+ HubStop()
l4g.Info(utils.T("api.server.stop_server.stopped.info"))
}
diff --git a/api/status.go b/api/status.go
index a1a5ac496..897102a4b 100644
--- a/api/status.go
+++ b/api/status.go
@@ -31,9 +31,11 @@ func AddStatusCache(status *model.Status) {
func InitStatus() {
l4g.Debug(utils.T("api.status.init.debug"))
- BaseRoutes.Users.Handle("/status", ApiUserRequiredActivity(getStatusesHttp, false)).Methods("GET")
- BaseRoutes.Users.Handle("/status/set_active_channel", ApiUserRequiredActivity(setActiveChannel, false)).Methods("POST")
+ BaseRoutes.Users.Handle("/status", ApiUserRequired(getStatusesHttp)).Methods("GET")
+ BaseRoutes.Users.Handle("/status/ids", ApiUserRequired(getStatusesByIdsHttp)).Methods("POST")
+ BaseRoutes.Users.Handle("/status/set_active_channel", ApiUserRequired(setActiveChannel)).Methods("POST")
BaseRoutes.WebSocket.Handle("get_statuses", ApiWebSocketHandler(getStatusesWebSocket))
+ BaseRoutes.WebSocket.Handle("get_statuses_by_ids", ApiWebSocketHandler(getStatusesByIdsWebSocket))
}
func getStatusesHttp(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -55,6 +57,7 @@ func getStatusesWebSocket(req *model.WebSocketRequest) (map[string]interface{},
return statusMap, nil
}
+// Only returns 300 statuses max
func GetAllStatuses() (map[string]interface{}, *model.AppError) {
if result := <-Srv.Store.Status().GetOnlineAway(); result.Err != nil {
return nil, result.Err
@@ -70,11 +73,82 @@ func GetAllStatuses() (map[string]interface{}, *model.AppError) {
}
}
+func getStatusesByIdsHttp(c *Context, w http.ResponseWriter, r *http.Request) {
+ userIds := model.ArrayFromJson(r.Body)
+
+ if len(userIds) == 0 {
+ c.SetInvalidParam("getStatusesByIdsHttp", "user_ids")
+ return
+ }
+
+ statusMap, err := GetStatusesByIds(userIds)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ w.Write([]byte(model.StringInterfaceToJson(statusMap)))
+}
+
+func getStatusesByIdsWebSocket(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) {
+ var userIds []string
+ if userIds = model.ArrayFromInterface(req.Data["user_ids"]); len(userIds) == 0 {
+ l4g.Error(model.StringInterfaceToJson(req.Data))
+ return nil, NewInvalidWebSocketParamError(req.Action, "user_ids")
+ }
+
+ statusMap, err := GetStatusesByIds(userIds)
+ if err != nil {
+ return nil, err
+ }
+
+ return statusMap, nil
+}
+
+func GetStatusesByIds(userIds []string) (map[string]interface{}, *model.AppError) {
+ statusMap := map[string]interface{}{}
+
+ missingUserIds := []string{}
+ for _, userId := range userIds {
+ if result, ok := statusCache.Get(userId); ok {
+ statusMap[userId] = result.(*model.Status).Status
+ } else {
+ missingUserIds = append(missingUserIds, userId)
+ }
+ }
+
+ if len(missingUserIds) > 0 {
+ if result := <-Srv.Store.Status().GetByIds(missingUserIds); result.Err != nil {
+ return nil, result.Err
+ } else {
+ statuses := result.Data.([]*model.Status)
+
+ for _, s := range statuses {
+ AddStatusCache(s)
+ statusMap[s.UserId] = s.Status
+ }
+ }
+ }
+
+ // For the case where the user does not have a row in the Status table and cache
+ for _, userId := range missingUserIds {
+ if _, ok := statusMap[userId]; !ok {
+ statusMap[userId] = model.STATUS_OFFLINE
+ }
+ }
+
+ return statusMap, nil
+}
+
func SetStatusOnline(userId string, sessionId string, manual bool) {
broadcast := false
+ var oldStatus string = model.STATUS_OFFLINE
+ var oldTime int64 = 0
+ var oldManual bool = false
var status *model.Status
var err *model.AppError
+
if status, err = GetStatus(userId); err != nil {
status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis(), ""}
broadcast = true
@@ -82,35 +156,45 @@ func SetStatusOnline(userId string, sessionId string, manual bool) {
if status.Manual && !manual {
return // manually set status always overrides non-manual one
}
+
if status.Status != model.STATUS_ONLINE {
broadcast = true
}
+
+ oldStatus = status.Status
+ oldTime = status.LastActivityAt
+ oldManual = status.Manual
+
status.Status = model.STATUS_ONLINE
- status.Manual = false // for "online" there's no manually or auto set
+ status.Manual = false // for "online" there's no manual setting
status.LastActivityAt = model.GetMillis()
}
AddStatusCache(status)
- achan := Srv.Store.Session().UpdateLastActivityAt(sessionId, model.GetMillis())
+ // Only update the database if the status has changed, the status has been manually set,
+ // or enough time has passed since the previous action
+ if status.Status != oldStatus || status.Manual != oldManual || status.LastActivityAt-oldTime > model.STATUS_MIN_UPDATE_TIME {
+ achan := Srv.Store.Session().UpdateLastActivityAt(sessionId, status.LastActivityAt)
- var schan store.StoreChannel
- if broadcast {
- schan = Srv.Store.Status().SaveOrUpdate(status)
- } else {
- schan = Srv.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt)
- }
+ var schan store.StoreChannel
+ if broadcast {
+ schan = Srv.Store.Status().SaveOrUpdate(status)
+ } else {
+ schan = Srv.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt)
+ }
- if result := <-achan; result.Err != nil {
- l4g.Error(utils.T("api.status.last_activity.error"), userId, sessionId, result.Err)
- }
+ if result := <-achan; result.Err != nil {
+ l4g.Error(utils.T("api.status.last_activity.error"), userId, sessionId, result.Err)
+ }
- if result := <-schan; result.Err != nil {
- l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
+ if result := <-schan; result.Err != nil {
+ l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
+ }
}
if broadcast {
- event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", "", nil)
+ event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
event.Add("status", model.STATUS_ONLINE)
event.Add("user_id", status.UserId)
go Publish(event)
@@ -131,7 +215,7 @@ func SetStatusOffline(userId string, manual bool) {
l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
}
- event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", "", nil)
+ event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
event.Add("status", model.STATUS_OFFLINE)
event.Add("user_id", status.UserId)
go Publish(event)
@@ -168,15 +252,18 @@ func SetStatusAwayIfNeeded(userId string, manual bool) {
l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
}
- event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", "", nil)
+ event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
event.Add("status", model.STATUS_AWAY)
event.Add("user_id", status.UserId)
go Publish(event)
}
func GetStatus(userId string) (*model.Status, *model.AppError) {
- if status, ok := statusCache.Get(userId); ok {
- return status.(*model.Status), nil
+ if result, ok := statusCache.Get(userId); ok {
+ status := result.(*model.Status)
+ statusCopy := &model.Status{}
+ *statusCopy = *status
+ return statusCopy, nil
}
if result := <-Srv.Store.Status().Get(userId); result.Err != nil {
@@ -232,6 +319,10 @@ func SetActiveChannel(userId string, channelId string) *model.AppError {
status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis(), channelId}
} else {
status.ActiveChannel = channelId
+ if !status.Manual {
+ status.Status = model.STATUS_ONLINE
+ }
+ status.LastActivityAt = model.GetMillis()
}
AddStatusCache(status)
diff --git a/api/status_test.go b/api/status_test.go
index f15c239db..ffc946817 100644
--- a/api/status_test.go
+++ b/api/status_test.go
@@ -4,13 +4,12 @@
package api
import (
- "strings"
- "testing"
- "time"
-
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
+ "strings"
+ "testing"
+ "time"
)
func TestStatuses(t *testing.T) {
@@ -59,7 +58,7 @@ func TestStatuses(t *testing.T) {
t.Fatal(err2)
}
- time.Sleep(300 * time.Millisecond)
+ time.Sleep(500 * time.Millisecond)
WebSocketClient.GetStatuses()
if resp := <-WebSocketClient.ResponseChannel; resp.Error != nil {
@@ -76,6 +75,7 @@ func TestStatuses(t *testing.T) {
}
if status, ok := resp.Data[th.BasicUser2.Id]; !ok {
+ t.Log(len(resp.Data))
t.Fatal("should have had user status")
} else if status != model.STATUS_ONLINE {
t.Log(status)
@@ -83,7 +83,55 @@ func TestStatuses(t *testing.T) {
}
}
- SetStatusAwayIfNeeded(th.BasicUser2.Id, false)
+ WebSocketClient.GetStatusesByIds([]string{th.BasicUser2.Id})
+ if resp := <-WebSocketClient.ResponseChannel; resp.Error != nil {
+ t.Fatal(resp.Error)
+ } else {
+ if resp.SeqReply != WebSocketClient.Sequence-1 {
+ t.Fatal("bad sequence number")
+ }
+
+ for _, status := range resp.Data {
+ if status != model.STATUS_OFFLINE && status != model.STATUS_AWAY && status != model.STATUS_ONLINE {
+ t.Fatal("one of the statuses had an invalid value")
+ }
+ }
+
+ if status, ok := resp.Data[th.BasicUser2.Id]; !ok {
+ t.Log(len(resp.Data))
+ t.Fatal("should have had user status")
+ } else if status != model.STATUS_ONLINE {
+ t.Log(status)
+ t.Fatal("status should have been online")
+ } else if len(resp.Data) != 1 {
+ t.Fatal("only 1 status should be returned")
+ }
+ }
+
+ WebSocketClient.GetStatusesByIds([]string{ruser2.Id, "junk"})
+ if resp := <-WebSocketClient.ResponseChannel; resp.Error != nil {
+ t.Fatal(resp.Error)
+ } else {
+ if resp.SeqReply != WebSocketClient.Sequence-1 {
+ t.Fatal("bad sequence number")
+ }
+
+ if len(resp.Data) != 2 {
+ t.Fatal("2 statuses should be returned")
+ }
+ }
+
+ WebSocketClient.GetStatusesByIds([]string{})
+ if resp := <-WebSocketClient.ResponseChannel; resp.Error == nil {
+ if resp.SeqReply != WebSocketClient.Sequence-1 {
+ t.Fatal("bad sequence number")
+ }
+ t.Fatal("should have errored - empty user ids")
+ }
+
+ WebSocketClient2.Close()
+
+ SetStatusAwayIfNeeded(th.BasicUser.Id, false)
awayTimeout := *utils.Cfg.TeamSettings.UserStatusAwayTimeout
defer func() {
@@ -93,10 +141,9 @@ func TestStatuses(t *testing.T) {
time.Sleep(1500 * time.Millisecond)
- SetStatusAwayIfNeeded(th.BasicUser2.Id, false)
- SetStatusAwayIfNeeded(th.BasicUser2.Id, false)
+ SetStatusAwayIfNeeded(th.BasicUser.Id, false)
+ SetStatusOnline(th.BasicUser.Id, "junk", false)
- WebSocketClient2.Close()
time.Sleep(300 * time.Millisecond)
WebSocketClient.GetStatuses()
@@ -115,20 +162,17 @@ func TestStatuses(t *testing.T) {
stop := make(chan bool)
onlineHit := false
awayHit := false
- offlineHit := false
go func() {
for {
select {
case resp := <-WebSocketClient.EventChannel:
- if resp.Event == model.WEBSOCKET_EVENT_STATUS_CHANGE && resp.Data["user_id"].(string) == th.BasicUser2.Id {
+ if resp.Event == model.WEBSOCKET_EVENT_STATUS_CHANGE && resp.Data["user_id"].(string) == th.BasicUser.Id {
status := resp.Data["status"].(string)
if status == model.STATUS_ONLINE {
onlineHit = true
} else if status == model.STATUS_AWAY {
awayHit = true
- } else if status == model.STATUS_OFFLINE {
- offlineHit = true
}
}
case <-stop:
@@ -147,11 +191,40 @@ func TestStatuses(t *testing.T) {
if !awayHit {
t.Fatal("didn't get away event")
}
- if !offlineHit {
- t.Fatal("didn't get offline event")
+
+ time.Sleep(500 * time.Millisecond)
+
+ WebSocketClient.Close()
+}
+
+func TestGetStatusesByIds(t *testing.T) {
+ th := Setup().InitBasic()
+ Client := th.BasicClient
+
+ if result, err := Client.GetStatusesByIds([]string{th.BasicUser.Id}); err != nil {
+ t.Fatal(err)
+ } else {
+ statuses := result.Data.(map[string]string)
+ if len(statuses) != 1 {
+ t.Fatal("should only have 1 status")
+ }
+ }
+
+ if result, err := Client.GetStatusesByIds([]string{th.BasicUser.Id, th.BasicUser2.Id, "junk"}); err != nil {
+ t.Fatal(err)
+ } else {
+ statuses := result.Data.(map[string]string)
+ if len(statuses) != 3 {
+ t.Fatal("should have 3 statuses")
+ }
+ }
+
+ if _, err := Client.GetStatusesByIds([]string{}); err == nil {
+ t.Fatal("should have errored")
}
}
+/*
func TestSetActiveChannel(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
@@ -185,8 +258,9 @@ func TestSetActiveChannel(t *testing.T) {
time.Sleep(500 * time.Millisecond)
status, _ = GetStatus(th.BasicUser.Id)
- // need to check if offline to catch race
+ need to check if offline to catch race
if status.Status != model.STATUS_OFFLINE && status.ActiveChannel != th.BasicChannel.Id {
t.Fatal("active channel should be set")
}
}
+*/
diff --git a/api/team.go b/api/team.go
index 2be7b8545..c57591d46 100644
--- a/api/team.go
+++ b/api/team.go
@@ -31,9 +31,12 @@ func InitTeam() {
BaseRoutes.Teams.Handle("/all_team_listings", ApiUserRequired(GetAllTeamListings)).Methods("GET")
BaseRoutes.Teams.Handle("/get_invite_info", ApiAppHandler(getInviteInfo)).Methods("POST")
BaseRoutes.Teams.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST")
- BaseRoutes.Teams.Handle("/members/{id:[A-Za-z0-9]+}", ApiUserRequired(getMembers)).Methods("GET")
BaseRoutes.NeedTeam.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET")
+ BaseRoutes.NeedTeam.Handle("/stats", ApiUserRequired(getTeamStats)).Methods("GET")
+ BaseRoutes.NeedTeam.Handle("/members/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getTeamMembers)).Methods("GET")
+ BaseRoutes.NeedTeam.Handle("/members/ids", ApiUserRequired(getTeamMembersByIds)).Methods("POST")
+ BaseRoutes.NeedTeam.Handle("/members/{user_id:[A-Za-z0-9]+}", ApiUserRequired(getTeamMember)).Methods("GET")
BaseRoutes.NeedTeam.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST")
BaseRoutes.NeedTeam.Handle("/update_member_roles", ApiUserRequired(updateMemberRoles)).Methods("POST")
@@ -305,7 +308,9 @@ func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError {
InvalidateCacheForUser(user.Id)
// This message goes to everyone, so the teamId, channelId and userId are irrelevant
- go Publish(model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil))
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil)
+ message.Add("user_id", user.Id)
+ go Publish(message)
return nil
}
@@ -335,11 +340,10 @@ func LeaveTeam(team *model.Team, user *model.User) *model.AppError {
for _, channel := range channelMembers.Channels {
if channel.Type != model.CHANNEL_DIRECT {
+ Srv.Store.User().InvalidateProfilesInChannelCache(channel.Id)
if result := <-Srv.Store.Channel().RemoveMember(channel.Id, user.Id); result.Err != nil {
return result.Err
}
-
- InvalidateCacheForChannel(channel.Id)
}
}
@@ -889,6 +893,25 @@ func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
+func getTeamStats(c *Context, w http.ResponseWriter, r *http.Request) {
+ if c.Session.GetTeamByTeamId(c.TeamId) == nil {
+ if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ return
+ }
+ }
+
+ if result := <-Srv.Store.Team().GetMemberCount(c.TeamId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ stats := &model.TeamStats{}
+ stats.MemberCount = result.Data.(int64)
+ stats.TeamId = c.TeamId
+ w.Write([]byte(stats.ToJson()))
+ return
+ }
+}
+
func importTeam(c *Context, w http.ResponseWriter, r *http.Request) {
if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_IMPORT_TEAM) {
c.Err = model.NewLocAppError("importTeam", "api.team.import_team.admin.app_error", nil, "userId="+c.Session.UserId)
@@ -982,17 +1005,76 @@ func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
-func getMembers(c *Context, w http.ResponseWriter, r *http.Request) {
+func getTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
- id := params["id"]
- if c.Session.GetTeamByTeamId(id) == nil {
- if !HasPermissionToTeamContext(c, id, model.PERMISSION_MANAGE_SYSTEM) {
+ offset, err := strconv.Atoi(params["offset"])
+ if err != nil {
+ c.SetInvalidParam("getTeamMembers", "offset")
+ return
+ }
+
+ limit, err := strconv.Atoi(params["limit"])
+ if err != nil {
+ c.SetInvalidParam("getTeamMembers", "limit")
+ return
+ }
+
+ if c.Session.GetTeamByTeamId(c.TeamId) == nil {
+ if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_MANAGE_SYSTEM) {
+ return
+ }
+ }
+
+ if result := <-Srv.Store.Team().GetMembers(c.TeamId, offset, limit); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ members := result.Data.([]*model.TeamMember)
+ w.Write([]byte(model.TeamMembersToJson(members)))
+ return
+ }
+}
+
+func getTeamMember(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+
+ userId := params["user_id"]
+ if len(userId) < 26 {
+ c.SetInvalidParam("getTeamMember", "user_id")
+ return
+ }
+
+ if c.Session.GetTeamByTeamId(c.TeamId) == nil {
+ if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_MANAGE_SYSTEM) {
+ return
+ }
+ }
+
+ if result := <-Srv.Store.Team().GetMember(c.TeamId, userId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ member := result.Data.(model.TeamMember)
+ w.Write([]byte(member.ToJson()))
+ return
+ }
+}
+
+func getTeamMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
+ userIds := model.ArrayFromJson(r.Body)
+ if len(userIds) == 0 {
+ c.SetInvalidParam("getTeamMembersByIds", "user_ids")
+ return
+ }
+
+ if c.Session.GetTeamByTeamId(c.TeamId) == nil {
+ if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_MANAGE_SYSTEM) {
return
}
}
- if result := <-Srv.Store.Team().GetMembers(id); result.Err != nil {
+ if result := <-Srv.Store.Team().GetMembersByIds(c.TeamId, userIds); result.Err != nil {
c.Err = result.Err
return
} else {
diff --git a/api/team_test.go b/api/team_test.go
index 1a66a826f..a58710145 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -560,14 +560,80 @@ func TestGetMyTeam(t *testing.T) {
func TestGetTeamMembers(t *testing.T) {
th := Setup().InitBasic()
- if result, err := th.BasicClient.GetTeamMembers(th.BasicTeam.Id); err != nil {
+ if result, err := th.BasicClient.GetTeamMembers(th.BasicTeam.Id, 0, 100); err != nil {
t.Fatal(err)
} else {
members := result.Data.([]*model.TeamMember)
- if members == nil {
+ if len(members) == 0 {
+ t.Fatal("should have results")
+ }
+ }
+
+ if _, err := th.BasicClient.GetTeamMembers("junk", 0, 100); err == nil {
+ t.Fatal("should have errored - bad team id")
+ }
+}
+
+func TestGetTeamMember(t *testing.T) {
+ th := Setup().InitBasic()
+
+ if result, err := th.BasicClient.GetTeamMember(th.BasicTeam.Id, th.BasicUser.Id); err != nil {
+ t.Fatal(err)
+ } else {
+ member := result.Data.(*model.TeamMember)
+ if member == nil {
t.Fatal("should be valid")
}
}
+
+ if _, err := th.BasicClient.GetTeamMember("junk", th.BasicUser.Id); err == nil {
+ t.Fatal("should have errored - bad team id")
+ }
+
+ if _, err := th.BasicClient.GetTeamMember(th.BasicTeam.Id, ""); err == nil {
+ t.Fatal("should have errored - blank user id")
+ }
+
+ if _, err := th.BasicClient.GetTeamMember(th.BasicTeam.Id, "junk"); err == nil {
+ t.Fatal("should have errored - bad user id")
+ }
+
+ if _, err := th.BasicClient.GetTeamMember(th.BasicTeam.Id, "12345678901234567890123456"); err == nil {
+ t.Fatal("should have errored - bad user id")
+ }
+}
+
+func TestGetTeamMembersByIds(t *testing.T) {
+ th := Setup().InitBasic()
+
+ if result, err := th.BasicClient.GetTeamMembersByIds(th.BasicTeam.Id, []string{th.BasicUser.Id}); err != nil {
+ t.Fatal(err)
+ } else {
+ member := result.Data.([]*model.TeamMember)[0]
+ if member.UserId != th.BasicUser.Id {
+ t.Fatal("user id did not match")
+ }
+ if member.TeamId != th.BasicTeam.Id {
+ t.Fatal("team id did not match")
+ }
+ }
+
+ if result, err := th.BasicClient.GetTeamMembersByIds(th.BasicTeam.Id, []string{th.BasicUser.Id, th.BasicUser2.Id, model.NewId()}); err != nil {
+ t.Fatal(err)
+ } else {
+ members := result.Data.([]*model.TeamMember)
+ if len(members) != 2 {
+ t.Fatal("length should have been 2")
+ }
+ }
+
+ if _, err := th.BasicClient.GetTeamMembersByIds("junk", []string{th.BasicUser.Id}); err == nil {
+ t.Fatal("should have errored - bad team id")
+ }
+
+ if _, err := th.BasicClient.GetTeamMembersByIds(th.BasicTeam.Id, []string{}); err == nil {
+ t.Fatal("should have errored - empty user ids")
+ }
}
func TestUpdateTeamMemberRoles(t *testing.T) {
@@ -632,3 +698,42 @@ func TestUpdateTeamMemberRoles(t *testing.T) {
t.Fatal("Should have worked, user is team admin")
}
}
+
+func TestGetTeamStats(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ Client := th.BasicClient
+
+ if result, err := th.SystemAdminClient.GetTeamStats(th.BasicTeam.Id); err != nil {
+ t.Fatal(err)
+ } else {
+ if result.Data.(*model.TeamStats).MemberCount != 2 {
+ t.Fatal("wrong count")
+ }
+ }
+
+ if result, err := th.SystemAdminClient.GetTeamStats("junk"); err != nil {
+ t.Fatal(err)
+ } else {
+ if result.Data.(*model.TeamStats).MemberCount != 0 {
+ t.Fatal("wrong count")
+ }
+ }
+
+ if result, err := th.SystemAdminClient.GetTeamStats(th.BasicTeam.Id); err != nil {
+ t.Fatal(err)
+ } else {
+ if result.Data.(*model.TeamStats).MemberCount != 2 {
+ t.Fatal("wrong count")
+ }
+ }
+
+ user := model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
+ ruser, _ := Client.CreateUser(&user, "")
+ store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
+
+ Client.Login(user.Email, user.Password)
+
+ if _, err := Client.GetTeamStats(th.BasicTeam.Id); err == nil {
+ t.Fatal("should have errored - not on team")
+ }
+}
diff --git a/api/user.go b/api/user.go
index 5f7e3ad10..12e57a33f 100644
--- a/api/user.go
+++ b/api/user.go
@@ -53,9 +53,15 @@ func InitUser() {
BaseRoutes.Users.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
BaseRoutes.Users.Handle("/me", ApiUserRequired(getMe)).Methods("GET")
BaseRoutes.Users.Handle("/initial_load", ApiAppHandler(getInitialLoad)).Methods("GET")
- BaseRoutes.Users.Handle("/direct_profiles", ApiUserRequired(getDirectProfiles)).Methods("GET")
- BaseRoutes.Users.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET")
- BaseRoutes.Users.Handle("/profiles_for_dm_list/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfilesForDirectMessageList)).Methods("GET")
+ BaseRoutes.Users.Handle("/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getProfiles)).Methods("GET")
+ BaseRoutes.NeedTeam.Handle("/users/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getProfilesInTeam)).Methods("GET")
+ BaseRoutes.NeedChannel.Handle("/users/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getProfilesInChannel)).Methods("GET")
+ BaseRoutes.NeedChannel.Handle("/users/not_in_channel/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getProfilesNotInChannel)).Methods("GET")
+ BaseRoutes.Users.Handle("/search", ApiUserRequired(searchUsers)).Methods("POST")
+ BaseRoutes.Users.Handle("/ids", ApiUserRequired(getProfilesByIds)).Methods("POST")
+
+ BaseRoutes.NeedTeam.Handle("/users/autocomplete", ApiUserRequired(autocompleteUsersInTeam)).Methods("GET")
+ BaseRoutes.NeedChannel.Handle("/users/autocomplete", ApiUserRequired(autocompleteUsersInChannel)).Methods("GET")
BaseRoutes.Users.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST")
BaseRoutes.Users.Handle("/generate_mfa_qr", ApiUserRequiredTrustRequester(generateMfaQrCode)).Methods("GET")
@@ -270,7 +276,9 @@ func CreateUser(user *model.User) (*model.User, *model.AppError) {
ruser.Sanitize(map[string]bool{})
// This message goes to everyone, so the teamId, channelId and userId are irrelevant
- go Publish(model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil))
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil)
+ message.Add("user_id", ruser.Id)
+ go Publish(message)
return ruser, nil
}
@@ -379,7 +387,7 @@ func sendWelcomeEmail(c *Context, userId string, email string, siteURL string, v
func addDirectChannels(teamId string, user *model.User) {
var profiles map[string]*model.User
- if result := <-Srv.Store.User().GetProfiles(teamId); result.Err != nil {
+ if result := <-Srv.Store.User().GetProfiles(teamId, 0, 100); result.Err != nil {
l4g.Error(utils.T("api.user.add_direct_channels_and_forget.failed.error"), user.Id, teamId, result.Err.Error())
return
} else {
@@ -875,7 +883,6 @@ func getInitialLoad(c *Context, w http.ResponseWriter, r *http.Request) {
uchan := Srv.Store.User().Get(c.Session.UserId)
pchan := Srv.Store.Preference().GetAll(c.Session.UserId)
tchan := Srv.Store.Team().GetTeamsByUserId(c.Session.UserId)
- dpchan := Srv.Store.User().GetDirectProfiles(c.Session.UserId)
il.TeamMembers = c.Session.TeamMembers
@@ -904,19 +911,6 @@ func getInitialLoad(c *Context, w http.ResponseWriter, r *http.Request) {
team.Sanitize()
}
}
-
- if dp := <-dpchan; dp.Err != nil {
- c.Err = dp.Err
- return
- } else {
- profiles := dp.Data.(map[string]*model.User)
-
- for k, p := range profiles {
- profiles[k] = sanitizeProfile(c, p)
- }
-
- il.DirectProfiles = profiles
- }
}
if cchan != nil {
@@ -960,25 +954,27 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
-func getProfilesForDirectMessageList(c *Context, w http.ResponseWriter, r *http.Request) {
+func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
- id := params["id"]
- var pchan store.StoreChannel
+ offset, err := strconv.Atoi(params["offset"])
+ if err != nil {
+ c.SetInvalidParam("getProfiles", "offset")
+ return
+ }
- if *utils.Cfg.TeamSettings.RestrictDirectMessage == model.DIRECT_MESSAGE_TEAM {
- if c.Session.GetTeamByTeamId(id) == nil {
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
- return
- }
- }
+ limit, err := strconv.Atoi(params["limit"])
+ if err != nil {
+ c.SetInvalidParam("getProfiles", "limit")
+ return
+ }
- pchan = Srv.Store.User().GetProfiles(id)
- } else {
- pchan = Srv.Store.User().GetAllProfiles()
+ etag := (<-Srv.Store.User().GetEtagForAllProfiles()).Data.(string)
+ if HandleEtag(etag, w, r) {
+ return
}
- if result := <-pchan; result.Err != nil {
+ if result := <-Srv.Store.User().GetAllProfiles(offset, limit); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -988,26 +984,39 @@ func getProfilesForDirectMessageList(c *Context, w http.ResponseWriter, r *http.
profiles[k] = sanitizeProfile(c, p)
}
+ w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.Write([]byte(model.UserMapToJson(profiles)))
}
}
-func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
+func getProfilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
- id := params["id"]
+ teamId := params["team_id"]
- if c.Session.GetTeamByTeamId(id) == nil {
+ if c.Session.GetTeamByTeamId(teamId) == nil {
if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
return
}
}
- etag := (<-Srv.Store.User().GetEtagForProfiles(id)).Data.(string)
+ offset, err := strconv.Atoi(params["offset"])
+ if err != nil {
+ c.SetInvalidParam("getProfilesInTeam", "offset")
+ return
+ }
+
+ limit, err := strconv.Atoi(params["limit"])
+ if err != nil {
+ c.SetInvalidParam("getProfilesInTeam", "limit")
+ return
+ }
+
+ etag := (<-Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string)
if HandleEtag(etag, w, r) {
return
}
- if result := <-Srv.Store.User().GetProfiles(id); result.Err != nil {
+ if result := <-Srv.Store.User().GetProfiles(teamId, offset, limit); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -1022,13 +1031,73 @@ func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
-func getDirectProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
- etag := (<-Srv.Store.User().GetEtagForDirectProfiles(c.Session.UserId)).Data.(string)
- if HandleEtag(etag, w, r) {
+func getProfilesInChannel(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ channelId := params["channel_id"]
+
+ if c.Session.GetTeamByTeamId(c.TeamId) == nil {
+ if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ return
+ }
+ }
+
+ if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ return
+ }
+
+ offset, err := strconv.Atoi(params["offset"])
+ if err != nil {
+ c.SetInvalidParam("getProfiles", "offset")
return
}
- if result := <-Srv.Store.User().GetDirectProfiles(c.Session.UserId); result.Err != nil {
+ limit, err := strconv.Atoi(params["limit"])
+ if err != nil {
+ c.SetInvalidParam("getProfiles", "limit")
+ return
+ }
+
+ if result := <-Srv.Store.User().GetProfilesInChannel(channelId, offset, limit, false); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ profiles := result.Data.(map[string]*model.User)
+
+ for k, p := range profiles {
+ profiles[k] = sanitizeProfile(c, p)
+ }
+
+ w.Write([]byte(model.UserMapToJson(profiles)))
+ }
+}
+
+func getProfilesNotInChannel(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ channelId := params["channel_id"]
+
+ if c.Session.GetTeamByTeamId(c.TeamId) == nil {
+ if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ return
+ }
+ }
+
+ if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ return
+ }
+
+ offset, err := strconv.Atoi(params["offset"])
+ if err != nil {
+ c.SetInvalidParam("getProfiles", "offset")
+ return
+ }
+
+ limit, err := strconv.Atoi(params["limit"])
+ if err != nil {
+ c.SetInvalidParam("getProfiles", "limit")
+ return
+ }
+
+ if result := <-Srv.Store.User().GetProfilesNotInChannel(c.TeamId, channelId, offset, limit); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -1038,7 +1107,6 @@ func getDirectProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
profiles[k] = sanitizeProfile(c, p)
}
- w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.Write([]byte(model.UserMapToJson(profiles)))
}
}
@@ -2522,3 +2590,152 @@ func sanitizeProfile(c *Context, user *model.User) *model.User {
return user
}
+
+func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+
+ term := props["term"]
+ if len(term) == 0 {
+ c.SetInvalidParam("searchUsers", "term")
+ return
+ }
+
+ teamId := props["team_id"]
+ inChannelId := props["in_channel"]
+ notInChannelId := props["not_in_channel"]
+
+ if inChannelId != "" && !HasPermissionToChannelContext(c, inChannelId, model.PERMISSION_READ_CHANNEL) {
+ return
+ }
+
+ if notInChannelId != "" && !HasPermissionToChannelContext(c, notInChannelId, model.PERMISSION_READ_CHANNEL) {
+ return
+ }
+
+ var uchan store.StoreChannel
+ if inChannelId != "" {
+ uchan = Srv.Store.User().SearchInChannel(inChannelId, term, store.USER_SEARCH_TYPE_USERNAME)
+ } else if notInChannelId != "" {
+ uchan = Srv.Store.User().SearchNotInChannel(teamId, notInChannelId, term, store.USER_SEARCH_TYPE_USERNAME)
+ } else {
+ uchan = Srv.Store.User().Search(teamId, term, store.USER_SEARCH_TYPE_USERNAME)
+ }
+
+ if result := <-uchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ profiles := result.Data.([]*model.User)
+
+ for _, p := range profiles {
+ sanitizeProfile(c, p)
+ }
+
+ w.Write([]byte(model.UserListToJson(profiles)))
+ }
+}
+
+func getProfilesByIds(c *Context, w http.ResponseWriter, r *http.Request) {
+ userIds := model.ArrayFromJson(r.Body)
+
+ if len(userIds) == 0 {
+ c.SetInvalidParam("getProfilesByIds", "user_ids")
+ return
+ }
+
+ if result := <-Srv.Store.User().GetProfileByIds(userIds); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ profiles := result.Data.(map[string]*model.User)
+
+ for _, p := range profiles {
+ sanitizeProfile(c, p)
+ }
+
+ w.Write([]byte(model.UserMapToJson(profiles)))
+ }
+}
+
+func autocompleteUsersInChannel(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ channelId := params["channel_id"]
+ teamId := params["team_id"]
+
+ term := r.URL.Query().Get("term")
+
+ if c.Session.GetTeamByTeamId(teamId) == nil {
+ if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ return
+ }
+ }
+
+ if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ return
+ }
+
+ uchan := Srv.Store.User().SearchInChannel(channelId, term, store.USER_SEARCH_TYPE_ALL)
+ nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, store.USER_SEARCH_TYPE_ALL)
+
+ autocomplete := &model.UserAutocompleteInChannel{}
+
+ if result := <-uchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ profiles := result.Data.([]*model.User)
+
+ for _, p := range profiles {
+ sanitizeProfile(c, p)
+ }
+
+ autocomplete.InChannel = profiles
+ }
+
+ if result := <-nuchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ profiles := result.Data.([]*model.User)
+
+ for _, p := range profiles {
+ sanitizeProfile(c, p)
+ }
+
+ autocomplete.OutOfChannel = profiles
+ }
+
+ w.Write([]byte(autocomplete.ToJson()))
+}
+
+func autocompleteUsersInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ teamId := params["team_id"]
+
+ term := r.URL.Query().Get("term")
+
+ if c.Session.GetTeamByTeamId(teamId) == nil {
+ if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ return
+ }
+ }
+
+ uchan := Srv.Store.User().Search(teamId, term, store.USER_SEARCH_TYPE_ALL)
+
+ autocomplete := &model.UserAutocompleteInTeam{}
+
+ if result := <-uchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ profiles := result.Data.([]*model.User)
+
+ for _, p := range profiles {
+ sanitizeProfile(c, p)
+ }
+
+ autocomplete.InTeam = profiles
+ }
+
+ w.Write([]byte(autocomplete.ToJson()))
+}
diff --git a/api/user_test.go b/api/user_test.go
index 3800f5d9e..3b6fcb1fb 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -435,7 +435,7 @@ func TestGetUser(t *testing.T) {
}
}
- if userMap, err := Client.GetProfiles(rteam.Data.(*model.Team).Id, ""); err != nil {
+ if userMap, err := Client.GetProfilesInTeam(rteam.Data.(*model.Team).Id, 0, 100, ""); err != nil {
t.Fatal(err)
} else if len(userMap.Data.(map[string]*model.User)) != 2 {
t.Fatal("should have been 2")
@@ -444,7 +444,7 @@ func TestGetUser(t *testing.T) {
} else {
// test etag caching
- if cache_result, err := Client.GetProfiles(rteam.Data.(*model.Team).Id, userMap.Etag); err != nil {
+ if cache_result, err := Client.GetProfilesInTeam(rteam.Data.(*model.Team).Id, 0, 100, userMap.Etag); err != nil {
t.Fatal(err)
} else if cache_result.Data.(map[string]*model.User) != nil {
t.Log(cache_result.Data)
@@ -452,7 +452,25 @@ func TestGetUser(t *testing.T) {
}
}
- if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err == nil {
+ if userMap, err := Client.GetProfilesInTeam(rteam.Data.(*model.Team).Id, 0, 1, ""); err != nil {
+ t.Fatal(err)
+ } else if len(userMap.Data.(map[string]*model.User)) != 1 {
+ t.Fatal("should have been 1")
+ }
+
+ if userMap, err := Client.GetProfilesInTeam(rteam.Data.(*model.Team).Id, 1, 1, ""); err != nil {
+ t.Fatal(err)
+ } else if len(userMap.Data.(map[string]*model.User)) != 1 {
+ t.Fatal("should have been 1")
+ }
+
+ if userMap, err := Client.GetProfilesInTeam(rteam.Data.(*model.Team).Id, 10, 10, ""); err != nil {
+ t.Fatal(err)
+ } else if len(userMap.Data.(map[string]*model.User)) != 0 {
+ t.Fatal("should have been 0")
+ }
+
+ if _, err := Client.GetProfilesInTeam(rteam2.Data.(*model.Team).Id, 0, 100, ""); err == nil {
t.Fatal("shouldn't have access")
}
@@ -468,12 +486,12 @@ func TestGetUser(t *testing.T) {
Client.Login(user.Email, "passwd1")
- if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err != nil {
+ if _, err := Client.GetProfilesInTeam(rteam2.Data.(*model.Team).Id, 0, 100, ""); err != nil {
t.Fatal(err)
}
}
-func TestGetDirectProfiles(t *testing.T) {
+func TestGetProfiles(t *testing.T) {
th := Setup().InitBasic()
th.BasicClient.Must(th.BasicClient.CreateDirectChannel(th.BasicUser2.Id))
@@ -485,41 +503,42 @@ func TestGetDirectProfiles(t *testing.T) {
utils.Cfg.PrivacySettings.ShowEmailAddress = true
- if result, err := th.BasicClient.GetDirectProfiles(""); err != nil {
+ if result, err := th.BasicClient.GetProfiles(0, 100, ""); err != nil {
t.Fatal(err)
} else {
users := result.Data.(map[string]*model.User)
- if len(users) != 1 {
+ if len(users) < 1 {
t.Fatal("map was wrong length")
}
- if users[th.BasicUser2.Id] == nil {
- t.Fatal("missing expected user")
- }
-
for _, user := range users {
if user.Email == "" {
t.Fatal("problem with show email")
}
}
+
+ // test etag caching
+ if cache_result, err := th.BasicClient.GetProfiles(0, 100, result.Etag); err != nil {
+ t.Fatal(err)
+ } else if cache_result.Data.(map[string]*model.User) != nil {
+ t.Log(cache_result.Etag)
+ t.Log(result.Etag)
+ t.Fatal("cache should be empty")
+ }
}
utils.Cfg.PrivacySettings.ShowEmailAddress = false
- if result, err := th.BasicClient.GetDirectProfiles(""); err != nil {
+ if result, err := th.BasicClient.GetProfiles(0, 100, ""); err != nil {
t.Fatal(err)
} else {
users := result.Data.(map[string]*model.User)
- if len(users) != 1 {
+ if len(users) < 1 {
t.Fatal("map was wrong length")
}
- if users[th.BasicUser2.Id] == nil {
- t.Fatal("missing expected user")
- }
-
for _, user := range users {
if user.Email != "" {
t.Fatal("problem with show email")
@@ -528,11 +547,9 @@ func TestGetDirectProfiles(t *testing.T) {
}
}
-func TestGetProfilesForDirectMessageList(t *testing.T) {
+func TestGetProfilesByIds(t *testing.T) {
th := Setup().InitBasic()
- th.BasicClient.Must(th.BasicClient.CreateDirectChannel(th.BasicUser2.Id))
-
prevShowEmail := utils.Cfg.PrivacySettings.ShowEmailAddress
defer func() {
utils.Cfg.PrivacySettings.ShowEmailAddress = prevShowEmail
@@ -540,12 +557,12 @@ func TestGetProfilesForDirectMessageList(t *testing.T) {
utils.Cfg.PrivacySettings.ShowEmailAddress = true
- if result, err := th.BasicClient.GetProfilesForDirectMessageList(th.BasicTeam.Id); err != nil {
+ if result, err := th.BasicClient.GetProfilesByIds([]string{th.BasicUser.Id}); err != nil {
t.Fatal(err)
} else {
users := result.Data.(map[string]*model.User)
- if len(users) < 1 {
+ if len(users) != 1 {
t.Fatal("map was wrong length")
}
@@ -558,12 +575,12 @@ func TestGetProfilesForDirectMessageList(t *testing.T) {
utils.Cfg.PrivacySettings.ShowEmailAddress = false
- if result, err := th.BasicClient.GetProfilesForDirectMessageList(th.BasicTeam.Id); err != nil {
+ if result, err := th.BasicClient.GetProfilesByIds([]string{th.BasicUser.Id}); err != nil {
t.Fatal(err)
} else {
users := result.Data.(map[string]*model.User)
- if len(users) < 1 {
+ if len(users) != 1 {
t.Fatal("map was wrong length")
}
@@ -573,6 +590,16 @@ func TestGetProfilesForDirectMessageList(t *testing.T) {
}
}
}
+
+ if result, err := th.BasicClient.GetProfilesByIds([]string{th.BasicUser.Id, th.BasicUser2.Id}); err != nil {
+ t.Fatal(err)
+ } else {
+ users := result.Data.(map[string]*model.User)
+
+ if len(users) != 2 {
+ t.Fatal("map was wrong length")
+ }
+ }
}
func TestGetAudits(t *testing.T) {
@@ -1837,3 +1864,366 @@ func TestUserTyping(t *testing.T) {
t.Fatal("did not receive typing event")
}
}
+
+func TestGetProfilesInChannel(t *testing.T) {
+ th := Setup().InitBasic()
+ Client := th.BasicClient
+
+ prevShowEmail := utils.Cfg.PrivacySettings.ShowEmailAddress
+ defer func() {
+ utils.Cfg.PrivacySettings.ShowEmailAddress = prevShowEmail
+ }()
+
+ utils.Cfg.PrivacySettings.ShowEmailAddress = true
+
+ if result, err := Client.GetProfilesInChannel(th.BasicChannel.Id, 0, 100, ""); err != nil {
+ t.Fatal(err)
+ } else {
+ users := result.Data.(map[string]*model.User)
+
+ if len(users) < 1 {
+ t.Fatal("map was wrong length")
+ }
+
+ for _, user := range users {
+ if user.Email == "" {
+ t.Fatal("problem with show email")
+ }
+ }
+ }
+
+ th.LoginBasic2()
+
+ if _, err := Client.GetProfilesInChannel(th.BasicChannel.Id, 0, 100, ""); err == nil {
+ t.Fatal("should not have access")
+ }
+
+ Client.Must(Client.JoinChannel(th.BasicChannel.Id))
+
+ utils.Cfg.PrivacySettings.ShowEmailAddress = false
+
+ if result, err := Client.GetProfilesInChannel(th.BasicChannel.Id, 0, 100, ""); err != nil {
+ t.Fatal(err)
+ } else {
+ users := result.Data.(map[string]*model.User)
+
+ if len(users) < 1 {
+ t.Fatal("map was wrong length")
+ }
+
+ found := false
+ for _, user := range users {
+ if user.Email != "" {
+ t.Fatal("problem with show email")
+ }
+ if user.Id == th.BasicUser2.Id {
+ found = true
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found profile")
+ }
+ }
+
+ user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
+ Client.Must(Client.CreateUser(&user, ""))
+
+ Client.Login(user.Email, "passwd1")
+ Client.SetTeamId("junk")
+
+ if _, err := Client.GetProfilesInChannel(th.BasicChannel.Id, 0, 100, ""); err == nil {
+ t.Fatal("should not have access")
+ }
+}
+
+func TestGetProfilesNotInChannel(t *testing.T) {
+ th := Setup().InitBasic()
+ Client := th.BasicClient
+
+ prevShowEmail := utils.Cfg.PrivacySettings.ShowEmailAddress
+ defer func() {
+ utils.Cfg.PrivacySettings.ShowEmailAddress = prevShowEmail
+ }()
+
+ utils.Cfg.PrivacySettings.ShowEmailAddress = true
+
+ if result, err := Client.GetProfilesNotInChannel(th.BasicChannel.Id, 0, 100, ""); err != nil {
+ t.Fatal(err)
+ } else {
+ users := result.Data.(map[string]*model.User)
+
+ if len(users) < 1 {
+ t.Fatal("map was wrong length")
+ }
+
+ found := false
+ for _, user := range users {
+ if user.Email == "" {
+ t.Fatal("problem with show email")
+ }
+ if user.Id == th.BasicUser2.Id {
+ found = true
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found profile")
+ }
+ }
+
+ user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ LinkUserToTeam(user, th.BasicTeam)
+
+ th.LoginBasic2()
+
+ if _, err := Client.GetProfilesNotInChannel(th.BasicChannel.Id, 0, 100, ""); err == nil {
+ t.Fatal("should not have access")
+ }
+
+ Client.Must(Client.JoinChannel(th.BasicChannel.Id))
+
+ utils.Cfg.PrivacySettings.ShowEmailAddress = false
+
+ if result, err := Client.GetProfilesNotInChannel(th.BasicChannel.Id, 0, 100, ""); err != nil {
+ t.Fatal(err)
+ } else {
+ users := result.Data.(map[string]*model.User)
+
+ if len(users) < 1 {
+ t.Fatal("map was wrong length")
+ }
+
+ found := false
+ for _, user := range users {
+ if user.Email != "" {
+ t.Fatal("problem with show email")
+ }
+ if user.Id == th.BasicUser2.Id {
+ found = true
+ }
+ }
+
+ if found {
+ t.Fatal("should not have found profile")
+ }
+ }
+
+ user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
+ Client.Must(Client.CreateUser(&user2, ""))
+
+ Client.Login(user2.Email, "passwd1")
+ Client.SetTeamId(th.BasicTeam.Id)
+
+ if _, err := Client.GetProfilesNotInChannel(th.BasicChannel.Id, 0, 100, ""); err == nil {
+ t.Fatal("should not have access")
+ }
+}
+
+func TestSearchUsers(t *testing.T) {
+ th := Setup().InitBasic()
+ Client := th.BasicClient
+
+ if result, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{}); err != nil {
+ t.Fatal(err)
+ } else {
+ users := result.Data.([]*model.User)
+
+ found := false
+ for _, user := range users {
+ if user.Id == th.BasicUser.Id {
+ found = true
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found profile")
+ }
+ }
+
+ if result, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{"in_channel": th.BasicChannel.Id}); err != nil {
+ t.Fatal(err)
+ } else {
+ users := result.Data.([]*model.User)
+
+ if len(users) != 1 {
+ t.Fatal("map was wrong length")
+ }
+
+ found := false
+ for _, user := range users {
+ if user.Id == th.BasicUser.Id {
+ found = true
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found profile")
+ }
+ }
+
+ if result, err := Client.SearchUsers(th.BasicUser2.Username, "", map[string]string{"not_in_channel": th.BasicChannel.Id}); err != nil {
+ t.Fatal(err)
+ } else {
+ users := result.Data.([]*model.User)
+
+ if len(users) != 1 {
+ t.Fatal("map was wrong length")
+ }
+
+ found1 := false
+ found2 := false
+ for _, user := range users {
+ if user.Id == th.BasicUser.Id {
+ found1 = true
+ } else if user.Id == th.BasicUser2.Id {
+ found2 = true
+ }
+ }
+
+ if found1 {
+ t.Fatal("should not have found profile")
+ }
+ if !found2 {
+ t.Fatal("should have found profile")
+ }
+ }
+
+ if result, err := Client.SearchUsers(th.BasicUser2.Username, th.BasicTeam.Id, map[string]string{"not_in_channel": th.BasicChannel.Id}); err != nil {
+ t.Fatal(err)
+ } else {
+ users := result.Data.([]*model.User)
+
+ if len(users) != 1 {
+ t.Fatal("map was wrong length")
+ }
+
+ found1 := false
+ found2 := false
+ for _, user := range users {
+ if user.Id == th.BasicUser.Id {
+ found1 = true
+ } else if user.Id == th.BasicUser2.Id {
+ found2 = true
+ }
+ }
+
+ if found1 {
+ t.Fatal("should not have found profile")
+ }
+ if !found2 {
+ t.Fatal("should have found profile")
+ }
+ }
+
+ if result, err := Client.SearchUsers(th.BasicUser.Username, "junk", map[string]string{"not_in_channel": th.BasicChannel.Id}); err != nil {
+ t.Fatal(err)
+ } else {
+ users := result.Data.([]*model.User)
+
+ if len(users) != 0 {
+ t.Fatal("map was wrong length")
+ }
+ }
+
+ th.LoginBasic2()
+
+ if result, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{}); err != nil {
+ t.Fatal(err)
+ } else {
+ users := result.Data.([]*model.User)
+
+ found := false
+ for _, user := range users {
+ if user.Id == th.BasicUser.Id {
+ found = true
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found profile")
+ }
+ }
+
+ if _, err := Client.SearchUsers("", "", map[string]string{}); err == nil {
+ t.Fatal("should have errored - blank term")
+ }
+
+ if _, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{"in_channel": th.BasicChannel.Id}); err == nil {
+ t.Fatal("should not have access")
+ }
+
+ if _, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{"not_in_channel": th.BasicChannel.Id}); err == nil {
+ t.Fatal("should not have access")
+ }
+}
+
+func TestAutocompleteUsers(t *testing.T) {
+ th := Setup().InitBasic()
+ Client := th.BasicClient
+
+ if result, err := Client.AutocompleteUsersInTeam(th.BasicUser.Username); err != nil {
+ t.Fatal(err)
+ } else {
+ autocomplete := result.Data.(*model.UserAutocompleteInTeam)
+ if len(autocomplete.InTeam) != 1 {
+ t.Fatal("should have returned 1 user in")
+ }
+ }
+
+ if result, err := Client.AutocompleteUsersInTeam(th.BasicUser.Username[0:5]); err != nil {
+ t.Fatal(err)
+ } else {
+ autocomplete := result.Data.(*model.UserAutocompleteInTeam)
+ if len(autocomplete.InTeam) < 1 {
+ t.Fatal("should have returned at least 1 user in")
+ }
+ }
+
+ if result, err := Client.AutocompleteUsersInChannel(th.BasicUser.Username, th.BasicChannel.Id); err != nil {
+ t.Fatal(err)
+ } else {
+ autocomplete := result.Data.(*model.UserAutocompleteInChannel)
+ if len(autocomplete.InChannel) != 1 {
+ t.Fatal("should have returned 1 user in")
+ }
+ if len(autocomplete.OutOfChannel) != 0 {
+ t.Fatal("should have returned no users out")
+ }
+ }
+
+ if result, err := Client.AutocompleteUsersInChannel("", th.BasicChannel.Id); err != nil {
+ t.Fatal(err)
+ } else {
+ autocomplete := result.Data.(*model.UserAutocompleteInChannel)
+ if len(autocomplete.InChannel) != 1 && autocomplete.InChannel[0].Id != th.BasicUser2.Id {
+ t.Fatal("should have returned at 1 user in")
+ }
+ if len(autocomplete.OutOfChannel) != 1 && autocomplete.OutOfChannel[0].Id != th.BasicUser2.Id {
+ t.Fatal("should have returned 1 user out")
+ }
+ }
+
+ if result, err := Client.AutocompleteUsersInTeam(""); err != nil {
+ t.Fatal(err)
+ } else {
+ autocomplete := result.Data.(*model.UserAutocompleteInTeam)
+ if len(autocomplete.InTeam) != 2 {
+ t.Fatal("should have returned 2 users in")
+ }
+ }
+
+ if _, err := Client.AutocompleteUsersInChannel("", "junk"); err == nil {
+ t.Fatal("should have errored - bad channel id")
+ }
+
+ Client.SetTeamId("junk")
+ if _, err := Client.AutocompleteUsersInChannel("", th.BasicChannel.Id); err == nil {
+ t.Fatal("should have errored - bad team id")
+ }
+
+ if _, err := Client.AutocompleteUsersInTeam(""); err == nil {
+ t.Fatal("should have errored - bad team id")
+ }
+}
diff --git a/api/web_conn.go b/api/web_conn.go
index f4bd493bb..7f3c1f875 100644
--- a/api/web_conn.go
+++ b/api/web_conn.go
@@ -4,54 +4,52 @@
package api
import (
+ "fmt"
"time"
"github.com/mattermost/platform/model"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/websocket"
goi18n "github.com/nicksnyder/go-i18n/i18n"
)
const (
- WRITE_WAIT = 10 * time.Second
- PONG_WAIT = 60 * time.Second
- PING_PERIOD = (PONG_WAIT * 9) / 10
- MAX_SIZE = 512
- REDIS_WAIT = 60 * time.Second
+ WRITE_WAIT = 30 * time.Second
+ PONG_WAIT = 100 * time.Second
+ PING_PERIOD = (PONG_WAIT * 6) / 10
)
type WebConn struct {
- WebSocket *websocket.Conn
- Send chan model.WebSocketMessage
- SessionToken string
- UserId string
- T goi18n.TranslateFunc
- Locale string
- isMemberOfChannel map[string]bool
- isMemberOfTeam map[string]bool
+ WebSocket *websocket.Conn
+ Send chan model.WebSocketMessage
+ SessionToken string
+ UserId string
+ T goi18n.TranslateFunc
+ Locale string
+ AllChannelMembers map[string]string
+ LastAllChannelMembersTime int64
}
func NewWebConn(c *Context, ws *websocket.Conn) *WebConn {
go SetStatusOnline(c.Session.UserId, c.Session.Id, false)
return &WebConn{
- Send: make(chan model.WebSocketMessage, 64),
- WebSocket: ws,
- UserId: c.Session.UserId,
- SessionToken: c.Session.Token,
- T: c.T,
- Locale: c.Locale,
- isMemberOfChannel: make(map[string]bool),
- isMemberOfTeam: make(map[string]bool),
+ Send: make(chan model.WebSocketMessage, 256),
+ WebSocket: ws,
+ UserId: c.Session.UserId,
+ SessionToken: c.Session.Token,
+ T: c.T,
+ Locale: c.Locale,
}
}
func (c *WebConn) readPump() {
defer func() {
- hub.Unregister(c)
+ HubUnregister(c)
c.WebSocket.Close()
}()
- c.WebSocket.SetReadLimit(MAX_SIZE)
+ c.WebSocket.SetReadLimit(SOCKET_MAX_MESSAGE_SIZE_KB)
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
c.WebSocket.SetPongHandler(func(string) error {
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
@@ -62,6 +60,13 @@ func (c *WebConn) readPump() {
for {
var req model.WebSocketRequest
if err := c.WebSocket.ReadJSON(&req); err != nil {
+ // browsers will appear as CloseNoStatusReceived
+ if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
+ l4g.Debug(fmt.Sprintf("websocket.read: client side closed socket userId=%v", c.UserId))
+ } else {
+ l4g.Debug(fmt.Sprintf("websocket.read: cannot read, closing websocket for userId=%v error=%v", c.UserId, err.Error()))
+ }
+
return
} else {
BaseRoutes.WebSocket.ServeWebSocket(c, &req)
@@ -87,63 +92,97 @@ func (c *WebConn) writePump() {
}
c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT))
- if err := c.WebSocket.WriteJSON(msg); err != nil {
+ if err := c.WebSocket.WriteMessage(websocket.TextMessage, msg.GetPreComputeJson()); err != nil {
+ // browsers will appear as CloseNoStatusReceived
+ if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
+ l4g.Debug(fmt.Sprintf("websocket.send: client side closed socket userId=%v", c.UserId))
+ } else {
+ l4g.Debug(fmt.Sprintf("websocket.send: cannot send, closing websocket for userId=%v, error=%v", c.UserId, err.Error()))
+ }
+
return
}
case <-ticker.C:
c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT))
if err := c.WebSocket.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
+ // browsers will appear as CloseNoStatusReceived
+ if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
+ l4g.Debug(fmt.Sprintf("websocket.ticker: client side closed socket userId=%v", c.UserId))
+ } else {
+ l4g.Debug(fmt.Sprintf("websocket.ticker: cannot read, closing websocket for userId=%v error=%v", c.UserId, err.Error()))
+ }
+
return
}
}
}
}
-func (c *WebConn) InvalidateCache() {
- c.isMemberOfTeam = make(map[string]bool)
- c.isMemberOfChannel = make(map[string]bool)
-}
+func (webCon *WebConn) InvalidateCache() {
+ webCon.AllChannelMembers = nil
+ webCon.LastAllChannelMembersTime = 0
-func (c *WebConn) InvalidateCacheForChannel(channelId string) {
- delete(c.isMemberOfChannel, channelId)
}
-func (c *WebConn) IsMemberOfTeam(teamId string) bool {
- isMember, ok := c.isMemberOfTeam[teamId]
- if !ok {
- session := GetSession(c.SessionToken)
- if session == nil {
- isMember = false
- c.isMemberOfTeam[teamId] = isMember
- } else {
- member := session.GetTeamByTeamId(teamId)
+func (webCon *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
+ // If the event is destined to a specific user
+ if len(msg.Broadcast.UserId) > 0 && webCon.UserId != msg.Broadcast.UserId {
+ return false
+ }
+
+ // if the user is omitted don't send the message
+ if len(msg.Broadcast.OmitUsers) > 0 {
+ if _, ok := msg.Broadcast.OmitUsers[webCon.UserId]; ok {
+ return false
+ }
+ }
- if member != nil {
- isMember = true
- c.isMemberOfTeam[teamId] = isMember
+ // Only report events to users who are in the channel for the event
+ if len(msg.Broadcast.ChannelId) > 0 {
+
+ if model.GetMillis()-webCon.LastAllChannelMembersTime > 1000*60*15 { // 15 minutes
+ webCon.AllChannelMembers = nil
+ webCon.LastAllChannelMembersTime = 0
+ }
+
+ if webCon.AllChannelMembers == nil {
+ if result := <-Srv.Store.Channel().GetAllChannelMembersForUser(webCon.UserId, true); result.Err != nil {
+ l4g.Error("webhub.shouldSendEvent: " + result.Err.Error())
+ return false
} else {
- isMember = true
- c.isMemberOfTeam[teamId] = isMember
+ webCon.AllChannelMembers = result.Data.(map[string]string)
+ webCon.LastAllChannelMembersTime = model.GetMillis()
}
+ }
+ if _, ok := webCon.AllChannelMembers[msg.Broadcast.ChannelId]; ok {
+ return true
+ } else {
+ return false
}
}
- return isMember
+ // Only report events to users who are in the team for the event
+ if len(msg.Broadcast.TeamId) > 0 {
+ return webCon.IsMemberOfTeam(msg.Broadcast.TeamId)
+
+ }
+
+ return true
}
-func (c *WebConn) IsMemberOfChannel(channelId string) bool {
- isMember, ok := c.isMemberOfChannel[channelId]
- if !ok {
- if cresult := <-Srv.Store.Channel().GetMember(channelId, c.UserId); cresult.Err != nil {
- isMember = false
- c.isMemberOfChannel[channelId] = isMember
+func (webCon *WebConn) IsMemberOfTeam(teamId string) bool {
+ session := GetSession(webCon.SessionToken)
+ if session == nil {
+ return false
+ } else {
+ member := session.GetTeamByTeamId(teamId)
+
+ if member != nil {
+ return true
} else {
- isMember = true
- c.isMemberOfChannel[channelId] = isMember
+ return false
}
}
-
- return isMember
}
diff --git a/api/web_hub.go b/api/web_hub.go
index 4a9719d80..5f480880e 100644
--- a/api/web_hub.go
+++ b/api/web_hub.go
@@ -5,6 +5,8 @@ package api
import (
"fmt"
+ "hash/fnv"
+ "runtime"
l4g "github.com/alecthomas/log4go"
@@ -14,27 +16,77 @@ import (
)
type Hub struct {
- connections map[*WebConn]bool
- register chan *WebConn
- unregister chan *WebConn
- broadcast chan *model.WebSocketEvent
- stop chan string
- invalidateUser chan string
- invalidateChannel chan string
+ connections map[*WebConn]bool
+ register chan *WebConn
+ unregister chan *WebConn
+ broadcast chan *model.WebSocketEvent
+ stop chan string
+ invalidateUser chan string
}
-var hub = &Hub{
- register: make(chan *WebConn),
- unregister: make(chan *WebConn),
- connections: make(map[*WebConn]bool),
- broadcast: make(chan *model.WebSocketEvent),
- stop: make(chan string),
- invalidateUser: make(chan string),
- invalidateChannel: make(chan string),
+var hubs []*Hub = make([]*Hub, 0)
+
+func NewWebHub() *Hub {
+ return &Hub{
+ register: make(chan *WebConn),
+ unregister: make(chan *WebConn),
+ connections: make(map[*WebConn]bool, model.SESSION_CACHE_SIZE),
+ broadcast: make(chan *model.WebSocketEvent, 4096),
+ stop: make(chan string),
+ invalidateUser: make(chan string),
+ }
+}
+
+func TotalWebsocketConnections() int {
+ // XXX TODO FIXME, this is racy and needs to be fixed
+ count := 0
+ for _, hub := range hubs {
+ count = count + len(hub.connections)
+ }
+
+ return count
+}
+
+func HubStart() {
+ l4g.Info(utils.T("api.web_hub.start.starting.debug"), runtime.NumCPU()*2)
+
+ // Total number of hubs is twice the number of CPUs.
+ hubs = make([]*Hub, runtime.NumCPU()*2)
+
+ for i := 0; i < len(hubs); i++ {
+ hubs[i] = NewWebHub()
+ hubs[i].Start()
+ }
+}
+
+func HubStop() {
+ l4g.Info(utils.T("api.web_hub.start.stopping.debug"))
+
+ for _, hub := range hubs {
+ hub.Stop()
+ }
+
+ hubs = make([]*Hub, 0)
+}
+
+func HubRegister(webConn *WebConn) {
+ hash := fnv.New32a()
+ hash.Write([]byte(webConn.UserId))
+ index := hash.Sum32() % uint32(len(hubs))
+ hubs[index].Register(webConn)
+}
+
+func HubUnregister(webConn *WebConn) {
+ for _, hub := range hubs {
+ hub.Unregister(webConn)
+ }
}
func Publish(message *model.WebSocketEvent) {
- hub.Broadcast(message)
+ message.DoPreComputeJson()
+ for _, hub := range hubs {
+ hub.Broadcast(message)
+ }
if einterfaces.GetClusterInterface() != nil {
einterfaces.GetClusterInterface().Publish(message)
@@ -42,11 +94,19 @@ func Publish(message *model.WebSocketEvent) {
}
func PublishSkipClusterSend(message *model.WebSocketEvent) {
- hub.Broadcast(message)
+ message.DoPreComputeJson()
+ for _, hub := range hubs {
+ hub.Broadcast(message)
+ }
}
func InvalidateCacheForUser(userId string) {
- hub.invalidateUser <- userId
+
+ Srv.Store.Channel().InvalidateAllChannelMembersForUser(userId)
+
+ for _, hub := range hubs {
+ hub.InvalidateUser(userId)
+ }
if einterfaces.GetClusterInterface() != nil {
einterfaces.GetClusterInterface().InvalidateCacheForUser(userId)
@@ -54,11 +114,17 @@ func InvalidateCacheForUser(userId string) {
}
func InvalidateCacheForChannel(channelId string) {
- hub.invalidateChannel <- channelId
- if einterfaces.GetClusterInterface() != nil {
- einterfaces.GetClusterInterface().InvalidateCacheForChannel(channelId)
- }
+ // XXX TODO FIX ME
+ // This can be removed, but the performance branch
+ // needs to be merged into master so it can be removed
+ // from the enterprise repo as well.
+
+ // hub.invalidateChannel <- channelId
+
+ // if einterfaces.GetClusterInterface() != nil {
+ // einterfaces.GetClusterInterface().InvalidateCacheForChannel(channelId)
+ // }
}
func (h *Hub) Register(webConn *WebConn) {
@@ -79,6 +145,10 @@ func (h *Hub) Broadcast(message *model.WebSocketEvent) {
}
}
+func (h *Hub) InvalidateUser(userId string) {
+ h.invalidateUser <- userId
+}
+
func (h *Hub) Stop() {
h.stop <- "all"
}
@@ -108,6 +178,7 @@ func (h *Hub) Start() {
if !found {
go SetStatusOffline(userId, false)
}
+
case userId := <-h.invalidateUser:
for webCon := range h.connections {
if webCon.UserId == userId {
@@ -115,26 +186,20 @@ func (h *Hub) Start() {
}
}
- case channelId := <-h.invalidateChannel:
- for webCon := range h.connections {
- webCon.InvalidateCacheForChannel(channelId)
- }
-
case msg := <-h.broadcast:
for webCon := range h.connections {
- if shouldSendEvent(webCon, msg) {
+ if webCon.ShouldSendEvent(msg) {
select {
case webCon.Send <- msg:
default:
+ l4g.Error(fmt.Sprintf("webhub.broadcast: cannot send, closing websocket for userId=%v", webCon.UserId))
close(webCon.Send)
delete(h.connections, webCon)
}
}
}
- case s := <-h.stop:
- l4g.Debug(utils.T("api.web_hub.start.stopping.debug"), s)
-
+ case <-h.stop:
for webCon := range h.connections {
webCon.WebSocket.Close()
}
@@ -144,28 +209,3 @@ func (h *Hub) Start() {
}
}()
}
-
-func shouldSendEvent(webCon *WebConn, msg *model.WebSocketEvent) bool {
- // If the event is destined to a specific user
- if len(msg.Broadcast.UserId) > 0 && webCon.UserId != msg.Broadcast.UserId {
- return false
- }
-
- // if the user is omitted don't send the message
- if _, ok := msg.Broadcast.OmitUsers[webCon.UserId]; ok {
- return false
- }
-
- // Only report events to users who are in the channel for the event
- if len(msg.Broadcast.ChannelId) > 0 {
- return webCon.IsMemberOfChannel(msg.Broadcast.ChannelId)
- }
-
- // Only report events to users who are in the team for the event
- if len(msg.Broadcast.TeamId) > 0 {
- return webCon.IsMemberOfTeam(msg.Broadcast.TeamId)
-
- }
-
- return true
-}
diff --git a/api/websocket.go b/api/websocket.go
index fe9fa0bf9..34d95f705 100644
--- a/api/websocket.go
+++ b/api/websocket.go
@@ -11,16 +11,20 @@ import (
"net/http"
)
+const (
+ SOCKET_MAX_MESSAGE_SIZE_KB = 8 * 1024 // 8KB
+)
+
func InitWebSocket() {
l4g.Debug(utils.T("api.web_socket.init.debug"))
BaseRoutes.Users.Handle("/websocket", ApiUserRequiredTrustRequester(connect)).Methods("GET")
- hub.Start()
+ HubStart()
}
func connect(c *Context, w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{
- ReadBufferSize: 1024,
- WriteBufferSize: 1024,
+ ReadBufferSize: SOCKET_MAX_MESSAGE_SIZE_KB,
+ WriteBufferSize: SOCKET_MAX_MESSAGE_SIZE_KB,
CheckOrigin: func(r *http.Request) bool {
return true
},
@@ -34,7 +38,7 @@ func connect(c *Context, w http.ResponseWriter, r *http.Request) {
}
wc := NewWebConn(c, ws)
- hub.Register(wc)
+ HubRegister(wc)
go wc.writePump()
wc.readPump()
}
diff --git a/api/websocket_handler.go b/api/websocket_handler.go
index 5a313fe13..95aad8fee 100644
--- a/api/websocket_handler.go
+++ b/api/websocket_handler.go
@@ -31,11 +31,17 @@ func (wh *webSocketHandler) ServeWebSocket(conn *WebConn, r *model.WebSocketRequ
if data, err = wh.handlerFunc(r); err != nil {
l4g.Error(utils.T("api.web_socket_handler.log.error"), "/api/v3/users/websocket", r.Action, r.Seq, r.Session.UserId, err.SystemMessage(utils.T), err.DetailedError)
err.DetailedError = ""
- conn.Send <- model.NewWebSocketError(r.Seq, err)
+ errResp := model.NewWebSocketError(r.Seq, err)
+ errResp.DoPreComputeJson()
+
+ conn.Send <- errResp
return
}
- conn.Send <- model.NewWebSocketResponse(model.STATUS_OK, r.Seq, data)
+ resp := model.NewWebSocketResponse(model.STATUS_OK, r.Seq, data)
+ resp.DoPreComputeJson()
+
+ conn.Send <- resp
}
func NewInvalidWebSocketParamError(action string, name string) *model.AppError {
diff --git a/api/websocket_router.go b/api/websocket_router.go
index cd3ff4d1a..34b576464 100644
--- a/api/websocket_router.go
+++ b/api/websocket_router.go
@@ -54,6 +54,7 @@ func (wr *WebSocketRouter) ReturnWebSocketError(conn *WebConn, r *model.WebSocke
err.DetailedError = ""
errorResp := model.NewWebSocketError(r.Seq, err)
+ errorResp.DoPreComputeJson()
conn.Send <- errorResp
}
diff --git a/api/websocket_test.go b/api/websocket_test.go
index dcbc8e0f4..b7ca4b691 100644
--- a/api/websocket_test.go
+++ b/api/websocket_test.go
@@ -82,7 +82,8 @@ func TestWebSocketEvent(t *testing.T) {
omitUser["somerandomid"] = true
evt1 := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", th.BasicChannel.Id, "", omitUser)
evt1.Add("user_id", "somerandomid")
- go Publish(evt1)
+ Publish(evt1)
+
time.Sleep(300 * time.Millisecond)
stop := make(chan bool)
diff --git a/config/config.json b/config/config.json
index 330ae78a2..195c0255e 100644
--- a/config/config.json
+++ b/config/config.json
@@ -64,7 +64,7 @@
},
"LogSettings": {
"EnableConsole": true,
- "ConsoleLevel": "DEBUG",
+ "ConsoleLevel": "INFO",
"EnableFile": true,
"FileLevel": "INFO",
"FileFormat": "",
@@ -240,4 +240,4 @@
"TurnUsername": "",
"TurnSharedKey": ""
}
-} \ No newline at end of file
+}
diff --git a/i18n/en.json b/i18n/en.json
index 4e213d446..5d27af7f1 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -2404,8 +2404,12 @@
"translation": "Bad verify email link."
},
{
+ "id": "api.web_hub.start.starting.debug",
+ "translation": "Starting %v websocket hubs"
+ },
+ {
"id": "api.web_hub.start.stopping.debug",
- "translation": "stopping %v connections"
+ "translation": "stopping websocket hub connections"
},
{
"id": "api.web_socket.connect.error",
@@ -4628,6 +4632,10 @@
"translation": "We couldn't get the team member"
},
{
+ "id": "store.sql_team.get_members_by_ids.app_error",
+ "translation": "We couldn't get the team members"
+ },
+ {
"id": "store.sql_team.get_member.missing.app_error",
"translation": "No team member found for that user id and team id"
},
@@ -4636,6 +4644,10 @@
"translation": "We couldn't get the team members"
},
{
+ "id": "store.sql_team.get_member_count.app_error",
+ "translation": "We couldn't count the team members"
+ },
+ {
"id": "store.sql_team.get_teams_for_email.app_error",
"translation": "We encountered a problem when looking up teams"
},
@@ -4724,6 +4736,10 @@
"translation": "We encountered an error while finding user profiles"
},
{
+ "id": "store.sql_user.get_recently_active_users.app_error",
+ "translation": "We encountered an error while finding the recently active users"
+ },
+ {
"id": "store.sql_user.get_sysadmin_profiles.app_error",
"translation": "We encountered an error while finding user profiles"
},
diff --git a/mattermost.go b/mattermost.go
index 8d4880504..7b9f6c0c1 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -14,6 +14,7 @@ import (
"os/exec"
"os/signal"
"runtime"
+ "runtime/pprof"
"strconv"
"strings"
"syscall"
@@ -83,6 +84,10 @@ var flagChannelHeader string
var flagChannelPurpose string
var flagUserSetInactive bool
var flagImportArchive string
+var flagCpuProfile bool
+var flagMemProfile bool
+var flagBlockProfile bool
+var flagHttpProfiler bool
func doLoadConfig(filename string) (err string) {
defer func() {
@@ -122,7 +127,26 @@ func main() {
cmdUpdateDb30()
- api.NewServer()
+ if flagCpuProfile {
+ f, err := os.Create(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".cpu.prof")
+ if err != nil {
+ l4g.Error("Error creating cpu profile log: " + err.Error())
+ }
+
+ l4g.Info("CPU Profiler is logging to " + utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".cpu.prof")
+ pprof.StartCPUProfile(f)
+ }
+
+ if flagBlockProfile {
+ l4g.Info("Block Profiler is logging to " + utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".blk.prof")
+ runtime.SetBlockProfileRate(1)
+ }
+
+ if flagMemProfile {
+ l4g.Info("Memory Profiler is logging to " + utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".mem.prof")
+ }
+
+ api.NewServer(flagHttpProfiler)
api.InitApi()
web.InitWeb()
@@ -169,6 +193,37 @@ func main() {
}
api.StopServer()
+
+ if flagCpuProfile {
+ l4g.Info("Closing CPU Profiler")
+ pprof.StopCPUProfile()
+ }
+
+ if flagBlockProfile {
+ f, err := os.Create(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".blk.prof")
+ if err != nil {
+ l4g.Error("Error creating block profile log: " + err.Error())
+ }
+
+ l4g.Info("Writing Block Profiler to: " + utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".blk.prof")
+ pprof.Lookup("block").WriteTo(f, 0)
+ f.Close()
+ runtime.SetBlockProfileRate(0)
+ }
+
+ if flagMemProfile {
+ f, err := os.Create(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".mem.prof")
+ if err != nil {
+ l4g.Error("Error creating memory profile file: " + err.Error())
+ }
+
+ l4g.Info("Writing Memory Profiler to: " + utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".mem.prof")
+ runtime.GC()
+ if err := pprof.WriteHeapProfile(f); err != nil {
+ l4g.Error("Error creating memory profile: " + err.Error())
+ }
+ f.Close()
+ }
}
}
@@ -380,6 +435,10 @@ func parseCmds() {
flag.BoolVar(&flagCmdActivateUser, "activate_user", false, "")
flag.BoolVar(&flagCmdSlackImport, "slack_import", false, "")
flag.BoolVar(&flagUserSetInactive, "inactive", false, "")
+ flag.BoolVar(&flagCpuProfile, "cpuprofile", false, "")
+ flag.BoolVar(&flagMemProfile, "memprofile", false, "")
+ flag.BoolVar(&flagBlockProfile, "blkprofile", false, "")
+ flag.BoolVar(&flagHttpProfiler, "httpprofiler", false, "")
flag.Parse()
diff --git a/model/autocomplete.go b/model/autocomplete.go
new file mode 100644
index 000000000..b7449a792
--- /dev/null
+++ b/model/autocomplete.go
@@ -0,0 +1,58 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type UserAutocompleteInChannel struct {
+ InChannel []*User `json:"in_channel"`
+ OutOfChannel []*User `json:"out_of_channel"`
+}
+
+type UserAutocompleteInTeam struct {
+ InTeam []*User `json:"in_team"`
+}
+
+func (o *UserAutocompleteInChannel) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func UserAutocompleteInChannelFromJson(data io.Reader) *UserAutocompleteInChannel {
+ decoder := json.NewDecoder(data)
+ var o UserAutocompleteInChannel
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
+
+func (o *UserAutocompleteInTeam) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func UserAutocompleteInTeamFromJson(data io.Reader) *UserAutocompleteInTeam {
+ decoder := json.NewDecoder(data)
+ var o UserAutocompleteInTeam
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/channel.go b/model/channel.go
index 3da9f4feb..7dee079c5 100644
--- a/model/channel.go
+++ b/model/channel.go
@@ -57,8 +57,8 @@ func (o *Channel) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
-func (o *Channel) ExtraEtag(memberLimit int) string {
- return Etag(o.Id, o.ExtraUpdateAt, memberLimit)
+func (o *Channel) StatsEtag() string {
+ return Etag(o.Id, o.ExtraUpdateAt)
}
func (o *Channel) IsValid() *AppError {
diff --git a/model/channel_extra.go b/model/channel_extra.go
deleted file mode 100644
index 55da588af..000000000
--- a/model/channel_extra.go
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package model
-
-import (
- "encoding/json"
- "io"
-)
-
-type ExtraMember struct {
- Id string `json:"id"`
- Nickname string `json:"nickname"`
- Email string `json:"email"`
- Roles string `json:"roles"`
- Username string `json:"username"`
-}
-
-func (o *ExtraMember) Sanitize(options map[string]bool) {
- if len(options) == 0 || !options["email"] {
- o.Email = ""
- }
-}
-
-type ChannelExtra struct {
- Id string `json:"id"`
- Members []ExtraMember `json:"members"`
- MemberCount int64 `json:"member_count"`
-}
-
-func (o *ChannelExtra) ToJson() string {
- b, err := json.Marshal(o)
- if err != nil {
- return ""
- } else {
- return string(b)
- }
-}
-
-func ChannelExtraFromJson(data io.Reader) *ChannelExtra {
- decoder := json.NewDecoder(data)
- var o ChannelExtra
- err := decoder.Decode(&o)
- if err == nil {
- return &o
- } else {
- return nil
- }
-}
diff --git a/model/channel_stats.go b/model/channel_stats.go
new file mode 100644
index 000000000..079769eb0
--- /dev/null
+++ b/model/channel_stats.go
@@ -0,0 +1,34 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type ChannelStats struct {
+ ChannelId string `json:"channel_id"`
+ MemberCount int64 `json:"member_count"`
+}
+
+func (o *ChannelStats) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func ChannelStatsFromJson(data io.Reader) *ChannelStats {
+ decoder := json.NewDecoder(data)
+ var o ChannelStats
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/client.go b/model/client.go
index f9a56b86e..f5aeea4db 100644
--- a/model/client.go
+++ b/model/client.go
@@ -131,6 +131,7 @@ func (c *Client) GetFileRoute(fileId string) string {
func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data))
rq.Header.Set("Content-Type", contentType)
+ rq.Close = true
if rp, err := c.HttpClient.Do(rq); err != nil {
return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
@@ -144,6 +145,7 @@ func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppErro
func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("POST", c.ApiUrl+url, strings.NewReader(data))
+ rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
@@ -161,6 +163,7 @@ func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError)
func (c *Client) DoApiGet(url string, data string, etag string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("GET", c.ApiUrl+url, strings.NewReader(data))
+ rq.Close = true
if len(etag) > 0 {
rq.Header.Set(HEADER_ETAG_CLIENT, etag)
@@ -508,10 +511,9 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) {
}
}
-// GetProfilesForDirectMessageList returns a map of users for a team that can be direct
-// messaged, using user id as the key. Must be authenticated.
-func (c *Client) GetProfilesForDirectMessageList(teamId string) (*Result, *AppError) {
- if r, err := c.DoApiGet("/users/profiles_for_dm_list/"+teamId, "", ""); err != nil {
+// GetProfiles returns a map of users using user id as the key. Must be authenticated.
+func (c *Client) GetProfiles(offset int, limit int, etag string) (*Result, *AppError) {
+ if r, err := c.DoApiGet(fmt.Sprintf("/users/%v/%v", offset, limit), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
@@ -520,10 +522,10 @@ func (c *Client) GetProfilesForDirectMessageList(teamId string) (*Result, *AppEr
}
}
-// GetProfiles returns a map of users for a team using user id as the key. Must
+// GetProfilesInTeam returns a map of users for a team using user id as the key. Must
// be authenticated.
-func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
- if r, err := c.DoApiGet("/users/profiles/"+teamId, "", etag); err != nil {
+func (c *Client) GetProfilesInTeam(teamId string, offset int, limit int, etag string) (*Result, *AppError) {
+ if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/users/%v/%v", teamId, offset, limit), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
@@ -532,10 +534,22 @@ func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
}
}
-// GetDirectProfiles gets a map of users that are currently shown in the sidebar,
-// using user id as the key. Must be authenticated.
-func (c *Client) GetDirectProfiles(etag string) (*Result, *AppError) {
- if r, err := c.DoApiGet("/users/direct_profiles", "", etag); err != nil {
+// GetProfilesInChannel returns a map of users for a channel using user id as the key. Must
+// be authenticated.
+func (c *Client) GetProfilesInChannel(channelId string, offset int, limit int, etag string) (*Result, *AppError) {
+ if r, err := c.DoApiGet(fmt.Sprintf(c.GetChannelRoute(channelId)+"/users/%v/%v", offset, limit), "", etag); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil
+ }
+}
+
+// GetProfilesNotInChannel returns a map of users not in a channel but on the team using user id as the key. Must
+// be authenticated.
+func (c *Client) GetProfilesNotInChannel(channelId string, offset int, limit int, etag string) (*Result, *AppError) {
+ if r, err := c.DoApiGet(fmt.Sprintf(c.GetChannelRoute(channelId)+"/users/not_in_channel/%v/%v", offset, limit), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
@@ -544,6 +558,60 @@ func (c *Client) GetDirectProfiles(etag string) (*Result, *AppError) {
}
}
+// GetProfilesByIds returns a map of users based on the user ids provided. Must
+// be authenticated.
+func (c *Client) GetProfilesByIds(userIds []string) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/users/ids", ArrayToJson(userIds)); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil
+ }
+}
+
+// SearchUsers returns a list of users that have a username matching or similar to the search term. Must
+// be authenticated.
+func (c *Client) SearchUsers(term string, teamId string, options map[string]string) (*Result, *AppError) {
+ options["term"] = term
+ options["team_id"] = teamId
+ if r, err := c.DoApiPost("/users/search", MapToJson(options)); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserListFromJson(r.Body)}, nil
+ }
+}
+
+// AutocompleteUsersInChannel returns two lists for autocompletion of users in a channel. The first list "in_channel",
+// specifies users in the channel. The second list "out_of_channel" specifies users outside of the
+// channel. Term, the string to search against, is required, channel id is also required. Must be authenticated.
+func (c *Client) AutocompleteUsersInChannel(term string, channelId string) (*Result, *AppError) {
+ url := fmt.Sprintf("%s/users/autocomplete?term=%s", c.GetChannelRoute(channelId), url.QueryEscape(term))
+ if r, err := c.DoApiGet(url, "", ""); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserAutocompleteInChannelFromJson(r.Body)}, nil
+ }
+}
+
+// AutocompleteUsersInTeam returns a list for autocompletion of users in a team. The list "in_team" specifies
+// the users in the team that match the provided term, matching against username, full name and
+// nickname. Must be authenticated.
+func (c *Client) AutocompleteUsersInTeam(term string) (*Result, *AppError) {
+ url := fmt.Sprintf("%s/users/autocomplete?term=%s", c.GetTeamRoute(), url.QueryEscape(term))
+ if r, err := c.DoApiGet(url, "", ""); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserAutocompleteInTeamFromJson(r.Body)}, nil
+ }
+}
+
// LoginById authenticates a user by user id and password.
func (c *Client) LoginById(id string, password string) (*Result, *AppError) {
m := make(map[string]string)
@@ -942,6 +1010,7 @@ func (c *Client) SaveComplianceReport(job *Compliance) (*Result, *AppError) {
func (c *Client) DownloadComplianceReport(id string) (*Result, *AppError) {
var rq *http.Request
rq, _ = http.NewRequest("GET", c.ApiUrl+"/admin/download_compliance_report/"+id, nil)
+ rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
@@ -1174,13 +1243,23 @@ func (c *Client) UpdateLastViewedAt(channelId string, active bool) (*Result, *Ap
}
}
-func (c *Client) GetChannelExtraInfo(id string, memberLimit int, etag string) (*Result, *AppError) {
- if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/extra_info/"+strconv.FormatInt(int64(memberLimit), 10), "", etag); err != nil {
+func (c *Client) GetChannelStats(id string, etag string) (*Result, *AppError) {
+ if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/stats", "", etag); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ChannelStatsFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetChannelMember(channelId string, userId string) (*Result, *AppError) {
+ if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/members/"+userId, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
- r.Header.Get(HEADER_ETAG_SERVER), ChannelExtraFromJson(r.Body)}, nil
+ r.Header.Get(HEADER_ETAG_SERVER), ChannelMemberFromJson(r.Body)}, nil
}
}
@@ -1325,6 +1404,7 @@ func (c *Client) UploadPostAttachment(data []byte, channelId string, filename st
func (c *Client) uploadFile(url string, data []byte, contentType string) (*Result, *AppError) {
rq, _ := http.NewRequest("POST", url, bytes.NewReader(data))
rq.Header.Set("Content-Type", contentType)
+ rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
@@ -1525,6 +1605,18 @@ func (c *Client) GetStatuses() (*Result, *AppError) {
}
}
+// GetStatusesByIds returns a map of string statuses using user id as the key,
+// based on the provided user ids
+func (c *Client) GetStatusesByIds(userIds []string) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/users/status/ids", ArrayToJson(userIds)); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
// SetActiveChannel sets the the channel id the user is currently viewing.
// The channelId key is required but the value can be blank. Returns standard
// response.
@@ -1550,8 +1642,46 @@ func (c *Client) GetMyTeam(etag string) (*Result, *AppError) {
}
}
-func (c *Client) GetTeamMembers(teamId string) (*Result, *AppError) {
- if r, err := c.DoApiGet("/teams/members/"+teamId, "", ""); err != nil {
+// GetTeamMembers will return a page of team member objects as an array paged based on the
+// team id, offset and limit provided. Must be authenticated.
+func (c *Client) GetTeamMembers(teamId string, offset int, limit int) (*Result, *AppError) {
+ if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/members/%v/%v", teamId, offset, limit), "", ""); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), TeamMembersFromJson(r.Body)}, nil
+ }
+}
+
+// GetTeamMember will return a team member object based on the team id and user id provided.
+// Must be authenticated.
+func (c *Client) GetTeamMember(teamId string, userId string) (*Result, *AppError) {
+ if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/members/%v", teamId, userId), "", ""); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), TeamMemberFromJson(r.Body)}, nil
+ }
+}
+
+// GetTeamStats will return a team stats object containing the number of users on the team
+// based on the team id provided. Must be authenticated.
+func (c *Client) GetTeamStats(teamId string) (*Result, *AppError) {
+ if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/stats", teamId), "", ""); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), TeamStatsFromJson(r.Body)}, nil
+ }
+}
+
+// GetTeamMembersByIds will return team member objects as an array based on the
+// team id and a list of user ids provided. Must be authenticated.
+func (c *Client) GetTeamMembersByIds(teamId string, userIds []string) (*Result, *AppError) {
+ if r, err := c.DoApiPost(fmt.Sprintf("/teams/%v/members/ids", teamId), ArrayToJson(userIds)); err != nil {
return nil, err
} else {
defer closeBody(r)
@@ -1866,6 +1996,7 @@ func (c *Client) CreateEmoji(emoji *Emoji, image []byte, filename string) (*Emoj
rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetEmojiRoute()+"/create", body)
rq.Header.Set("Content-Type", writer.FormDataContentType())
+ rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
@@ -1908,6 +2039,7 @@ func (c *Client) UploadCertificateFile(data []byte, contentType string) *AppErro
url := c.ApiUrl + "/admin/add_certificate"
rq, _ := http.NewRequest("POST", url, bytes.NewReader(data))
rq.Header.Set("Content-Type", contentType)
+ rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
diff --git a/model/initial_load.go b/model/initial_load.go
index d7587e6d4..afb0a2762 100644
--- a/model/initial_load.go
+++ b/model/initial_load.go
@@ -9,14 +9,13 @@ import (
)
type InitialLoad struct {
- User *User `json:"user"`
- TeamMembers []*TeamMember `json:"team_members"`
- Teams []*Team `json:"teams"`
- DirectProfiles map[string]*User `json:"direct_profiles"`
- Preferences Preferences `json:"preferences"`
- ClientCfg map[string]string `json:"client_cfg"`
- LicenseCfg map[string]string `json:"license_cfg"`
- NoAccounts bool `json:"no_accounts"`
+ User *User `json:"user"`
+ TeamMembers []*TeamMember `json:"team_members"`
+ Teams []*Team `json:"teams"`
+ Preferences Preferences `json:"preferences"`
+ ClientCfg map[string]string `json:"client_cfg"`
+ LicenseCfg map[string]string `json:"license_cfg"`
+ NoAccounts bool `json:"no_accounts"`
}
func (me *InitialLoad) ToJson() string {
diff --git a/model/session.go b/model/session.go
index c3171ed7c..a6a753e7f 100644
--- a/model/session.go
+++ b/model/session.go
@@ -11,7 +11,7 @@ import (
const (
SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
- SESSION_CACHE_SIZE = 10000
+ SESSION_CACHE_SIZE = 25000
SESSION_PROP_PLATFORM = "platform"
SESSION_PROP_OS = "os"
SESSION_PROP_BROWSER = "browser"
diff --git a/model/status.go b/model/status.go
index f4ad8e775..324866427 100644
--- a/model/status.go
+++ b/model/status.go
@@ -12,8 +12,9 @@ const (
STATUS_OFFLINE = "offline"
STATUS_AWAY = "away"
STATUS_ONLINE = "online"
- STATUS_CACHE_SIZE = 10000
- STATUS_CHANNEL_TIMEOUT = 20000 // 20 seconds
+ STATUS_CACHE_SIZE = 25000
+ STATUS_CHANNEL_TIMEOUT = 20000 // 20 seconds
+ STATUS_MIN_UPDATE_TIME = 120000 // 2 minutes
)
type Status struct {
diff --git a/model/team_stats.go b/model/team_stats.go
new file mode 100644
index 000000000..8634c3d1b
--- /dev/null
+++ b/model/team_stats.go
@@ -0,0 +1,34 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type TeamStats struct {
+ TeamId string `json:"team_id"`
+ MemberCount int64 `json:"member_count"`
+}
+
+func (o *TeamStats) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func TeamStatsFromJson(data io.Reader) *TeamStats {
+ decoder := json.NewDecoder(data)
+ var o TeamStats
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/user.go b/model/user.go
index 1b51171ad..e713444ba 100644
--- a/model/user.go
+++ b/model/user.go
@@ -413,6 +413,26 @@ func UserMapFromJson(data io.Reader) map[string]*User {
}
}
+func UserListToJson(u []*User) string {
+ b, err := json.Marshal(u)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func UserListFromJson(data io.Reader) []*User {
+ decoder := json.NewDecoder(data)
+ var users []*User
+ err := decoder.Decode(&users)
+ if err == nil {
+ return users
+ } else {
+ return nil
+ }
+}
+
// HashPassword generates a hash using the bcrypt.GenerateFromPassword
func HashPassword(password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
diff --git a/model/utils.go b/model/utils.go
index a9c441fee..4ebd23939 100644
--- a/model/utils.go
+++ b/model/utils.go
@@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "io/ioutil"
"net/mail"
"net/url"
"regexp"
@@ -74,13 +75,21 @@ func (er *AppError) ToJson() string {
// AppErrorFromJson will decode the input and return an AppError
func AppErrorFromJson(data io.Reader) *AppError {
- decoder := json.NewDecoder(data)
+ str := ""
+ bytes, rerr := ioutil.ReadAll(data)
+ if rerr != nil {
+ str = rerr.Error()
+ } else {
+ str = string(bytes)
+ }
+
+ decoder := json.NewDecoder(strings.NewReader(str))
var er AppError
err := decoder.Decode(&er)
if err == nil {
return &er
} else {
- return NewLocAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, err.Error())
+ return NewLocAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, "body: "+str)
}
}
@@ -166,6 +175,23 @@ func ArrayFromJson(data io.Reader) []string {
}
}
+func ArrayFromInterface(data interface{}) []string {
+ stringArray := []string{}
+
+ dataArray, ok := data.([]interface{})
+ if !ok {
+ return stringArray
+ }
+
+ for _, v := range dataArray {
+ if str, ok := v.(string); ok {
+ stringArray = append(stringArray, str)
+ }
+ }
+
+ return stringArray
+}
+
func StringInterfaceToJson(objmap map[string]interface{}) string {
if b, err := json.Marshal(objmap); err != nil {
return ""
diff --git a/model/utils_test.go b/model/utils_test.go
index 5d9289a9a..dbe1e59be 100644
--- a/model/utils_test.go
+++ b/model/utils_test.go
@@ -37,6 +37,13 @@ func TestAppError(t *testing.T) {
err.Error()
}
+func TestAppErrorJunk(t *testing.T) {
+ rerr := AppErrorFromJson(strings.NewReader("<html><body>This is a broken test</body></html>"))
+ if "body: <html><body>This is a broken test</body></html>" != rerr.DetailedError {
+ t.Fatal()
+ }
+}
+
func TestMapJson(t *testing.T) {
m := make(map[string]string)
diff --git a/model/websocket_client.go b/model/websocket_client.go
index a048bd855..a4983e385 100644
--- a/model/websocket_client.go
+++ b/model/websocket_client.go
@@ -17,6 +17,7 @@ type WebSocketClient struct {
Sequence int64 // The ever-incrementing sequence attached to each WebSocket action
EventChannel chan *WebSocketEvent
ResponseChannel chan *WebSocketResponse
+ ListenError *AppError
}
// NewWebSocketClient constructs a new WebSocket client with convienence
@@ -37,6 +38,7 @@ func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) {
1,
make(chan *WebSocketEvent, 100),
make(chan *WebSocketResponse, 100),
+ nil,
}, nil
}
@@ -59,10 +61,20 @@ func (wsc *WebSocketClient) Close() {
func (wsc *WebSocketClient) Listen() {
go func() {
+ defer func() {
+ wsc.Conn.Close()
+ close(wsc.EventChannel)
+ close(wsc.ResponseChannel)
+ }()
+
for {
var rawMsg json.RawMessage
var err error
if _, rawMsg, err = wsc.Conn.ReadMessage(); err != nil {
+ if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
+ wsc.ListenError = NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
+ }
+
return
}
@@ -107,3 +119,12 @@ func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
func (wsc *WebSocketClient) GetStatuses() {
wsc.SendMessage("get_statuses", nil)
}
+
+// GetStatusesByIds will fetch certain user statuses based on ids and return
+// a map of string statuses using user id as the key
+func (wsc *WebSocketClient) GetStatusesByIds(userIds []string) {
+ data := map[string]interface{}{
+ "user_ids": userIds,
+ }
+ wsc.SendMessage("get_statuses_by_ids", data)
+}
diff --git a/model/websocket_message.go b/model/websocket_message.go
index 9bce1e825..c7ab04857 100644
--- a/model/websocket_message.go
+++ b/model/websocket_message.go
@@ -31,6 +31,8 @@ const (
type WebSocketMessage interface {
ToJson() string
IsValid() bool
+ DoPreComputeJson()
+ GetPreComputeJson() []byte
}
type WebsocketBroadcast struct {
@@ -41,9 +43,10 @@ type WebsocketBroadcast struct {
}
type WebSocketEvent struct {
- Event string `json:"event"`
- Data map[string]interface{} `json:"data"`
- Broadcast *WebsocketBroadcast `json:"broadcast"`
+ Event string `json:"event"`
+ Data map[string]interface{} `json:"data"`
+ Broadcast *WebsocketBroadcast `json:"broadcast"`
+ PreComputeJson []byte `json:"-"`
}
func (m *WebSocketEvent) Add(key string, value interface{}) {
@@ -59,6 +62,19 @@ func (o *WebSocketEvent) IsValid() bool {
return o.Event != ""
}
+func (o *WebSocketEvent) DoPreComputeJson() {
+ b, err := json.Marshal(o)
+ if err != nil {
+ o.PreComputeJson = []byte("")
+ } else {
+ o.PreComputeJson = b
+ }
+}
+
+func (o *WebSocketEvent) GetPreComputeJson() []byte {
+ return o.PreComputeJson
+}
+
func (o *WebSocketEvent) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
@@ -80,10 +96,11 @@ func WebSocketEventFromJson(data io.Reader) *WebSocketEvent {
}
type WebSocketResponse struct {
- Status string `json:"status"`
- SeqReply int64 `json:"seq_reply,omitempty"`
- Data map[string]interface{} `json:"data,omitempty"`
- Error *AppError `json:"error,omitempty"`
+ Status string `json:"status"`
+ SeqReply int64 `json:"seq_reply,omitempty"`
+ Data map[string]interface{} `json:"data,omitempty"`
+ Error *AppError `json:"error,omitempty"`
+ PreComputeJson []byte `json:"-"`
}
func (m *WebSocketResponse) Add(key string, value interface{}) {
@@ -111,6 +128,19 @@ func (o *WebSocketResponse) ToJson() string {
}
}
+func (o *WebSocketResponse) DoPreComputeJson() {
+ b, err := json.Marshal(o)
+ if err != nil {
+ o.PreComputeJson = []byte("")
+ } else {
+ o.PreComputeJson = b
+ }
+}
+
+func (o *WebSocketResponse) GetPreComputeJson() []byte {
+ return o.PreComputeJson
+}
+
func WebSocketResponseFromJson(data io.Reader) *WebSocketResponse {
decoder := json.NewDecoder(data)
var o WebSocketResponse
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index eb150a63c..a860fea73 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -6,21 +6,26 @@ package store
import (
"database/sql"
+ l4g "github.com/alecthomas/log4go"
"github.com/go-gorp/gorp"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
const (
- MISSING_CHANNEL_ERROR = "store.sql_channel.get_by_name.missing.app_error"
- MISSING_CHANNEL_MEMBER_ERROR = "store.sql_channel.get_member.missing.app_error"
- CHANNEL_EXISTS_ERROR = "store.sql_channel.save_channel.exists.app_error"
+ MISSING_CHANNEL_ERROR = "store.sql_channel.get_by_name.missing.app_error"
+ MISSING_CHANNEL_MEMBER_ERROR = "store.sql_channel.get_member.missing.app_error"
+ CHANNEL_EXISTS_ERROR = "store.sql_channel.save_channel.exists.app_error"
+ ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SIZE = model.SESSION_CACHE_SIZE
+ ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SEC = 900 // 15 mins
)
type SqlChannelStore struct {
*SqlStore
}
+var allChannelMembersForUserCache *utils.Cache = utils.NewLru(ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SIZE)
+
func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
s := &SqlChannelStore{sqlStore}
@@ -517,6 +522,8 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel {
}
}
+ s.InvalidateAllChannelMembersForUser(member.UserId)
+
storeChannel <- result
close(storeChannel)
}()
@@ -619,6 +626,33 @@ func (s SqlChannelStore) GetMember(channelId string, userId string) StoreChannel
return storeChannel
}
+func (us SqlChannelStore) InvalidateAllChannelMembersForUser(userId string) {
+ allChannelMembersForUserCache.Remove(userId)
+}
+
+func (us SqlChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool {
+ if cacheItem, ok := allChannelMembersForUserCache.Get(userId); ok {
+ ids := cacheItem.(map[string]string)
+ if _, ok := ids[channelId]; ok {
+ return true
+ } else {
+ return false
+ }
+ }
+
+ if result := <-us.GetAllChannelMembersForUser(userId, true); result.Err != nil {
+ l4g.Error("SqlChannelStore.IsUserInChannelUseCache: " + result.Err.Error())
+ return false
+ } else {
+ ids := result.Data.(map[string]string)
+ if _, ok := ids[channelId]; ok {
+ return true
+ } else {
+ return false
+ }
+ }
+}
+
func (s SqlChannelStore) GetMemberForPost(postId string, userId string) StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -649,26 +683,43 @@ func (s SqlChannelStore) GetMemberForPost(postId string, userId string) StoreCha
return storeChannel
}
-func (s SqlChannelStore) GetMemberCount(channelId string) StoreChannel {
+type allChannelMember struct {
+ ChannelId string
+ Roles string
+}
+
+func (s SqlChannelStore) GetAllChannelMembersForUser(userId string, allowFromCache bool) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
- count, err := s.GetReplica().SelectInt(`
- SELECT
- count(*)
- FROM
- ChannelMembers,
- Users
- WHERE
- ChannelMembers.UserId = Users.Id
- AND ChannelMembers.ChannelId = :ChannelId
- AND Users.DeleteAt = 0`, map[string]interface{}{"ChannelId": channelId})
+ if allowFromCache {
+ if cacheItem, ok := allChannelMembersForUserCache.Get(userId); ok {
+ result.Data = cacheItem.(map[string]string)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+ }
+
+ var data []allChannelMember
+ _, err := s.GetReplica().Select(&data, "SELECT ChannelId, Roles FROM Channels, ChannelMembers WHERE Channels.Id = ChannelMembers.ChannelId AND ChannelMembers.UserId = :UserId AND Channels.DeleteAt = 0", map[string]interface{}{"UserId": userId})
+
if err != nil {
- result.Err = model.NewLocAppError("SqlChannelStore.GetMemberCount", "store.sql_channel.get_member_count.app_error", nil, "channel_id="+channelId+", "+err.Error())
+ result.Err = model.NewLocAppError("SqlChannelStore.GetAllChannelMembersForUser", "store.sql_channel.get_channels.get.app_error", nil, "userId="+userId+", err="+err.Error())
} else {
- result.Data = count
+
+ ids := make(map[string]string)
+ for i := range data {
+ ids[data[i].ChannelId] = data[i].Roles
+ }
+
+ result.Data = ids
+
+ if allowFromCache {
+ allChannelMembersForUserCache.AddWithExpiresInSecs(userId, ids, ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SEC)
+ }
}
storeChannel <- result
@@ -678,55 +729,26 @@ func (s SqlChannelStore) GetMemberCount(channelId string) StoreChannel {
return storeChannel
}
-func (s SqlChannelStore) GetExtraMembers(channelId string, limit int) StoreChannel {
+func (s SqlChannelStore) GetMemberCount(channelId string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
- var members []model.ExtraMember
- var err error
-
- if limit != -1 {
- _, err = s.GetReplica().Select(&members, `
- SELECT
- Id,
- Nickname,
- Email,
- ChannelMembers.Roles,
- Username
- FROM
- ChannelMembers,
- Users
- WHERE
- ChannelMembers.UserId = Users.Id
- AND Users.DeleteAt = 0
- AND ChannelId = :ChannelId
- LIMIT :Limit`, map[string]interface{}{"ChannelId": channelId, "Limit": limit})
- } else {
- _, err = s.GetReplica().Select(&members, `
+ count, err := s.GetReplica().SelectInt(`
SELECT
- Id,
- Nickname,
- Email,
- ChannelMembers.Roles,
- Username
+ count(*)
FROM
ChannelMembers,
Users
WHERE
ChannelMembers.UserId = Users.Id
- AND Users.DeleteAt = 0
- AND ChannelId = :ChannelId`, map[string]interface{}{"ChannelId": channelId})
- }
-
+ AND ChannelMembers.ChannelId = :ChannelId
+ AND Users.DeleteAt = 0`, map[string]interface{}{"ChannelId": channelId})
if err != nil {
- result.Err = model.NewLocAppError("SqlChannelStore.GetExtraMembers", "store.sql_channel.get_extra_members.app_error", nil, "channel_id="+channelId+", "+err.Error())
+ result.Err = model.NewLocAppError("SqlChannelStore.GetMemberCount", "store.sql_channel.get_member_count.app_error", nil, "channel_id="+channelId+", "+err.Error())
} else {
- for i := range members {
- members[i].Sanitize(utils.Cfg.GetSanitizeOptions())
- }
- result.Data = members
+ result.Data = count
}
storeChannel <- result
diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go
index 19db3d003..d80d54d52 100644
--- a/store/sql_channel_store_test.go
+++ b/store/sql_channel_store_test.go
@@ -408,11 +408,6 @@ func TestChannelMemberStore(t *testing.T) {
t.Fatal("should have go member")
}
- extraMembers := (<-store.Channel().GetExtraMembers(o1.ChannelId, 20)).Data.([]model.ExtraMember)
- if len(extraMembers) != 1 {
- t.Fatal("should have 1 extra members")
- }
-
if err := (<-store.Channel().SaveMember(&o1)).Err; err == nil {
t.Fatal("Should have been a duplicate")
}
@@ -422,18 +417,6 @@ func TestChannelMemberStore(t *testing.T) {
if t4 != t3 {
t.Fatal("Should not update time upon failure")
}
-
- // rejoin the channel and make sure that an inactive user isn't returned by GetExtraMambers
- Must(store.Channel().SaveMember(&o2))
-
- u2.DeleteAt = 1000
- Must(store.User().Update(&u2, true))
-
- if result := <-store.Channel().GetExtraMembers(o1.ChannelId, 20); result.Err != nil {
- t.Fatal(result.Err)
- } else if extraMembers := result.Data.([]model.ExtraMember); len(extraMembers) != 1 {
- t.Fatal("should have 1 extra members")
- }
}
func TestChannelDeleteMemberStore(t *testing.T) {
@@ -534,6 +517,42 @@ func TestChannelStoreGetChannels(t *testing.T) {
if list.Channels[0].Id != o1.Id {
t.Fatal("missing channel")
}
+
+ acresult := <-store.Channel().GetAllChannelMembersForUser(m1.UserId, false)
+ ids := acresult.Data.(map[string]string)
+ if _, ok := ids[o1.Id]; !ok {
+ t.Fatal("missing channel")
+ }
+
+ acresult2 := <-store.Channel().GetAllChannelMembersForUser(m1.UserId, true)
+ ids2 := acresult2.Data.(map[string]string)
+ if _, ok := ids2[o1.Id]; !ok {
+ t.Fatal("missing channel")
+ }
+
+ acresult3 := <-store.Channel().GetAllChannelMembersForUser(m1.UserId, true)
+ ids3 := acresult3.Data.(map[string]string)
+ if _, ok := ids3[o1.Id]; !ok {
+ t.Fatal("missing channel")
+ }
+
+ if !store.Channel().IsUserInChannelUseCache(m1.UserId, o1.Id) {
+ t.Fatal("missing channel")
+ }
+
+ if store.Channel().IsUserInChannelUseCache(m1.UserId, o2.Id) {
+ t.Fatal("missing channel")
+ }
+
+ if store.Channel().IsUserInChannelUseCache(m1.UserId, "blahblah") {
+ t.Fatal("missing channel")
+ }
+
+ if store.Channel().IsUserInChannelUseCache("blahblah", "blahblah") {
+ t.Fatal("missing channel")
+ }
+
+ store.Channel().InvalidateAllChannelMembersForUser(m1.UserId)
}
func TestChannelStoreGetMoreChannels(t *testing.T) {
@@ -974,22 +993,10 @@ func TestUpdateExtrasByUser(t *testing.T) {
t.Fatal("failed to update extras by user: %v", result.Err)
}
- if result := <-store.Channel().GetExtraMembers(c1.Id, -1); result.Err != nil {
- t.Fatal("failed to get extras: %v", result.Err)
- } else if len(result.Data.([]model.ExtraMember)) != 0 {
- t.Fatal("got incorrect member count %v", len(result.Data.([]model.ExtraMember)))
- }
-
u1.DeleteAt = 0
Must(store.User().Update(u1, true))
if result := <-store.Channel().ExtraUpdateByUser(u1.Id, u1.DeleteAt); result.Err != nil {
t.Fatal("failed to update extras by user: %v", result.Err)
}
-
- if result := <-store.Channel().GetExtraMembers(c1.Id, -1); result.Err != nil {
- t.Fatal("failed to get extras: %v", result.Err)
- } else if len(result.Data.([]model.ExtraMember)) != 1 {
- t.Fatal("got incorrect member count %v", len(result.Data.([]model.ExtraMember)))
- }
}
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index ec8679b31..44ffb556e 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -43,6 +43,7 @@ func (s SqlPostStore) CreateIndexesIfNotExists() {
s.CreateIndexIfNotExists("idx_posts_create_at", "Posts", "CreateAt")
s.CreateIndexIfNotExists("idx_posts_channel_id", "Posts", "ChannelId")
s.CreateIndexIfNotExists("idx_posts_root_id", "Posts", "RootId")
+ s.CreateIndexIfNotExists("idx_posts_user_id", "Posts", "UserId")
s.CreateFullTextIndexIfNotExists("idx_posts_message_txt", "Posts", "Message")
s.CreateFullTextIndexIfNotExists("idx_posts_hashtags_txt", "Posts", "Hashtags")
@@ -811,47 +812,36 @@ func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChan
result := StoreResult{}
query :=
- `SELECT
- t1.Name, COUNT(t1.UserId) AS Value
- FROM
- (SELECT DISTINCT
+ `SELECT DISTINCT
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
- Posts.UserId
- FROM
- Posts, Channels
- WHERE
- Posts.ChannelId = Channels.Id`
+ COUNT(DISTINCT Posts.UserId) AS Value
+ FROM Posts
+ INNER JOIN Channels
+ ON Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
query += " AND Channels.TeamId = :TeamId"
}
query += ` AND Posts.CreateAt >= :StartTime AND Posts.CreateAt <= :EndTime
- ORDER BY Name DESC) AS t1
- GROUP BY Name
+ GROUP BY DATE(FROM_UNIXTIME(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
query =
`SELECT
- TO_CHAR(t1.Name, 'YYYY-MM-DD') AS Name, COUNT(t1.UserId) AS Value
- FROM
- (SELECT DISTINCT
- DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)) AS Name,
- Posts.UserId
- FROM
- Posts, Channels
- WHERE
- Posts.ChannelId = Channels.Id`
+ TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name, COUNT(DISTINCT Posts.UserId) AS Value
+ FROM Posts
+ INNER JOIN Channels
+ ON Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
query += " AND Channels.TeamId = :TeamId"
}
query += ` AND Posts.CreateAt >= :StartTime AND Posts.CreateAt <= :EndTime
- ORDER BY Name DESC) AS t1
- GROUP BY Name
+ GROUP BY DATE(TO_TIMESTAMP(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
}
@@ -884,15 +874,12 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
result := StoreResult{}
query :=
- `SELECT
- Name, COUNT(Value) AS Value
- FROM
- (SELECT
+ `SELECT
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
- '1' AS Value
- FROM
- Posts, Channels
- WHERE
+ COUNT(Posts.Id) AS Value
+ FROM Posts
+ INNER JOIN Channels
+ ON
Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
@@ -900,31 +887,26 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
}
query += ` AND Posts.CreateAt <= :EndTime
- AND Posts.CreateAt >= :StartTime) AS t1
- GROUP BY Name
+ AND Posts.CreateAt >= :StartTime
+ GROUP BY DATE(FROM_UNIXTIME(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
query =
- `SELECT
- Name, COUNT(Value) AS Value
- FROM
- (SELECT
- TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name,
- '1' AS Value
- FROM
- Posts, Channels
- WHERE
- Posts.ChannelId = Channels.Id`
+ `SELECT
+ TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name, Count(Posts.Id) AS Value
+ FROM Posts
+ INNER JOIN Channels
+ ON Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
query += " AND Channels.TeamId = :TeamId"
}
query += ` AND Posts.CreateAt <= :EndTime
- AND Posts.CreateAt >= :StartTime) AS t1
- GROUP BY Name
+ AND Posts.CreateAt >= :StartTime
+ GROUP BY DATE(TO_TIMESTAMP(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
}
diff --git a/store/sql_status_store.go b/store/sql_status_store.go
index 4d186a30e..7b9fdea5d 100644
--- a/store/sql_status_store.go
+++ b/store/sql_status_store.go
@@ -5,6 +5,7 @@ package store
import (
"database/sql"
+ "strconv"
"github.com/mattermost/platform/model"
)
@@ -43,11 +44,11 @@ func (s SqlStatusStore) SaveOrUpdate(status *model.Status) StoreChannel {
if err := s.GetReplica().SelectOne(&model.Status{}, "SELECT * FROM Status WHERE UserId = :UserId", map[string]interface{}{"UserId": status.UserId}); err == nil {
if _, err := s.GetMaster().Update(status); err != nil {
- result.Err = model.NewLocAppError("SqlStatusStore.SaveOrUpdate", "store.sql_status.update.app_error", nil, "")
+ result.Err = model.NewLocAppError("SqlStatusStore.SaveOrUpdate", "store.sql_status.update.app_error", nil, err.Error())
}
} else {
if err := s.GetMaster().Insert(status); err != nil {
- result.Err = model.NewLocAppError("SqlStatusStore.SaveOrUpdate", "store.sql_status.save.app_error", nil, "")
+ result.Err = model.NewLocAppError("SqlStatusStore.SaveOrUpdate", "store.sql_status.save.app_error", nil, err.Error())
}
}
@@ -89,6 +90,38 @@ func (s SqlStatusStore) Get(userId string) StoreChannel {
return storeChannel
}
+func (s SqlStatusStore) GetByIds(userIds []string) StoreChannel {
+ storeChannel := make(StoreChannel, 1)
+
+ go func() {
+ result := StoreResult{}
+
+ props := make(map[string]interface{})
+ idQuery := ""
+
+ for index, userId := range userIds {
+ if len(idQuery) > 0 {
+ idQuery += ", "
+ }
+
+ props["userId"+strconv.Itoa(index)] = userId
+ idQuery += ":userId" + strconv.Itoa(index)
+ }
+
+ var statuses []*model.Status
+ if _, err := s.GetReplica().Select(&statuses, "SELECT * FROM Status WHERE UserId IN ("+idQuery+")", props); err != nil {
+ result.Err = model.NewLocAppError("SqlStatusStore.GetByIds", "store.sql_status.get.app_error", nil, err.Error())
+ } else {
+ result.Data = statuses
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlStatusStore) GetOnlineAway() StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -96,7 +129,7 @@ func (s SqlStatusStore) GetOnlineAway() StoreChannel {
result := StoreResult{}
var statuses []*model.Status
- if _, err := s.GetReplica().Select(&statuses, "SELECT * FROM Status WHERE Status = :Online OR Status = :Away", map[string]interface{}{"Online": model.STATUS_ONLINE, "Away": model.STATUS_AWAY}); err != nil {
+ if _, err := s.GetReplica().Select(&statuses, "SELECT * FROM Status WHERE Status = :Online OR Status = :Away LIMIT 300", map[string]interface{}{"Online": model.STATUS_ONLINE, "Away": model.STATUS_AWAY}); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.GetOnlineAway", "store.sql_status.get_online_away.app_error", nil, err.Error())
} else {
result.Data = statuses
@@ -157,7 +190,7 @@ func (s SqlStatusStore) ResetAll() StoreChannel {
go func() {
result := StoreResult{}
- if _, err := s.GetMaster().Exec("UPDATE Status SET Status = :Status", map[string]interface{}{"Status": model.STATUS_OFFLINE}); err != nil {
+ if _, err := s.GetMaster().Exec("UPDATE Status SET Status = :Status WHERE Manual = 0", map[string]interface{}{"Status": model.STATUS_OFFLINE}); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.ResetAll", "store.sql_status.reset_all.app_error", nil, "")
}
diff --git a/store/sql_status_store_test.go b/store/sql_status_store_test.go
index dff4db55e..dce973850 100644
--- a/store/sql_status_store_test.go
+++ b/store/sql_status_store_test.go
@@ -60,6 +60,15 @@ func TestSqlStatusStore(t *testing.T) {
}
}
+ if result := <-store.Status().GetByIds([]string{status.UserId, "junk"}); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ statuses := result.Data.([]*model.Status)
+ if len(statuses) != 1 {
+ t.Fatal("should only have 1 status")
+ }
+ }
+
if err := (<-store.Status().ResetAll()).Err; err != nil {
t.Fatal(err)
}
diff --git a/store/sql_store.go b/store/sql_store.go
index a2bc8f1b8..1c0de5932 100644
--- a/store/sql_store.go
+++ b/store/sql_store.go
@@ -33,6 +33,7 @@ import (
const (
INDEX_TYPE_FULL_TEXT = "full_text"
INDEX_TYPE_DEFAULT = "default"
+ MAX_DB_CONN_LIFETIME = 15
)
const (
@@ -94,9 +95,7 @@ func initConnection() *SqlStore {
if len(utils.Cfg.SqlSettings.DataSourceReplicas) == 0 {
sqlStore.replicas = make([]*gorp.DbMap, 1)
- sqlStore.replicas[0] = setupConnection(fmt.Sprintf("replica-%v", 0), utils.Cfg.SqlSettings.DriverName, utils.Cfg.SqlSettings.DataSource,
- utils.Cfg.SqlSettings.MaxIdleConns, utils.Cfg.SqlSettings.MaxOpenConns,
- utils.Cfg.SqlSettings.Trace)
+ sqlStore.replicas[0] = sqlStore.master
} else {
sqlStore.replicas = make([]*gorp.DbMap, len(utils.Cfg.SqlSettings.DataSourceReplicas))
for i, replica := range utils.Cfg.SqlSettings.DataSourceReplicas {
@@ -183,6 +182,7 @@ func setupConnection(con_type string, driver string, dataSource string, maxIdle
db.SetMaxIdleConns(maxIdle)
db.SetMaxOpenConns(maxOpen)
+ db.SetConnMaxLifetime(time.Duration(MAX_DB_CONN_LIFETIME) * time.Minute)
var dbmap *gorp.DbMap
@@ -205,6 +205,26 @@ func setupConnection(con_type string, driver string, dataSource string, maxIdle
return dbmap
}
+func (ss SqlStore) TotalMasterDbConnections() int {
+ return ss.GetMaster().Db.Stats().OpenConnections
+}
+
+func (ss SqlStore) TotalReadDbConnections() int {
+
+ if len(utils.Cfg.SqlSettings.DataSourceReplicas) == 0 {
+ return 0
+ } else {
+ count := 0
+ for _, db := range ss.replicas {
+ count = count + db.Db.Stats().OpenConnections
+ }
+
+ return count
+ }
+
+ return 0
+}
+
func (ss SqlStore) GetCurrentSchemaVersion() string {
version, _ := ss.GetMaster().SelectStr("SELECT Value FROM Systems WHERE Name='Version'")
return version
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 34a4a097d..a69c84904 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -5,6 +5,7 @@ package store
import (
"database/sql"
+ "strconv"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
@@ -441,14 +442,14 @@ func (s SqlTeamStore) GetMember(teamId string, userId string) StoreChannel {
return storeChannel
}
-func (s SqlTeamStore) GetMembers(teamId string) StoreChannel {
+func (s SqlTeamStore) GetMembers(teamId string, offset int, limit int) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
var members []*model.TeamMember
- _, err := s.GetReplica().Select(&members, "SELECT * FROM TeamMembers WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId})
+ _, err := s.GetReplica().Select(&members, "SELECT * FROM TeamMembers WHERE TeamId = :TeamId AND DeleteAt = 0 LIMIT :Limit OFFSET :Offset", map[string]interface{}{"TeamId": teamId, "Offset": offset, "Limit": limit})
if err != nil {
result.Err = model.NewLocAppError("SqlTeamStore.GetMembers", "store.sql_team.get_members.app_error", nil, "teamId="+teamId+" "+err.Error())
} else {
@@ -462,6 +463,70 @@ func (s SqlTeamStore) GetMembers(teamId string) StoreChannel {
return storeChannel
}
+func (s SqlTeamStore) GetMemberCount(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel, 1)
+
+ go func() {
+ result := StoreResult{}
+
+ count, err := s.GetReplica().SelectInt(`
+ SELECT
+ count(*)
+ FROM
+ TeamMembers,
+ Users
+ WHERE
+ TeamMembers.UserId = Users.Id
+ AND TeamMembers.TeamId = :TeamId
+ AND TeamMembers.DeleteAt = 0
+ AND Users.DeleteAt = 0`, map[string]interface{}{"TeamId": teamId})
+ if err != nil {
+ result.Err = model.NewLocAppError("SqlTeamStore.GetMemberCount", "store.sql_team.get_member_count.app_error", nil, "teamId="+teamId+" "+err.Error())
+ } else {
+ result.Data = count
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlTeamStore) GetMembersByIds(teamId string, userIds []string) StoreChannel {
+ storeChannel := make(StoreChannel, 1)
+
+ go func() {
+ result := StoreResult{}
+
+ var members []*model.TeamMember
+ props := make(map[string]interface{})
+ idQuery := ""
+
+ for index, userId := range userIds {
+ if len(idQuery) > 0 {
+ idQuery += ", "
+ }
+
+ props["userId"+strconv.Itoa(index)] = userId
+ idQuery += ":userId" + strconv.Itoa(index)
+ }
+
+ props["TeamId"] = teamId
+
+ if _, err := s.GetReplica().Select(&members, "SELECT * FROM TeamMembers WHERE TeamId = :TeamId AND UserId IN ("+idQuery+") AND DeleteAt = 0", props); err != nil {
+ result.Err = model.NewLocAppError("SqlTeamStore.GetMembersByIds", "store.sql_team.get_members_by_ids.app_error", nil, "teamId="+teamId+" "+err.Error())
+ } else {
+ result.Data = members
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlTeamStore) GetTeamsForUser(userId string) StoreChannel {
storeChannel := make(StoreChannel, 1)
diff --git a/store/sql_team_store_test.go b/store/sql_team_store_test.go
index be72786d3..46215d9be 100644
--- a/store/sql_team_store_test.go
+++ b/store/sql_team_store_test.go
@@ -298,7 +298,7 @@ func TestTeamMembers(t *testing.T) {
Must(store.Team().SaveMember(m2))
Must(store.Team().SaveMember(m3))
- if r1 := <-store.Team().GetMembers(teamId1); r1.Err != nil {
+ if r1 := <-store.Team().GetMembers(teamId1, 0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
ms := r1.Data.([]*model.TeamMember)
@@ -308,7 +308,7 @@ func TestTeamMembers(t *testing.T) {
}
}
- if r1 := <-store.Team().GetMembers(teamId2); r1.Err != nil {
+ if r1 := <-store.Team().GetMembers(teamId2, 0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
ms := r1.Data.([]*model.TeamMember)
@@ -342,7 +342,7 @@ func TestTeamMembers(t *testing.T) {
t.Fatal(r1.Err)
}
- if r1 := <-store.Team().GetMembers(teamId1); r1.Err != nil {
+ if r1 := <-store.Team().GetMembers(teamId1, 0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
ms := r1.Data.([]*model.TeamMember)
@@ -363,7 +363,7 @@ func TestTeamMembers(t *testing.T) {
t.Fatal(r1.Err)
}
- if r1 := <-store.Team().GetMembers(teamId1); r1.Err != nil {
+ if r1 := <-store.Team().GetMembers(teamId1, 0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
ms := r1.Data.([]*model.TeamMember)
@@ -434,3 +434,74 @@ func TestGetTeamMember(t *testing.T) {
t.Fatal("empty team id - should have failed")
}
}
+
+func TestGetTeamMembersByIds(t *testing.T) {
+ Setup()
+
+ teamId1 := model.NewId()
+
+ m1 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
+ Must(store.Team().SaveMember(m1))
+
+ if r := <-store.Team().GetMembersByIds(m1.TeamId, []string{m1.UserId}); r.Err != nil {
+ t.Fatal(r.Err)
+ } else {
+ rm1 := r.Data.([]*model.TeamMember)[0]
+
+ if rm1.TeamId != m1.TeamId {
+ t.Fatal("bad team id")
+ }
+
+ if rm1.UserId != m1.UserId {
+ t.Fatal("bad user id")
+ }
+ }
+
+ m2 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
+ Must(store.Team().SaveMember(m2))
+
+ if r := <-store.Team().GetMembersByIds(m1.TeamId, []string{m1.UserId, m2.UserId, model.NewId()}); r.Err != nil {
+ t.Fatal(r.Err)
+ } else {
+ rm := r.Data.([]*model.TeamMember)
+
+ if len(rm) != 2 {
+ t.Fatal("return wrong number of results")
+ }
+ }
+
+ if r := <-store.Team().GetMembersByIds(m1.TeamId, []string{}); r.Err == nil {
+ t.Fatal("empty user ids - should have failed")
+ }
+}
+
+func TestTeamStoreMemberCount(t *testing.T) {
+ Setup()
+
+ u1 := &model.User{}
+ u1.Email = model.NewId()
+ Must(store.User().Save(u1))
+
+ teamId1 := model.NewId()
+ m1 := &model.TeamMember{TeamId: teamId1, UserId: u1.Id}
+ Must(store.Team().SaveMember(m1))
+
+ if result := <-store.Team().GetMemberCount(teamId1); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ if result.Data.(int64) != 1 {
+ t.Fatal("wrong count")
+ }
+ }
+
+ m2 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
+ Must(store.Team().SaveMember(m2))
+
+ if result := <-store.Team().GetMemberCount(teamId1); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ if result.Data.(int64) != 1 {
+ t.Fatal("wrong count")
+ }
+ }
+}
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 8ada9eb2c..ca86ef115 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -15,14 +15,20 @@ import (
)
const (
- MISSING_ACCOUNT_ERROR = "store.sql_user.missing_account.const"
- MISSING_AUTH_ACCOUNT_ERROR = "store.sql_user.get_by_auth.missing_account.app_error"
+ MISSING_ACCOUNT_ERROR = "store.sql_user.missing_account.const"
+ MISSING_AUTH_ACCOUNT_ERROR = "store.sql_user.get_by_auth.missing_account.app_error"
+ PROFILES_IN_CHANNEL_CACHE_SIZE = 5000
+ PROFILES_IN_CHANNEL_CACHE_SEC = 900 // 15 mins
+ USER_SEARCH_TYPE_ALL = "Username, FirstName, LastName, Nickname"
+ USER_SEARCH_TYPE_USERNAME = "Username"
)
type SqlUserStore struct {
*SqlStore
}
+var profilesInChannelCache *utils.Cache = utils.NewLru(PROFILES_IN_CHANNEL_CACHE_SIZE)
+
func NewSqlUserStore(sqlStore *SqlStore) UserStore {
us := &SqlUserStore{sqlStore}
@@ -49,6 +55,9 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
func (us SqlUserStore) CreateIndexesIfNotExists() {
us.CreateIndexIfNotExists("idx_users_email", "Users", "Email")
+
+ us.CreateFullTextIndexIfNotExists("idx_users_username_txt", "Users", USER_SEARCH_TYPE_USERNAME)
+ us.CreateFullTextIndexIfNotExists("idx_users_all_names_txt", "Users", USER_SEARCH_TYPE_ALL)
}
func (us SqlUserStore) Save(user *model.User) StoreChannel {
@@ -457,7 +466,7 @@ func (s SqlUserStore) GetEtagForAllProfiles() StoreChannel {
return storeChannel
}
-func (us SqlUserStore) GetAllProfiles() StoreChannel {
+func (us SqlUserStore) GetAllProfiles(offset int, limit int) StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -466,8 +475,8 @@ func (us SqlUserStore) GetAllProfiles() StoreChannel {
var users []*model.User
- if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users"); err != nil {
- result.Err = model.NewLocAppError("SqlUserStore.GetProfiles", "store.sql_user.get_profiles.app_error", nil, err.Error())
+ if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users ORDER BY Username ASC LIMIT :Limit OFFSET :Offset", map[string]interface{}{"Offset": offset, "Limit": limit}); err != nil {
+ result.Err = model.NewLocAppError("SqlUserStore.GetAllProfiles", "store.sql_user.get_profiles.app_error", nil, err.Error())
} else {
userMap := make(map[string]*model.User)
@@ -509,7 +518,7 @@ func (s SqlUserStore) GetEtagForProfiles(teamId string) StoreChannel {
return storeChannel
}
-func (us SqlUserStore) GetProfiles(teamId string) StoreChannel {
+func (us SqlUserStore) GetProfiles(teamId string, offset int, limit int) StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -518,7 +527,7 @@ func (us SqlUserStore) GetProfiles(teamId string) StoreChannel {
var users []*model.User
- if _, err := us.GetReplica().Select(&users, "SELECT Users.* FROM Users, TeamMembers WHERE TeamMembers.TeamId = :TeamId AND Users.Id = TeamMembers.UserId", map[string]interface{}{"TeamId": teamId}); err != nil {
+ if _, err := us.GetReplica().Select(&users, "SELECT Users.* FROM Users, TeamMembers WHERE TeamMembers.TeamId = :TeamId AND Users.Id = TeamMembers.UserId AND TeamMembers.DeleteAt = 0 ORDER BY Users.Username ASC LIMIT :Limit OFFSET :Offset", map[string]interface{}{"TeamId": teamId, "Offset": offset, "Limit": limit}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.GetProfiles", "store.sql_user.get_profiles.app_error", nil, err.Error())
} else {
@@ -541,9 +550,64 @@ func (us SqlUserStore) GetProfiles(teamId string) StoreChannel {
return storeChannel
}
-func (us SqlUserStore) GetDirectProfiles(userId string) StoreChannel {
+func (us SqlUserStore) InvalidateProfilesInChannelCache(channelId string) {
+ profilesInChannelCache.Remove(channelId)
+}
- storeChannel := make(StoreChannel, 1)
+func (us SqlUserStore) GetProfilesInChannel(channelId string, offset int, limit int, allowFromCache bool) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if allowFromCache && offset == -1 && limit == -1 {
+ if cacheItem, ok := profilesInChannelCache.Get(channelId); ok {
+ result.Data = cacheItem.(map[string]*model.User)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+ }
+
+ var users []*model.User
+
+ query := "SELECT Users.* FROM Users, ChannelMembers WHERE ChannelMembers.ChannelId = :ChannelId AND Users.Id = ChannelMembers.UserId AND Users.DeleteAt = 0"
+
+ if limit >= 0 && offset >= 0 {
+ query += " ORDER BY Users.Username ASC LIMIT :Limit OFFSET :Offset"
+ }
+
+ if _, err := us.GetReplica().Select(&users, query, map[string]interface{}{"ChannelId": channelId, "Offset": offset, "Limit": limit}); err != nil {
+ result.Err = model.NewLocAppError("SqlUserStore.GetProfilesInChannel", "store.sql_user.get_profiles.app_error", nil, err.Error())
+ } else {
+
+ userMap := make(map[string]*model.User)
+
+ for _, u := range users {
+ u.Password = ""
+ u.AuthData = new(string)
+ *u.AuthData = ""
+ userMap[u.Id] = u
+ }
+
+ result.Data = userMap
+
+ if allowFromCache && offset == -1 && limit == -1 {
+ profilesInChannelCache.AddWithExpiresInSecs(channelId, userMap, PROFILES_IN_CHANNEL_CACHE_SEC)
+ }
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int) StoreChannel {
+
+ storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
@@ -551,35 +615,20 @@ func (us SqlUserStore) GetDirectProfiles(userId string) StoreChannel {
var users []*model.User
if _, err := us.GetReplica().Select(&users, `
- SELECT
- Users.*
- FROM
- Users
- WHERE
- Id IN (SELECT DISTINCT
- UserId
- FROM
- ChannelMembers
- WHERE
- ChannelMembers.UserId != :UserId
- AND ChannelMembers.ChannelId IN (SELECT
- Channels.Id
- FROM
- Channels,
- ChannelMembers
- WHERE
- Channels.Type = 'D'
- AND Channels.Id = ChannelMembers.ChannelId
- AND ChannelMembers.UserId = :UserId))
- OR Id IN (SELECT
- Name
- FROM
- Preferences
- WHERE
- UserId = :UserId
- AND Category = 'direct_channel_show')
- `, map[string]interface{}{"UserId": userId}); err != nil {
- result.Err = model.NewLocAppError("SqlUserStore.GetDirectProfiles", "store.sql_user.get_profiles.app_error", nil, err.Error())
+ SELECT
+ u.*
+ FROM Users u
+ INNER JOIN TeamMembers tm
+ ON tm.UserId = u.Id
+ AND tm.TeamId = :TeamId
+ LEFT JOIN ChannelMembers cm
+ ON cm.UserId = u.Id
+ AND cm.ChannelId = :ChannelId
+ WHERE cm.UserId IS NULL
+ ORDER BY u.Username ASC
+ LIMIT :Limit OFFSET :Offset
+ `, map[string]interface{}{"TeamId": teamId, "ChannelId": channelId, "Offset": offset, "Limit": limit}); err != nil {
+ result.Err = model.NewLocAppError("SqlUserStore.GetProfilesNotInChannel", "store.sql_user.get_profiles.app_error", nil, err.Error())
} else {
userMap := make(map[string]*model.User)
@@ -601,6 +650,99 @@ func (us SqlUserStore) GetDirectProfiles(userId string) StoreChannel {
return storeChannel
}
+func (us SqlUserStore) GetProfilesByUsernames(usernames []string, teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var users []*model.User
+ props := make(map[string]interface{})
+ idQuery := ""
+
+ for index, usernames := range usernames {
+ if len(idQuery) > 0 {
+ idQuery += ", "
+ }
+
+ props["username"+strconv.Itoa(index)] = usernames
+ idQuery += ":username" + strconv.Itoa(index)
+ }
+
+ props["TeamId"] = teamId
+
+ if _, err := us.GetReplica().Select(&users, `SELECT Users.* FROM Users INNER JOIN TeamMembers ON
+ Users.Id = TeamMembers.UserId AND Users.Username IN (`+idQuery+`) AND TeamMembers.TeamId = :TeamId `, props); err != nil {
+ result.Err = model.NewLocAppError("SqlUserStore.GetProfilesByUsernames", "store.sql_user.get_profiles.app_error", nil, err.Error())
+ } else {
+ userMap := make(map[string]*model.User)
+
+ for _, u := range users {
+ u.Password = ""
+ u.AuthData = new(string)
+ *u.AuthData = ""
+ userMap[u.Id] = u
+ }
+
+ result.Data = userMap
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+type UserWithLastActivityAt struct {
+ model.User
+ LastActivityAt int64
+}
+
+func (us SqlUserStore) GetRecentlyActiveUsersForTeam(teamId string) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var users []*UserWithLastActivityAt
+
+ if _, err := us.GetReplica().Select(&users, `
+ SELECT
+ u.*,
+ s.LastActivityAt
+ FROM Users AS u
+ INNER JOIN TeamMembers AS t ON u.Id = t.UserId
+ INNER JOIN Status AS s ON s.UserId = t.UserId
+ WHERE t.TeamId = :TeamId
+ ORDER BY s.LastActivityAt DESC
+ LIMIT 100
+ `, map[string]interface{}{"TeamId": teamId}); err != nil {
+ result.Err = model.NewLocAppError("SqlUserStore.GetRecentlyActiveUsers", "store.sql_user.get_recently_active_users.app_error", nil, err.Error())
+ } else {
+
+ userMap := make(map[string]*model.User)
+
+ for _, userWithLastActivityAt := range users {
+ u := userWithLastActivityAt.User
+ u.Password = ""
+ u.AuthData = new(string)
+ *u.AuthData = ""
+ u.LastActivityAt = userWithLastActivityAt.LastActivityAt
+ userMap[u.Id] = &u
+ }
+
+ result.Data = userMap
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (us SqlUserStore) GetProfileByIds(userIds []string) StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -938,3 +1080,144 @@ func (us SqlUserStore) GetUnreadCountForChannel(userId string, channelId string)
return storeChannel
}
+
+func (us SqlUserStore) Search(teamId string, term string, searchType string) StoreChannel {
+ storeChannel := make(StoreChannel, 1)
+
+ go func() {
+ searchQuery := ""
+ if teamId == "" {
+ searchQuery = `
+ SELECT
+ *
+ FROM
+ Users
+ WHERE
+ DeleteAt = 0
+ SEARCH_CLAUSE
+ ORDER BY Username ASC
+ LIMIT 50`
+ } else {
+ searchQuery = `
+ SELECT
+ Users.*
+ FROM
+ Users, TeamMembers
+ WHERE
+ TeamMembers.TeamId = :TeamId
+ AND Users.Id = TeamMembers.UserId
+ AND Users.DeleteAt = 0
+ AND TeamMembers.DeleteAt = 0
+ SEARCH_CLAUSE
+ ORDER BY Users.Username ASC
+ LIMIT 100`
+ }
+
+ storeChannel <- us.performSearch(searchQuery, term, searchType, map[string]interface{}{"TeamId": teamId})
+ close(storeChannel)
+
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term string, searchType string) StoreChannel {
+ storeChannel := make(StoreChannel, 1)
+
+ go func() {
+ searchQuery := ""
+ if teamId == "" {
+ searchQuery = `
+ SELECT
+ u.*
+ FROM Users u
+ LEFT JOIN ChannelMembers cm
+ ON cm.UserId = u.Id
+ AND cm.ChannelId = :ChannelId
+ WHERE cm.UserId IS NULL
+ SEARCH_CLAUSE
+ ORDER BY u.Username ASC
+ LIMIT 100`
+ } else {
+ searchQuery = `
+ SELECT
+ u.*
+ FROM Users u
+ INNER JOIN TeamMembers tm
+ ON tm.UserId = u.Id
+ AND tm.TeamId = :TeamId
+ LEFT JOIN ChannelMembers cm
+ ON cm.UserId = u.Id
+ AND cm.ChannelId = :ChannelId
+ WHERE cm.UserId IS NULL
+ SEARCH_CLAUSE
+ ORDER BY u.Username ASC
+ LIMIT 100`
+ }
+
+ storeChannel <- us.performSearch(searchQuery, term, searchType, map[string]interface{}{"TeamId": teamId, "ChannelId": channelId})
+ close(storeChannel)
+
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) SearchInChannel(channelId string, term string, searchType string) StoreChannel {
+ storeChannel := make(StoreChannel, 1)
+
+ go func() {
+ searchQuery := `
+ SELECT
+ Users.*
+ FROM
+ Users, ChannelMembers
+ WHERE
+ ChannelMembers.ChannelId = :ChannelId
+ AND ChannelMembers.UserId = Users.Id
+ AND Users.DeleteAt = 0
+ SEARCH_CLAUSE
+ ORDER BY Users.Username ASC
+ LIMIT 100`
+
+ storeChannel <- us.performSearch(searchQuery, term, searchType, map[string]interface{}{"ChannelId": channelId})
+ close(storeChannel)
+
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) performSearch(searchQuery string, term string, searchType string, parameters map[string]interface{}) StoreResult {
+ result := StoreResult{}
+
+ if term == "" {
+ searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1)
+ } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
+ term = term + ":*"
+ searchClause := fmt.Sprintf("AND (%s) @@ to_tsquery(:Term)", searchType)
+ searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
+ } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
+ term = term + "*"
+ searchClause := fmt.Sprintf("AND MATCH(%s) AGAINST (:Term IN BOOLEAN MODE)", searchType)
+ searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
+ }
+
+ var users []*model.User
+
+ parameters["Term"] = term
+
+ if _, err := us.GetReplica().Select(&users, searchQuery, parameters); err != nil {
+ result.Err = model.NewLocAppError("SqlUserStore.Search", "store.sql_user.search.app_error", nil, "term="+term+", "+"search_type="+searchType+", "+err.Error())
+ } else {
+ for _, u := range users {
+ u.Password = ""
+ u.AuthData = new(string)
+ *u.AuthData = ""
+ }
+
+ result.Data = users
+ }
+
+ return result
+}
diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go
index 076be1a81..7ffb68a47 100644
--- a/store/sql_user_store_test.go
+++ b/store/sql_user_store_test.go
@@ -205,7 +205,7 @@ func TestUserStoreGetAllProfiles(t *testing.T) {
Must(store.User().Save(u2))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}))
- if r1 := <-store.User().GetAllProfiles(); r1.Err != nil {
+ if r1 := <-store.User().GetAllProfiles(0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
users := r1.Data.(map[string]*model.User)
@@ -213,6 +213,15 @@ func TestUserStoreGetAllProfiles(t *testing.T) {
t.Fatal("invalid returned users")
}
}
+
+ if r2 := <-store.User().GetAllProfiles(0, 1); r2.Err != nil {
+ t.Fatal(r2.Err)
+ } else {
+ users := r2.Data.(map[string]*model.User)
+ if len(users) != 1 {
+ t.Fatal("invalid returned users, limit did not work")
+ }
+ }
}
func TestUserStoreGetProfiles(t *testing.T) {
@@ -230,7 +239,7 @@ func TestUserStoreGetProfiles(t *testing.T) {
Must(store.User().Save(u2))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}))
- if r1 := <-store.User().GetProfiles(teamId); r1.Err != nil {
+ if r1 := <-store.User().GetProfiles(teamId, 0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
users := r1.Data.(map[string]*model.User)
@@ -243,7 +252,7 @@ func TestUserStoreGetProfiles(t *testing.T) {
}
}
- if r2 := <-store.User().GetProfiles("123"); r2.Err != nil {
+ if r2 := <-store.User().GetProfiles("123", 0, 100); r2.Err != nil {
t.Fatal(r2.Err)
} else {
if len(r2.Data.(map[string]*model.User)) != 0 {
@@ -252,7 +261,7 @@ func TestUserStoreGetProfiles(t *testing.T) {
}
}
-func TestUserStoreGetDirectProfiles(t *testing.T) {
+func TestUserStoreGetProfilesInChannel(t *testing.T) {
Setup()
teamId := model.NewId()
@@ -267,22 +276,166 @@ func TestUserStoreGetDirectProfiles(t *testing.T) {
Must(store.User().Save(u2))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}))
- if r1 := <-store.User().GetDirectProfiles(u1.Id); r1.Err != nil {
+ c1 := model.Channel{}
+ c1.TeamId = teamId
+ c1.DisplayName = "Profiles in channel"
+ c1.Name = "profiles-" + model.NewId()
+ c1.Type = model.CHANNEL_OPEN
+
+ c2 := model.Channel{}
+ c2.TeamId = teamId
+ c2.DisplayName = "Profiles in private"
+ c2.Name = "profiles-" + model.NewId()
+ c2.Type = model.CHANNEL_PRIVATE
+
+ Must(store.Channel().Save(&c1))
+ Must(store.Channel().Save(&c2))
+
+ m1 := model.ChannelMember{}
+ m1.ChannelId = c1.Id
+ m1.UserId = u1.Id
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
+
+ m2 := model.ChannelMember{}
+ m2.ChannelId = c1.Id
+ m2.UserId = u2.Id
+ m2.NotifyProps = model.GetDefaultChannelNotifyProps()
+
+ m3 := model.ChannelMember{}
+ m3.ChannelId = c2.Id
+ m3.UserId = u1.Id
+ m3.NotifyProps = model.GetDefaultChannelNotifyProps()
+
+ Must(store.Channel().SaveMember(&m1))
+ Must(store.Channel().SaveMember(&m2))
+ Must(store.Channel().SaveMember(&m3))
+
+ if r1 := <-store.User().GetProfilesInChannel(c1.Id, -1, -1, false); r1.Err != nil {
t.Fatal(r1.Err)
} else {
users := r1.Data.(map[string]*model.User)
- if len(users) != 0 {
+ if len(users) != 2 {
t.Fatal("invalid returned users")
}
+
+ if users[u1.Id].Id != u1.Id {
+ t.Fatal("invalid returned user")
+ }
}
- if r2 := <-store.User().GetDirectProfiles("123"); r2.Err != nil {
+ if r2 := <-store.User().GetProfilesInChannel(c2.Id, -1, -1, false); r2.Err != nil {
t.Fatal(r2.Err)
} else {
- if len(r2.Data.(map[string]*model.User)) != 0 {
+ if len(r2.Data.(map[string]*model.User)) != 1 {
+ t.Fatal("should have returned empty map")
+ }
+ }
+
+ if r2 := <-store.User().GetProfilesInChannel(c2.Id, -1, -1, true); r2.Err != nil {
+ t.Fatal(r2.Err)
+ } else {
+ if len(r2.Data.(map[string]*model.User)) != 1 {
+ t.Fatal("should have returned empty map")
+ }
+ }
+
+ if r2 := <-store.User().GetProfilesInChannel(c2.Id, -1, -1, true); r2.Err != nil {
+ t.Fatal(r2.Err)
+ } else {
+ if len(r2.Data.(map[string]*model.User)) != 1 {
t.Fatal("should have returned empty map")
}
}
+
+ store.User().InvalidateProfilesInChannelCache(c2.Id)
+}
+
+func TestUserStoreGetProfilesNotInChannel(t *testing.T) {
+ Setup()
+
+ teamId := model.NewId()
+
+ u1 := &model.User{}
+ u1.Email = model.NewId()
+ Must(store.User().Save(u1))
+ Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}))
+
+ u2 := &model.User{}
+ u2.Email = model.NewId()
+ Must(store.User().Save(u2))
+ Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}))
+
+ c1 := model.Channel{}
+ c1.TeamId = teamId
+ c1.DisplayName = "Profiles in channel"
+ c1.Name = "profiles-" + model.NewId()
+ c1.Type = model.CHANNEL_OPEN
+
+ c2 := model.Channel{}
+ c2.TeamId = teamId
+ c2.DisplayName = "Profiles in private"
+ c2.Name = "profiles-" + model.NewId()
+ c2.Type = model.CHANNEL_PRIVATE
+
+ Must(store.Channel().Save(&c1))
+ Must(store.Channel().Save(&c2))
+
+ if r1 := <-store.User().GetProfilesNotInChannel(teamId, c1.Id, 0, 100); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ users := r1.Data.(map[string]*model.User)
+ if len(users) != 2 {
+ t.Fatal("invalid returned users")
+ }
+
+ if users[u1.Id].Id != u1.Id {
+ t.Fatal("invalid returned user")
+ }
+ }
+
+ if r2 := <-store.User().GetProfilesNotInChannel(teamId, c2.Id, 0, 100); r2.Err != nil {
+ t.Fatal(r2.Err)
+ } else {
+ if len(r2.Data.(map[string]*model.User)) != 2 {
+ t.Fatal("invalid returned users")
+ }
+ }
+
+ m1 := model.ChannelMember{}
+ m1.ChannelId = c1.Id
+ m1.UserId = u1.Id
+ m1.NotifyProps = model.GetDefaultChannelNotifyProps()
+
+ m2 := model.ChannelMember{}
+ m2.ChannelId = c1.Id
+ m2.UserId = u2.Id
+ m2.NotifyProps = model.GetDefaultChannelNotifyProps()
+
+ m3 := model.ChannelMember{}
+ m3.ChannelId = c2.Id
+ m3.UserId = u1.Id
+ m3.NotifyProps = model.GetDefaultChannelNotifyProps()
+
+ Must(store.Channel().SaveMember(&m1))
+ Must(store.Channel().SaveMember(&m2))
+ Must(store.Channel().SaveMember(&m3))
+
+ if r1 := <-store.User().GetProfilesNotInChannel(teamId, c1.Id, 0, 100); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ users := r1.Data.(map[string]*model.User)
+ if len(users) != 0 {
+ t.Fatal("invalid returned users")
+ }
+ }
+
+ if r2 := <-store.User().GetProfilesNotInChannel(teamId, c2.Id, 0, 100); r2.Err != nil {
+ t.Fatal(r2.Err)
+ } else {
+ if len(r2.Data.(map[string]*model.User)) != 1 {
+ t.Fatal("should have had 1 user not in channel")
+ }
+ }
}
func TestUserStoreGetProfilesByIds(t *testing.T) {
@@ -326,7 +479,7 @@ func TestUserStoreGetProfilesByIds(t *testing.T) {
}
}
- if r2 := <-store.User().GetProfiles("123"); r2.Err != nil {
+ if r2 := <-store.User().GetProfiles("123", 0, 100); r2.Err != nil {
t.Fatal(r2.Err)
} else {
if len(r2.Data.(map[string]*model.User)) != 0 {
@@ -335,6 +488,50 @@ func TestUserStoreGetProfilesByIds(t *testing.T) {
}
}
+func TestUserStoreGetProfilesByUsernames(t *testing.T) {
+ Setup()
+
+ teamId := model.NewId()
+
+ u1 := &model.User{}
+ u1.Email = model.NewId()
+ u1.Username = "username1" + model.NewId()
+ Must(store.User().Save(u1))
+ Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}))
+
+ u2 := &model.User{}
+ u2.Email = model.NewId()
+ u2.Username = "username2" + model.NewId()
+ Must(store.User().Save(u2))
+ Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}))
+
+ if r1 := <-store.User().GetProfilesByUsernames([]string{u1.Username, u2.Username}, teamId); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ users := r1.Data.(map[string]*model.User)
+ if len(users) != 2 {
+ t.Fatal("invalid returned users")
+ }
+
+ if users[u1.Id].Id != u1.Id {
+ t.Fatal("invalid returned user")
+ }
+ }
+
+ if r1 := <-store.User().GetProfilesByUsernames([]string{u1.Username}, teamId); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ users := r1.Data.(map[string]*model.User)
+ if len(users) != 1 {
+ t.Fatal("invalid returned users")
+ }
+
+ if users[u1.Id].Id != u1.Id {
+ t.Fatal("invalid returned user")
+ }
+ }
+}
+
func TestUserStoreGetSystemAdminProfiles(t *testing.T) {
Setup()
@@ -713,3 +910,216 @@ func TestUserStoreUpdateMfaActive(t *testing.T) {
t.Fatal(err)
}
}
+
+func TestUserStoreGetRecentlyActiveUsersForTeam(t *testing.T) {
+ Setup()
+
+ u1 := &model.User{}
+ u1.Email = model.NewId()
+ Must(store.User().Save(u1))
+ Must(store.Status().SaveOrUpdate(&model.Status{u1.Id, model.STATUS_ONLINE, false, model.GetMillis(), ""}))
+ tid := model.NewId()
+ Must(store.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}))
+
+ if r1 := <-store.User().GetRecentlyActiveUsersForTeam(tid); r1.Err != nil {
+ t.Fatal(r1.Err)
+ }
+}
+
+func TestUserStoreSearch(t *testing.T) {
+ Setup()
+
+ u1 := &model.User{}
+ u1.Username = "jimbo" + model.NewId()
+ u1.FirstName = "Tim"
+ u1.LastName = "Bill"
+ u1.Nickname = "Rob"
+ u1.Email = model.NewId()
+ Must(store.User().Save(u1))
+
+ tid := model.NewId()
+ Must(store.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}))
+
+ if r1 := <-store.User().Search(tid, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ profiles := r1.Data.([]*model.User)
+ found := false
+ for _, profile := range profiles {
+ if profile.Id == u1.Id {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found user")
+ }
+ }
+
+ if r1 := <-store.User().Search("", "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ profiles := r1.Data.([]*model.User)
+ found := false
+ for _, profile := range profiles {
+ if profile.Id == u1.Id {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found user")
+ }
+ }
+
+ if r1 := <-store.User().Search(tid, "", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
+ t.Fatal(r1.Err)
+ }
+
+ c1 := model.Channel{}
+ c1.TeamId = tid
+ c1.DisplayName = "NameName"
+ c1.Name = "a" + model.NewId() + "b"
+ c1.Type = model.CHANNEL_OPEN
+ c1 = *Must(store.Channel().Save(&c1)).(*model.Channel)
+
+ if r1 := <-store.User().SearchNotInChannel(tid, c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ profiles := r1.Data.([]*model.User)
+ found := false
+ for _, profile := range profiles {
+ if profile.Id == u1.Id {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found user")
+ }
+ }
+
+ if r1 := <-store.User().SearchNotInChannel("", c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ profiles := r1.Data.([]*model.User)
+ found := false
+ for _, profile := range profiles {
+ if profile.Id == u1.Id {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found user")
+ }
+ }
+
+ if r1 := <-store.User().SearchNotInChannel("junk", c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ profiles := r1.Data.([]*model.User)
+ found := false
+ for _, profile := range profiles {
+ if profile.Id == u1.Id {
+ found = true
+ break
+ }
+ }
+
+ if found {
+ t.Fatal("should not have found user")
+ }
+ }
+
+ if r1 := <-store.User().SearchInChannel(c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ profiles := r1.Data.([]*model.User)
+ found := false
+ for _, profile := range profiles {
+ if profile.Id == u1.Id {
+ found = true
+ break
+ }
+ }
+
+ if found {
+ t.Fatal("should not have found user")
+ }
+ }
+
+ Must(store.Channel().SaveMember(&model.ChannelMember{ChannelId: c1.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}))
+
+ if r1 := <-store.User().SearchInChannel(c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ profiles := r1.Data.([]*model.User)
+ found := false
+ for _, profile := range profiles {
+ if profile.Id == u1.Id {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found user")
+ }
+ }
+
+ if r1 := <-store.User().Search(tid, "Tim", USER_SEARCH_TYPE_ALL); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ profiles := r1.Data.([]*model.User)
+ found := false
+ for _, profile := range profiles {
+ if profile.Id == u1.Id {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found user")
+ }
+ }
+
+ if r1 := <-store.User().Search(tid, "Bill", USER_SEARCH_TYPE_ALL); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ profiles := r1.Data.([]*model.User)
+ found := false
+ for _, profile := range profiles {
+ if profile.Id == u1.Id {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found user")
+ }
+ }
+
+ if r1 := <-store.User().Search(tid, "Rob", USER_SEARCH_TYPE_ALL); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ profiles := r1.Data.([]*model.User)
+ found := false
+ for _, profile := range profiles {
+ if profile.Id == u1.Id {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ t.Fatal("should have found user")
+ }
+ }
+}
diff --git a/store/store.go b/store/store.go
index 7474d3afb..900709f16 100644
--- a/store/store.go
+++ b/store/store.go
@@ -49,6 +49,8 @@ type Store interface {
MarkSystemRanUnitTests()
Close()
DropAllTables()
+ TotalMasterDbConnections() int
+ TotalReadDbConnections() int
}
type TeamStore interface {
@@ -66,7 +68,9 @@ type TeamStore interface {
SaveMember(member *model.TeamMember) StoreChannel
UpdateMember(member *model.TeamMember) StoreChannel
GetMember(teamId string, userId string) StoreChannel
- GetMembers(teamId string) StoreChannel
+ GetMembers(teamId string, offset int, limit int) StoreChannel
+ GetMembersByIds(teamId string, userIds []string) StoreChannel
+ GetMemberCount(teamId string) StoreChannel
GetTeamsForUser(userId string) StoreChannel
RemoveMember(teamId string, userId string) StoreChannel
RemoveAllMembersByTeam(teamId string) StoreChannel
@@ -89,16 +93,17 @@ type ChannelStore interface {
GetChannelCounts(teamId string, userId string) StoreChannel
GetAll(teamId string) StoreChannel
GetForPost(postId string) StoreChannel
-
SaveMember(member *model.ChannelMember) StoreChannel
UpdateMember(member *model.ChannelMember) StoreChannel
GetMembers(channelId string) StoreChannel
GetMember(channelId string, userId string) StoreChannel
+ GetAllChannelMembersForUser(userId string, allowFromCache bool) StoreChannel
+ InvalidateAllChannelMembersForUser(userId string)
+ IsUserInChannelUseCache(userId string, channelId string) bool
GetMemberForPost(postId string, userId string) StoreChannel
GetMemberCount(channelId string) StoreChannel
RemoveMember(channelId string, userId string) StoreChannel
PermanentDeleteMembersByUser(userId string) StoreChannel
- GetExtraMembers(channelId string, limit int) StoreChannel
UpdateLastViewedAt(channelId string, userId string) StoreChannel
SetLastViewedAt(channelId string, userId string, newLastViewedAt int64) StoreChannel
IncrementMentionCount(channelId string, userId string) StoreChannel
@@ -135,9 +140,12 @@ type UserStore interface {
UpdateMfaActive(userId string, active bool) StoreChannel
Get(id string) StoreChannel
GetAll() StoreChannel
- GetAllProfiles() StoreChannel
- GetProfiles(teamId string) StoreChannel
- GetDirectProfiles(userId string) StoreChannel
+ InvalidateProfilesInChannelCache(channelId string)
+ GetProfilesInChannel(channelId string, offset int, limit int, allowFromCache bool) StoreChannel
+ GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int) StoreChannel
+ GetProfilesByUsernames(usernames []string, teamId string) StoreChannel
+ GetAllProfiles(offset int, limit int) StoreChannel
+ GetProfiles(teamId string, offset int, limit int) StoreChannel
GetProfileByIds(userId []string) StoreChannel
GetByEmail(email string) StoreChannel
GetByAuth(authData *string, authService string) StoreChannel
@@ -147,7 +155,6 @@ type UserStore interface {
VerifyEmail(userId string) StoreChannel
GetEtagForAllProfiles() StoreChannel
GetEtagForProfiles(teamId string) StoreChannel
- GetEtagForDirectProfiles(userId string) StoreChannel
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
GetTotalUsersCount() StoreChannel
GetSystemAdminProfiles() StoreChannel
@@ -155,6 +162,10 @@ type UserStore interface {
AnalyticsUniqueUserCount(teamId string) StoreChannel
GetUnreadCount(userId string) StoreChannel
GetUnreadCountForChannel(userId string, channelId string) StoreChannel
+ GetRecentlyActiveUsersForTeam(teamId string) StoreChannel
+ Search(teamId string, term string, searchType string) StoreChannel
+ SearchInChannel(channelId string, term string, searchType string) StoreChannel
+ SearchNotInChannel(teamId string, channelId string, term string, searchType string) StoreChannel
}
type SessionStore interface {
@@ -274,6 +285,7 @@ type EmojiStore interface {
type StatusStore interface {
SaveOrUpdate(status *model.Status) StoreChannel
Get(userId string) StoreChannel
+ GetByIds(userIds []string) StoreChannel
GetOnlineAway() StoreChannel
GetOnline() StoreChannel
GetAllFromTeam(teamId string) StoreChannel
diff --git a/web/web_test.go b/web/web_test.go
index 21819191b..66969930f 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -23,7 +23,7 @@ func Setup() {
utils.TranslationsPreInit()
utils.LoadConfig("config.json")
utils.InitTranslations(utils.Cfg.LocalizationSettings)
- api.NewServer()
+ api.NewServer(false)
api.StartServer()
api.InitApi()
InitWeb()
@@ -161,7 +161,7 @@ func TestGetAccessToken(t *testing.T) {
}
}
- if result, err := ApiClient.DoApiGet("/users/profiles/"+teamId+"?access_token="+token, "", ""); err != nil {
+ if result, err := ApiClient.DoApiGet("/teams/"+teamId+"/users/0/100?access_token="+token, "", ""); err != nil {
t.Fatal(err)
} else {
userMap := model.UserMapFromJson(result.Body)
@@ -170,16 +170,16 @@ func TestGetAccessToken(t *testing.T) {
}
}
- if _, err := ApiClient.DoApiGet("/users/profiles/"+teamId, "", ""); err == nil {
+ if _, err := ApiClient.DoApiGet("/teams/"+teamId+"/users/0/100", "", ""); err == nil {
t.Fatal("should have failed - no access token provided")
}
- if _, err := ApiClient.DoApiGet("/users/profiles/"+teamId+"?access_token=junk", "", ""); err == nil {
+ if _, err := ApiClient.DoApiGet("/teams/"+teamId+"/users/0/100?access_token=junk", "", ""); err == nil {
t.Fatal("should have failed - bad access token provided")
}
ApiClient.SetOAuthToken(token)
- if result, err := ApiClient.DoApiGet("/users/profiles/"+teamId, "", ""); err != nil {
+ if result, err := ApiClient.DoApiGet("/teams/"+teamId+"/users/0/100", "", ""); err != nil {
t.Fatal(err)
} else {
userMap := model.UserMapFromJson(result.Body)
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 = '';