summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md26
-rw-r--r--api/post.go66
-rw-r--r--api/post_test.go77
-rw-r--r--api/user.go19
-rw-r--r--api/user_test.go8
-rw-r--r--doc/integrations/Single-Sign-On/Gitlab.md2
-rw-r--r--model/client.go22
-rw-r--r--store/sql_post_store.go98
-rw-r--r--store/sql_post_store_test.go105
-rw-r--r--store/store.go2
-rw-r--r--web/react/components/admin_console/team_users.jsx8
-rw-r--r--web/react/components/channel_invite_modal.jsx2
-rw-r--r--web/react/components/channel_loader.jsx9
-rw-r--r--web/react/components/channel_members.jsx8
-rw-r--r--web/react/components/member_list.jsx6
-rw-r--r--web/react/components/member_list_item.jsx26
-rw-r--r--web/react/components/member_list_team.jsx8
-rw-r--r--web/react/components/more_direct_channels.jsx2
-rw-r--r--web/react/components/navbar_dropdown.jsx18
-rw-r--r--web/react/components/sidebar.jsx82
-rw-r--r--web/react/components/sidebar_header.jsx3
-rw-r--r--web/react/components/signup_team.jsx15
-rw-r--r--web/react/components/tutorial/tutorial_intro_screens.jsx6
-rw-r--r--web/react/stores/channel_store.jsx14
-rw-r--r--web/react/stores/user_store.jsx33
-rw-r--r--web/react/utils/async_client.jsx18
-rw-r--r--web/react/utils/client.jsx5
-rw-r--r--web/react/utils/constants.jsx1
-rw-r--r--web/react/utils/markdown.jsx3
-rw-r--r--web/react/utils/utils.jsx21
-rw-r--r--web/sass-files/sass/partials/_modal.scss22
-rw-r--r--web/sass-files/sass/partials/_responsive.scss2
-rw-r--r--web/sass-files/sass/partials/_signup.scss55
-rw-r--r--web/templates/channel.html7
34 files changed, 615 insertions, 184 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0bf805e7d..3ac8a3261 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -64,6 +64,32 @@ System Console
- IE 11 new minimum version for IE, since IE 10 share fell below 5% on desktop
- Safari 8 new minimum version for Safari, since Safari 7 fell below 1% on desktop
+#### Config.json Changes from v1.1 to v1.2
+
+Multiple settings were added to [`config.json`](./config/config.json). These options can be modified in the System Console, or manually updated in the existing config.json file. This is a list of changes and their new default values in a fresh install:
+- Under `TeamSettings` in `config.json`:
+ - Added: `"RestrictTeamNames": true` to control whether team names are restricted
+ - Added: `"EnableTeamListing": false` to control whether teams can be listed on the root page of the site
+- Under `ServiceSettings` in `config.json`
+ - Added: `EnableOutgoingWebhooks": true` to turn on outgoing webhooks
+
+#### Database Changes from v1.1 to v1.2
+
+The following is for informational purposes only, no action needed. Mattermost automatically upgrades database tables from the previous version's schema using only additions. Sessions table is dropped and rebuilt, no team data is affected by this.
+
+##### Channels Table
+1. Renamed `Description` to `Header`
+2. Added `Purpose` column with type `varchar(1024)`
+
+##### Preferences Table
+1. Added `Preferences` Table
+
+##### Teams Table
+1. Added `InviteId` column with type `varchar(32)`
+2. Added `AllowOpenInvite` column with type `tinyint(1)`
+3. Added `AllowTeamListing` column with type `tinyint(1)`
+4. Added `idx_teams_invite_id` index
+
#### Known Issues
- Microsoft Edge does not yet support drag and drop
diff --git a/api/post.go b/api/post.go
index c98bf2d71..31a7ab3b5 100644
--- a/api/post.go
+++ b/api/post.go
@@ -31,6 +31,8 @@ func InitPost(r *mux.Router) {
sr.Handle("/posts/{time:[0-9]+}", ApiUserRequiredActivity(getPostsSince, false)).Methods("GET")
sr.Handle("/post/{post_id:[A-Za-z0-9]+}", ApiUserRequired(getPost)).Methods("GET")
sr.Handle("/post/{post_id:[A-Za-z0-9]+}/delete", ApiUserRequired(deletePost)).Methods("POST")
+ sr.Handle("/post/{post_id:[A-Za-z0-9]+}/before/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsBefore)).Methods("GET")
+ sr.Handle("/post/{post_id:[A-Za-z0-9]+}/after/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsAfter)).Methods("GET")
}
func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -812,6 +814,70 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
+func getPostsBefore(c *Context, w http.ResponseWriter, r *http.Request) {
+ getPostsBeforeOrAfter(c, w, r, true)
+}
+func getPostsAfter(c *Context, w http.ResponseWriter, r *http.Request) {
+ getPostsBeforeOrAfter(c, w, r, false)
+}
+func getPostsBeforeOrAfter(c *Context, w http.ResponseWriter, r *http.Request, before bool) {
+ params := mux.Vars(r)
+
+ id := params["id"]
+ if len(id) != 26 {
+ c.SetInvalidParam("getPostsBeforeOrAfter", "channelId")
+ return
+ }
+
+ postId := params["post_id"]
+ if len(postId) != 26 {
+ c.SetInvalidParam("getPostsBeforeOrAfter", "postId")
+ return
+ }
+
+ numPosts, err := strconv.Atoi(params["num_posts"])
+ if err != nil || numPosts <= 0 {
+ c.SetInvalidParam("getPostsBeforeOrAfter", "numPosts")
+ return
+ }
+
+ offset, err := strconv.Atoi(params["offset"])
+ if err != nil || offset < 0 {
+ c.SetInvalidParam("getPostsBeforeOrAfter", "offset")
+ return
+ }
+
+ cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId)
+ // We can do better than this etag in this situation
+ etagChan := Srv.Store.Post().GetEtag(id)
+
+ if !c.HasPermissionsToChannel(cchan, "getPostsBeforeOrAfter") {
+ return
+ }
+
+ etag := (<-etagChan).Data.(string)
+ if HandleEtag(etag, w, r) {
+ return
+ }
+
+ var pchan store.StoreChannel
+ if before {
+ pchan = Srv.Store.Post().GetPostsBefore(id, postId, numPosts, offset)
+ } else {
+ pchan = Srv.Store.Post().GetPostsAfter(id, postId, numPosts, offset)
+ }
+
+ if result := <-pchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ list := result.Data.(*model.PostList)
+
+ w.Header().Set(model.HEADER_ETAG_SERVER, etag)
+ w.Write([]byte(list.ToJson()))
+ }
+}
+
func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) {
terms := r.FormValue("terms")
diff --git a/api/post_test.go b/api/post_test.go
index e54e9ef0c..3452c9788 100644
--- a/api/post_test.go
+++ b/api/post_test.go
@@ -329,6 +329,83 @@ func TestGetPostsSince(t *testing.T) {
}
}
+func TestGetPostsBeforeAfter(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ time.Sleep(10 * time.Millisecond)
+ post0 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post0 = Client.Must(Client.CreatePost(post0)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post1a1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post1.Id}
+ post1a1 = Client.Must(Client.CreatePost(post1a1)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post3 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post3a1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post3.Id}
+ post3a1 = Client.Must(Client.CreatePost(post3a1)).Data.(*model.Post)
+
+ r1 := Client.Must(Client.GetPostsBefore(channel1.Id, post1a1.Id, 0, 10, "")).Data.(*model.PostList)
+
+ if r1.Order[0] != post1.Id {
+ t.Fatal("wrong order")
+ }
+
+ if r1.Order[1] != post0.Id {
+ t.Fatal("wrong order")
+ }
+
+ if len(r1.Posts) != 2 {
+ t.Fatal("wrong size")
+ }
+
+ r2 := Client.Must(Client.GetPostsAfter(channel1.Id, post3a1.Id, 0, 3, "")).Data.(*model.PostList)
+
+ if len(r2.Posts) != 0 {
+ t.Fatal("should have been empty")
+ }
+
+ post2.Message = "new message"
+ Client.Must(Client.UpdatePost(post2))
+
+ r3 := Client.Must(Client.GetPostsAfter(channel1.Id, post1a1.Id, 0, 2, "")).Data.(*model.PostList)
+
+ if r3.Order[0] != post3.Id {
+ t.Fatal("wrong order")
+ }
+
+ if r3.Order[1] != post2.Id {
+ t.Fatal("wrong order")
+ }
+
+ if len(r3.Order) != 2 {
+ t.Fatal("missing post update")
+ }
+}
+
func TestSearchPosts(t *testing.T) {
Setup()
diff --git a/api/user.go b/api/user.go
index 732c6b9a8..42d3a43e7 100644
--- a/api/user.go
+++ b/api/user.go
@@ -49,7 +49,7 @@ func InitUser(r *mux.Router) {
sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
- sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("GET")
+ sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST")
sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET")
sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}", ApiUserRequired(getUser)).Methods("GET")
@@ -1483,16 +1483,31 @@ func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
+ userIds := model.ArrayFromJson(r.Body)
+ if len(userIds) == 0 {
+ c.SetInvalidParam("getStatuses", "userIds")
+ return
+ }
if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
-
profiles := result.Data.(map[string]*model.User)
statuses := map[string]string{}
for _, profile := range profiles {
+ found := false
+ for _, uid := range userIds {
+ if uid == profile.Id {
+ found = true
+ }
+ }
+
+ if !found {
+ continue
+ }
+
if profile.IsOffline() {
statuses[profile.Id] = model.USER_OFFLINE
} else if profile.IsAway() {
diff --git a/api/user_test.go b/api/user_test.go
index 0ad3541bc..f067182cb 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -1020,9 +1020,15 @@ func TestStatuses(t *testing.T) {
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
+ user2 := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(ruser2.Id))
+
Client.LoginByEmail(team.Name, user.Email, user.Password)
- r1, err := Client.GetStatuses()
+ userIds := []string{ruser2.Id}
+
+ r1, err := Client.GetStatuses(userIds)
if err != nil {
t.Fatal(err)
}
diff --git a/doc/integrations/Single-Sign-On/Gitlab.md b/doc/integrations/Single-Sign-On/Gitlab.md
index 7939c47fb..1242fd13e 100644
--- a/doc/integrations/Single-Sign-On/Gitlab.md
+++ b/doc/integrations/Single-Sign-On/Gitlab.md
@@ -11,7 +11,7 @@ Follow these steps to configure Mattermost to use GitLab as a single-sign-on (SS
3. Submit the application and copy the given _Id_ and _Secret_ into the appropriate _SSOSettings_ fields in config/config.json
-4. Also in config/config.json, set _Allow_ to `true` for the _gitlab_ section, leave _Scope_ blank and use the following for the endpoints:
+4. Also in config/config.json, set _Enable_ to `true` for the _gitlab_ section, leave _Scope_ blank and use the following for the endpoints:
* _AuthEndpoint_: `https://<your-gitlab-url>/oauth/authorize` (example https://example.com/oauth/authorize)
* _TokenEndpoint_: `https://<your-gitlab-url>/oauth/token`
* _UserApiEndpoint_: `https://<your-gitlab-url>/api/v3/user`
diff --git a/model/client.go b/model/client.go
index 19183098e..ac85b0d1c 100644
--- a/model/client.go
+++ b/model/client.go
@@ -618,6 +618,24 @@ func (c *Client) GetPostsSince(channelId string, time int64) (*Result, *AppError
}
}
+func (c *Client) GetPostsBefore(channelId string, postid string, offset int, limit int, etag string) (*Result, *AppError) {
+ if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/post/%v/before/%v/%v", channelId, postid, offset, limit), "", etag); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetPostsAfter(channelId string, postid string, offset int, limit int, etag string) (*Result, *AppError) {
+ if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/post/%v/after/%v/%v", channelId, postid, offset, limit), "", etag); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil {
return nil, err
@@ -783,8 +801,8 @@ func (c *Client) ResetPassword(data map[string]string) (*Result, *AppError) {
}
}
-func (c *Client) GetStatuses() (*Result, *AppError) {
- if r, err := c.DoApiGet("/users/status", "", ""); err != nil {
+func (c *Client) GetStatuses(data []string) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/users/status", ArrayToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index de8c4f356..fdae20f60 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -332,6 +332,104 @@ func (s SqlPostStore) GetPostsSince(channelId string, time int64) StoreChannel {
return storeChannel
}
+func (s SqlPostStore) GetPostsBefore(channelId string, postId string, numPosts int, offset int) StoreChannel {
+ return s.getPostsAround(channelId, postId, numPosts, offset, true)
+}
+
+func (s SqlPostStore) GetPostsAfter(channelId string, postId string, numPosts int, offset int) StoreChannel {
+ return s.getPostsAround(channelId, postId, numPosts, offset, false)
+}
+
+func (s SqlPostStore) getPostsAround(channelId string, postId string, numPosts int, offset int, before bool) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var direction string
+ var sort string
+ if before {
+ direction = "<"
+ sort = "DESC"
+ } else {
+ direction = ">"
+ sort = "ASC"
+ }
+
+ var posts []*model.Post
+ var parents []*model.Post
+ _, err1 := s.GetReplica().Select(&posts,
+ `(SELECT
+ *
+ FROM
+ Posts
+ WHERE
+ (CreateAt `+direction+` (SELECT CreateAt FROM Posts WHERE Id = :PostId)
+ AND ChannelId = :ChannelId
+ AND DeleteAt = 0)
+ ORDER BY CreateAt `+sort+`
+ LIMIT :NumPosts
+ OFFSET :Offset)`,
+ map[string]interface{}{"ChannelId": channelId, "PostId": postId, "NumPosts": numPosts, "Offset": offset})
+ _, err2 := s.GetReplica().Select(&parents,
+ `(SELECT
+ *
+ FROM
+ Posts
+ WHERE
+ Id
+ IN
+ (SELECT * FROM (SELECT
+ RootId
+ FROM
+ Posts
+ WHERE
+ (CreateAt `+direction+` (SELECT CreateAt FROM Posts WHERE Id = :PostId)
+ AND ChannelId = :ChannelId
+ AND DeleteAt = 0)
+ ORDER BY CreateAt `+sort+`
+ LIMIT :NumPosts
+ OFFSET :Offset)
+ temp_tab))
+ ORDER BY CreateAt DESC`,
+ map[string]interface{}{"ChannelId": channelId, "PostId": postId, "NumPosts": numPosts, "Offset": offset})
+
+ if err1 != nil {
+ result.Err = model.NewAppError("SqlPostStore.GetPostContext", "We couldn't get the posts for the channel", "channelId="+channelId+err1.Error())
+ } else if err2 != nil {
+ result.Err = model.NewAppError("SqlPostStore.GetPostContext", "We couldn't get the parent posts for the channel", "channelId="+channelId+err2.Error())
+ } else {
+
+ list := &model.PostList{Order: make([]string, 0, len(posts))}
+
+ // We need to flip the order if we selected backwards
+ if before {
+ for _, p := range posts {
+ list.AddPost(p)
+ list.AddOrder(p.Id)
+ }
+ } else {
+ l := len(posts)
+ for i := range posts {
+ list.AddPost(posts[l-i-1])
+ list.AddOrder(posts[l-i-1].Id)
+ }
+ }
+
+ for _, p := range parents {
+ list.AddPost(p)
+ }
+
+ result.Data = list
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlPostStore) getRootPosts(channelId string, offset int, limit int) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index 0980b1a11..fe7195a54 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -383,6 +383,111 @@ func TestPostStoreGetPostsWtihDetails(t *testing.T) {
}
}
+func TestPostStoreGetPostsBeforeAfter(t *testing.T) {
+ Setup()
+ o0 := &model.Post{}
+ o0.ChannelId = model.NewId()
+ o0.UserId = model.NewId()
+ o0.Message = "a" + model.NewId() + "b"
+ o0 = (<-store.Post().Save(o0)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o1 := &model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "b"
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o2 := &model.Post{}
+ o2.ChannelId = o1.ChannelId
+ o2.UserId = model.NewId()
+ o2.Message = "a" + model.NewId() + "b"
+ o2.ParentId = o1.Id
+ o2.RootId = o1.Id
+ o2 = (<-store.Post().Save(o2)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o2a := &model.Post{}
+ o2a.ChannelId = o1.ChannelId
+ o2a.UserId = model.NewId()
+ o2a.Message = "a" + model.NewId() + "b"
+ o2a.ParentId = o1.Id
+ o2a.RootId = o1.Id
+ o2a = (<-store.Post().Save(o2a)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o3 := &model.Post{}
+ o3.ChannelId = o1.ChannelId
+ o3.UserId = model.NewId()
+ o3.Message = "a" + model.NewId() + "b"
+ o3.ParentId = o1.Id
+ o3.RootId = o1.Id
+ o3 = (<-store.Post().Save(o3)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o4 := &model.Post{}
+ o4.ChannelId = o1.ChannelId
+ o4.UserId = model.NewId()
+ o4.Message = "a" + model.NewId() + "b"
+ o4 = (<-store.Post().Save(o4)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o5 := &model.Post{}
+ o5.ChannelId = o1.ChannelId
+ o5.UserId = model.NewId()
+ o5.Message = "a" + model.NewId() + "b"
+ o5.ParentId = o4.Id
+ o5.RootId = o4.Id
+ o5 = (<-store.Post().Save(o5)).Data.(*model.Post)
+
+ r1 := (<-store.Post().GetPostsBefore(o1.ChannelId, o1.Id, 4, 0)).Data.(*model.PostList)
+
+ if len(r1.Posts) != 0 {
+ t.Fatal("Wrong size")
+ }
+
+ r2 := (<-store.Post().GetPostsAfter(o1.ChannelId, o1.Id, 4, 0)).Data.(*model.PostList)
+
+ if r2.Order[0] != o4.Id {
+ t.Fatal("invalid order")
+ }
+
+ if r2.Order[1] != o3.Id {
+ t.Fatal("invalid order")
+ }
+
+ if r2.Order[2] != o2a.Id {
+ t.Fatal("invalid order")
+ }
+
+ if r2.Order[3] != o2.Id {
+ t.Fatal("invalid order")
+ }
+
+ if len(r2.Posts) != 5 {
+ t.Fatal("wrong size")
+ }
+
+ r3 := (<-store.Post().GetPostsBefore(o3.ChannelId, o3.Id, 2, 0)).Data.(*model.PostList)
+
+ if r3.Order[0] != o2a.Id {
+ t.Fatal("invalid order")
+ }
+
+ if r3.Order[1] != o2.Id {
+ t.Fatal("invalid order")
+ }
+
+ if len(r3.Posts) != 3 {
+ t.Fatal("wrong size")
+ }
+
+ if r3.Posts[o1.Id].Message != o1.Message {
+ t.Fatal("Missing parent")
+ }
+}
+
func TestPostStoreGetPostsSince(t *testing.T) {
Setup()
o0 := &model.Post{}
diff --git a/store/store.go b/store/store.go
index 53a6e053b..ce4d90883 100644
--- a/store/store.go
+++ b/store/store.go
@@ -86,6 +86,8 @@ type PostStore interface {
Get(id string) StoreChannel
Delete(postId string, time int64) StoreChannel
GetPosts(channelId string, offset int, limit int) StoreChannel
+ GetPostsBefore(channelId string, postId string, numPosts int, offset int) StoreChannel
+ GetPostsAfter(channelId string, postId string, numPosts int, offset int) StoreChannel
GetPostsSince(channelId string, time int64) StoreChannel
GetEtag(channelId string) StoreChannel
Search(teamId string, userId string, params *model.SearchParams) StoreChannel
diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx
index b44aba56e..7161139e6 100644
--- a/web/react/components/admin_console/team_users.jsx
+++ b/web/react/components/admin_console/team_users.jsx
@@ -147,9 +147,11 @@ export default class UserList extends React.Component {
className='form-horizontal'
role='form'
>
- <div className='member-list-holder'>
- {memberList}
- </div>
+ <table className='table more-table member-list-holder'>
+ <tbody>
+ {memberList}
+ </tbody>
+ </table>
</form>
<ResetPasswordModal
user={this.state.user}
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 7c7770095..e90d1a666 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -139,7 +139,7 @@ export default class ChannelInviteModal extends React.Component {
return (
<div
- className='modal fade'
+ className='modal fade more-modal'
id='channel_invite'
tabIndex='-1'
role='dialog'
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index 55b4a55c0..4fc115a92 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -30,19 +30,14 @@ export default class ChannelLoader extends React.Component {
AsyncClient.getChannels(true, true);
AsyncClient.getChannelExtraInfo(true);
AsyncClient.findTeams();
- AsyncClient.getStatuses();
AsyncClient.getMyTeam();
+ setTimeout(() => AsyncClient.getStatuses(), 3000); // temporary until statuses are reworked a bit
/* Perform pending post clean-up */
PostStore.clearPendingPosts();
/* Set up interval functions */
- this.intervalId = setInterval(
- function pollStatuses() {
- AsyncClient.getStatuses();
- },
- 30000
- );
+ this.intervalId = setInterval(() => AsyncClient.getStatuses(), 30000);
/* Device tracking setup */
var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
diff --git a/web/react/components/channel_members.jsx b/web/react/components/channel_members.jsx
index 86cc2464d..d0ea7278b 100644
--- a/web/react/components/channel_members.jsx
+++ b/web/react/components/channel_members.jsx
@@ -150,7 +150,7 @@ export default class ChannelMembers extends React.Component {
return (
<div
- className='modal fade'
+ className='modal fade more-modal'
ref='modal'
id='channel_members'
tabIndex='-1'
@@ -181,11 +181,7 @@ export default class ChannelMembers extends React.Component {
ref='modalBody'
className='modal-body'
>
- <div className='col-sm-12'>
- <div className='team-member-list'>
- {memberList}
- </div>
- </div>
+ {memberList}
</div>
<div className='modal-footer'>
<button
diff --git a/web/react/components/member_list.jsx b/web/react/components/member_list.jsx
index fe744760f..70eb0a500 100644
--- a/web/react/components/member_list.jsx
+++ b/web/react/components/member_list.jsx
@@ -21,7 +21,8 @@ export default class MemberList extends React.Component {
}
return (
- <div className='member-list-holder'>
+ <table className='table more-table member-list-holder'>
+ <tbody>
{members.map(function mymembers(member) {
return (
<MemberListItem
@@ -34,8 +35,9 @@ export default class MemberList extends React.Component {
/>
);
}, this)}
+ </tbody>
{message}
- </div>
+ </table>
);
}
}
diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx
index 8ed94680e..8251d67bc 100644
--- a/web/react/components/member_list_item.jsx
+++ b/web/react/components/member_list_item.jsx
@@ -37,7 +37,7 @@ export default class MemberListItem extends React.Component {
invite = (
<a
onClick={this.handleInvite}
- className='btn btn-sm btn-primary member-invite'
+ className='btn btn-sm btn-primary'
>
<i className='glyphicon glyphicon-envelope'/>
{' Add'}
@@ -102,17 +102,19 @@ export default class MemberListItem extends React.Component {
}
return (
- <div className='row member-div'>
- <img
- className='post-profile-img pull-left'
- src={'/api/v1/users/' + member.id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
- height='36'
- width='36'
- />
- <span className='member-name'>{member.username}</span>
- <span className='member-email'>{member.email}</span>
- {invite}
- </div>
+ <tr>
+ <td className='direct-channel'>
+ <img
+ className='profile-img pull-left'
+ src={'/api/v1/users/' + member.id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
+ height='36'
+ width='36'
+ />
+ <div className='member-name'>{member.username}</div>
+ <div className='member-description'>{member.email}</div>
+ </td>
+ <td className='td--action lg'>{invite}</td>
+ </tr>
);
}
}
diff --git a/web/react/components/member_list_team.jsx b/web/react/components/member_list_team.jsx
index 5ca40a39d..cb2d0660b 100644
--- a/web/react/components/member_list_team.jsx
+++ b/web/react/components/member_list_team.jsx
@@ -15,9 +15,11 @@ export default class MemberListTeam extends React.Component {
}, this);
return (
- <div className='member-list-holder'>
- {memberList}
- </div>
+ <table className='table more-table member-list-holder'>
+ <tbody>
+ {memberList}
+ </tbody>
+ </table>
);
}
}
diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index b0232fc08..40deb37f2 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -206,7 +206,7 @@ export default class MoreDirectChannels extends React.Component {
return (
<Modal
- className='modal-direct-channels'
+ dialogClassName='more-modal'
show={this.props.show}
onHide={this.handleHide}
>
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index f43bdffdf..029b9c137 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -11,23 +11,15 @@ var AboutBuildModal = require('./about_build_modal.jsx');
var Constants = require('../utils/constants.jsx');
function getStateFromStores() {
- let teams = [];
- let teamsObject = UserStore.getTeams();
- for (let teamId in teamsObject) {
+ const teams = [];
+ const teamsObject = UserStore.getTeams();
+ for (const teamId in teamsObject) {
if (teamsObject.hasOwnProperty(teamId)) {
teams.push(teamsObject[teamId]);
}
}
- teams.sort(function sortByDisplayName(teamA, teamB) {
- let teamADisplayName = teamA.display_name.toLowerCase();
- let teamBDisplayName = teamB.display_name.toLowerCase();
- if (teamADisplayName < teamBDisplayName) {
- return -1;
- } else if (teamADisplayName > teamBDisplayName) {
- return 1;
- }
- return 0;
- });
+
+ teams.sort(Utils.sortByDisplayName);
return {teams};
}
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index c47919885..aab9919a4 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -100,74 +100,56 @@ export default class Sidebar extends React.Component {
}
getStateFromStores() {
const members = ChannelStore.getAllMembers();
- var teamMemberMap = UserStore.getActiveOnlyProfiles();
- var currentId = ChannelStore.getCurrentId();
- const currentUserId = UserStore.getCurrentId();
+ const currentChannelId = ChannelStore.getCurrentId();
- var teammates = [];
- for (var id in teamMemberMap) {
- if (id === currentUserId) {
- continue;
- }
- teammates.push(teamMemberMap[id]);
- }
+ const channels = Object.assign([], ChannelStore.getAll());
+ const publicChannels = channels.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
+ const privateChannels = channels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
+ const directChannels = channels.filter((channel) => channel.type === Constants.DM_CHANNEL);
const preferences = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
var visibleDirectChannels = [];
- var hiddenDirectChannelCount = 0;
- for (var i = 0; i < teammates.length; i++) {
- const teammate = teammates[i];
-
- if (teammate.id === currentUserId) {
+ for (var i = 0; i < directChannels.length; i++) {
+ const dm = directChannels[i];
+ const teammate = Utils.getDirectTeammate(dm.id);
+ if (!teammate) {
continue;
}
- const channelName = Utils.getDirectChannelName(currentUserId, teammate.id);
+ const member = members[dm.id];
+ const msgCount = dm.total_msg_count - member.msg_count;
- let forceShow = false;
- let channel = ChannelStore.getByName(channelName);
+ // always show a channel if either it is the current one or if it is unread, but it is not currently being left
+ const forceShow = (currentChannelId === dm.id || msgCount > 0) && !this.isLeaving.get(dm.id);
+ const preferenceShow = preferences.some((preference) => (preference.name === teammate.id && preference.value !== 'false'));
- if (channel) {
- const member = members[channel.id];
- const msgCount = channel.total_msg_count - member.msg_count;
+ if (preferenceShow || forceShow) {
+ dm.display_name = Utils.displayUsername(teammate.id);
+ dm.teammate_id = teammate.id;
+ dm.status = UserStore.getStatus(teammate.id);
- // always show a channel if either it is the current one or if it is unread, but it is not currently being left
- forceShow = (currentId === channel.id || msgCount > 0) && !this.isLeaving.get(channel.id);
- } else {
- channel = {};
- channel.fake = true;
- channel.name = channelName;
- channel.last_post_at = 0;
- channel.total_msg_count = 0;
- channel.type = 'D';
- }
+ visibleDirectChannels.push(dm);
- channel.display_name = Utils.displayUsername(teammate.id);
- channel.teammate_id = teammate.id;
- channel.status = UserStore.getStatus(teammate.id);
-
- if (preferences.some((preference) => (preference.name === teammate.id && preference.value !== 'false'))) {
- visibleDirectChannels.push(channel);
- } else if (forceShow) {
- // make sure that unread direct channels are visible
- const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true');
- AsyncClient.savePreferences([preference]);
-
- visibleDirectChannels.push(channel);
- } else {
- hiddenDirectChannelCount += 1;
+ if (forceShow && !preferenceShow) {
+ // make sure that unread direct channels are visible
+ const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true');
+ AsyncClient.savePreferences([preference]);
+ }
}
}
+ const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList().length - visibleDirectChannels.length;
+
visibleDirectChannels.sort(this.sortChannelsByDisplayName);
const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
return {
- activeId: currentId,
- channels: ChannelStore.getAll(),
+ activeId: currentChannelId,
members,
+ publicChannels,
+ privateChannels,
visibleDirectChannels,
hiddenDirectChannelCount,
showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.CHANNEL_POPOVER
@@ -534,11 +516,9 @@ export default class Sidebar extends React.Component {
this.lastUnreadChannel = null;
// create elements for all 3 types of channels
- const publicChannels = this.state.channels.filter((channel) => channel.type === 'O');
- const publicChannelItems = publicChannels.map(this.createChannelElement);
+ const publicChannelItems = this.state.publicChannels.map(this.createChannelElement);
- const privateChannels = this.state.channels.filter((channel) => channel.type === 'P');
- const privateChannelItems = privateChannels.map(this.createChannelElement);
+ const privateChannelItems = this.state.privateChannels.map(this.createChannelElement);
const directMessageItems = this.state.visibleDirectChannels.map((channel, index, arr) => {
return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index 3f777d93c..46730e1e6 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -62,6 +62,9 @@ export default class SidebarHeader extends React.Component {
<p>
{'Team administrators can also access their '}<strong>{'Team Settings'}</strong>{' from this menu.'}
</p>
+ <p>
+ {'System administrators will find a '}<strong>{'System Console'}</strong>{' option to administrate the entire system.'}
+ </p>
</div>
);
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx
index 37760a2a2..516765a3f 100644
--- a/web/react/components/signup_team.jsx
+++ b/web/react/components/signup_team.jsx
@@ -46,7 +46,7 @@ export default class TeamSignUp extends React.Component {
} else {
teamListing = (
<div>
- <h3>{'Choose a Team'}</h3>
+ <h4>{'Choose a Team'}</h4>
<div className='signup-team-all'>
{
this.props.teams.map((team) => {
@@ -58,19 +58,18 @@ export default class TeamSignUp extends React.Component {
<a
href={'/' + team.name}
>
- <div className='signup-team-dir__group'>
- <span className='signup-team-dir__name'>{team.display_name}</span>
- <span
- className='glyphicon glyphicon-menu-right right signup-team-dir__arrow'
- aria-hidden='true'
- />
- </div>
+ <span className='signup-team-dir__name'>{team.display_name}</span>
+ <span
+ className='glyphicon glyphicon-menu-right right signup-team-dir__arrow'
+ aria-hidden='true'
+ />
</a>
</div>
);
})
}
</div>
+ <h4>{'Or Create a Team'}</h4>
</div>
);
}
diff --git a/web/react/components/tutorial/tutorial_intro_screens.jsx b/web/react/components/tutorial/tutorial_intro_screens.jsx
index 8fc38b1e3..a99e9fe28 100644
--- a/web/react/components/tutorial/tutorial_intro_screens.jsx
+++ b/web/react/components/tutorial/tutorial_intro_screens.jsx
@@ -53,7 +53,7 @@ export default class TutorialIntroScreens extends React.Component {
<div>
<h3>{'Welcome to:'}</h3>
<h1>{'Mattermost'}</h1>
- <p>{'Your team communications all in one place, instantly searchable and available anywhere.'}</p>
+ <p>{'Your team communication all in one place, instantly searchable and available anywhere.'}</p>
<p>{'Keep your team connected to help them achieve what matters most.'}</p>
<div className='tutorial__circles'>
<div className='circle active'/>
@@ -68,7 +68,7 @@ export default class TutorialIntroScreens extends React.Component {
<div>
<h3>{'How Mattermost works:'}</h3>
<p>{'Communication happens in public discussion channels, private groups and direct messages.'}</p>
- <p>{'Everything is archived and searchable from any web-enabled laptop, tablet or phone.'}</p>
+ <p>{'Everything is archived and searchable from any web-enabled desktop, laptop or phone.'}</p>
<div className='tutorial__circles'>
<div className='circle'/>
<div className='circle active'/>
@@ -123,7 +123,7 @@ export default class TutorialIntroScreens extends React.Component {
</a>
{'.'}
</p>
- {'Click “Next” to enter Town Square. This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'}
+ {'Click “Next” to enter Town Square. This is the first channel teammates see when they sign up. Use it for posting updates everyone needs to know.'}
<div className='tutorial__circles'>
<div className='circle'/>
<div className='circle'/>
diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx
index d1f548d50..cc0d0d14b 100644
--- a/web/react/stores/channel_store.jsx
+++ b/web/react/stores/channel_store.jsx
@@ -4,6 +4,7 @@
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
+var Utils;
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -183,16 +184,11 @@ class ChannelStoreClass extends EventEmitter {
channels.push(channel);
}
- channels.sort(function chanSort(a, b) {
- if (a.display_name.toLowerCase() < b.display_name.toLowerCase()) {
- return -1;
- }
- if (a.display_name.toLowerCase() > b.display_name.toLowerCase()) {
- return 1;
- }
- return 0;
- });
+ if (!Utils) {
+ Utils = require('../utils/utils.jsx'); //eslint-disable-line global-require
+ }
+ channels.sort(Utils.sortByDisplayName);
this.pStoreChannels(channels);
}
pStoreChannels(channels) {
diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx
index ce80c5ec9..aedb3dc09 100644
--- a/web/react/stores/user_store.jsx
+++ b/web/react/stores/user_store.jsx
@@ -204,12 +204,13 @@ class UserStoreClass extends EventEmitter {
}
getActiveOnlyProfiles() {
- var active = {};
- var current = this.getProfiles();
+ const active = {};
+ const profiles = this.getProfiles();
+ const currentId = this.getCurrentId();
- for (var key in current) {
- if (current[key].delete_at === 0) {
- active[key] = current[key];
+ for (var key in profiles) {
+ if (profiles[key].delete_at === 0 && profiles[key].id !== currentId) {
+ active[key] = profiles[key];
}
}
@@ -219,9 +220,10 @@ class UserStoreClass extends EventEmitter {
getActiveOnlyProfileList() {
const profileMap = this.getActiveOnlyProfiles();
const profiles = [];
+ const currentId = this.getCurrentId();
for (const id in profileMap) {
- if (profileMap.hasOwnProperty(id)) {
+ if (profileMap.hasOwnProperty(id) && id !== currentId) {
profiles.push(profileMap[id]);
}
}
@@ -235,6 +237,14 @@ class UserStoreClass extends EventEmitter {
BrowserStore.setItem('profiles', ps);
}
+ saveProfiles(profiles) {
+ const currentId = this.getCurrentId();
+ if (currentId in profiles) {
+ delete profiles[currentId];
+ }
+ BrowserStore.setItem('profiles', profiles);
+ }
+
setSessions(sessions) {
BrowserStore.setItem('sessions', sessions);
}
@@ -320,15 +330,8 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => {
switch (action.type) {
case ActionTypes.RECIEVED_PROFILES:
- for (var id in action.profiles) {
- // profiles can have incomplete data, so don't overwrite current user
- if (id === UserStore.getCurrentId()) {
- continue;
- }
- var profile = action.profiles[id];
- UserStore.saveProfile(profile);
- UserStore.emitChange(profile.id);
- }
+ UserStore.saveProfiles(action.profiles);
+ UserStore.emitChange();
break;
case ActionTypes.RECIEVED_ME:
UserStore.setCurrentUser(action.me);
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 75dd35e3f..205c7461c 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -588,13 +588,23 @@ export function getMe() {
}
export function getStatuses() {
- if (isCallInProgress('getStatuses')) {
+ const directChannels = ChannelStore.getAll().filter((channel) => channel.type === Constants.DM_CHANNEL);
+
+ const teammateIds = [];
+ for (var i = 0; i < directChannels.length; i++) {
+ const teammate = utils.getDirectTeammate(directChannels[i].id);
+ if (teammate) {
+ teammateIds.push(teammate.id);
+ }
+ }
+
+ if (isCallInProgress('getStatuses') || teammateIds.length === 0) {
return;
}
callTracker.getStatuses = utils.getTimestamp();
- client.getStatuses(
- function getStatusesSuccess(data, textStatus, xhr) {
+ client.getStatuses(teammateIds,
+ (data, textStatus, xhr) => {
callTracker.getStatuses = 0;
if (xhr.status === 304 || !data) {
@@ -606,7 +616,7 @@ export function getStatuses() {
statuses: data
});
},
- function getStatusesFailure(err) {
+ (err) => {
callTracker.getStatuses = 0;
dispatchError(err, 'getStatuses');
}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 7ce1346f9..003e24d33 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -1069,12 +1069,13 @@ export function exportTeam(success, error) {
});
}
-export function getStatuses(success, error) {
+export function getStatuses(ids, success, error) {
$.ajax({
url: '/api/v1/users/status',
dataType: 'json',
contentType: 'application/json',
- type: 'GET',
+ type: 'POST',
+ data: JSON.stringify(ids),
success,
error: function onError(xhr, status, err) {
var e = handleError('getStatuses', xhr, status, err);
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index fd64b1554..39be577df 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -127,6 +127,7 @@ module.exports = {
MAX_DMS: 20,
DM_CHANNEL: 'D',
OPEN_CHANNEL: 'O',
+ PRIVATE_CHANNEL: 'P',
INVITE_TEAM: 'I',
OPEN_TEAM: 'O',
MAX_POST_LEN: 4000,
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index f9416b2de..3ef09211f 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -208,7 +208,8 @@ export function format(text, options) {
const markdownOptions = {
renderer: new MattermostMarkdownRenderer(null, options),
sanitize: true,
- gfm: true
+ gfm: true,
+ tables: true
};
const tokens = marked.lexer(text, markdownOptions);
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index c82bd1065..e8d34dccd 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -1104,3 +1104,24 @@ export function openDirectChannelToUser(user, successCb, errorCb) {
);
}
}
+
+// Use when sorting multiple channels or teams by their `display_name` field
+export function sortByDisplayName(a, b) {
+ let aDisplayName = '';
+ let bDisplayName = '';
+
+ if (a && a.display_name) {
+ aDisplayName = a.display_name.toLowerCase();
+ }
+ if (b && b.display_name) {
+ bDisplayName = b.display_name.toLowerCase();
+ }
+
+ if (aDisplayName < bDisplayName) {
+ return -1;
+ }
+ if (aDisplayName > bDisplayName) {
+ return 1;
+ }
+ return 0;
+}
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index 9314b4980..0333e0c65 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -149,6 +149,12 @@
@include opacity(0.8);
margin: 5px 0;
}
+ .profile-img {
+ -moz-border-radius: 50px;
+ -webkit-border-radius: 50px;
+ border-radius: 50px;
+ margin-right: 8px;
+ }
.more-name {
font-weight: 600;
font-size: 0.95em;
@@ -230,8 +236,8 @@
position: relative;
&:hover .file-playback-controls.stop {
- @include opacity(1);
- }
+ @include opacity(1);
+ }
}
img {
max-width: 100%;
@@ -347,7 +353,7 @@
}
}
-.modal-direct-channels {
+.more-modal {
.user-list {
margin-top: 10px;
@@ -362,11 +368,12 @@
}
.modal-body {
- padding: 20px 0 0;
+ padding: 10px 0 0;
@include clearfix;
}
.filter-row {
+ margin-top: 10px;
padding: 0 15px;
}
@@ -379,11 +386,4 @@
.more-purpose {
@include opacity(0.7);
}
-
- .profile-img {
- -moz-border-radius: 50px;
- -webkit-border-radius: 50px;
- border-radius: 50px;
- margin-right: 8px;
- }
}
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 8f25f58cd..339412b45 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -100,7 +100,7 @@
.date-separator, .new-separator {
&.hovered--comment {
&:before, &:after {
- background: none;
+ background: none !important;
}
}
}
diff --git a/web/sass-files/sass/partials/_signup.scss b/web/sass-files/sass/partials/_signup.scss
index 84f9892f4..b9486e254 100644
--- a/web/sass-files/sass/partials/_signup.scss
+++ b/web/sass-files/sass/partials/_signup.scss
@@ -314,32 +314,39 @@
}
.signup-team-all {
- width: 280px;
- box-shadow: 3px 3px 1px #d5d5d5;
- margin: 0px 0px 50px 5px;
-}
-
-.signup-team-dir {
- background: #fafafa;
- border-bottom: 1px solid #d5d5d5;
-}
-
-.signup-team-dir__group {
- padding: 15px 10px 15px 10px;
-}
-
-.signup-team-dir__name {
- line-height: 1.3 !important;
- font-size: 1.5em !important;
- font-weight: 300 !important;
+ margin: 0 0 20px;
+ border: 1px solid #ddd;
+ @include border-radius(2px);
+ .signup-team-dir {
+ background: #fafafa;
+ border-top: 1px solid #d5d5d5;
+ &:first-child {
+ border: none;
+ }
+ a {
+ color: inherit;
+ display: block;
+ padding: 0 15px;
+ line-height: 3.5em;
+ height: 3.5em;
+ font-size: 1.1em;
+ }
+ }
+ .signup-team-dir__name {
+ white-space: nowrap;
+ float: left;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 90%;
+ }
+ .signup-team-dir__arrow {
+ float: right;
+ font-size: 0.9em;
+ color: #999;
+ line-height: 3.5em;
+ }
}
-.signup-team-dir__arrow {
- float: right;
- line-height: 1.3 !important;
- font-size: 1.5em !important;
- font-weight: 300 !important;
-}
.authorize-box {
margin: 100px auto;
diff --git a/web/templates/channel.html b/web/templates/channel.html
index 63fe38587..aabd01ecb 100644
--- a/web/templates/channel.html
+++ b/web/templates/channel.html
@@ -37,7 +37,12 @@
<script>
window.setup_channel_page({{ .Props }});
$('body').tooltip( {selector: '[data-toggle=tooltip]'} );
- $('.modal-body').css('max-height', $(window).height() - 200);
+ if($(window).height() > 1200){
+ $('.modal-body').css('max-height', 1000);
+ }
+ else {
+ $('.modal-body').css('max-height', $(window).height() - 200);
+ }
$('.modal-body').perfectScrollbar();
</script>
</body>