diff options
33 files changed, 839 insertions, 121 deletions
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/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/doc/usage/Markdown.md b/doc/usage/Markdown.md index 9e2342a0b..055f47619 100644 --- a/doc/usage/Markdown.md +++ b/doc/usage/Markdown.md @@ -26,7 +26,7 @@ Renders as: code block ``` -Create in-line monospaced font by surrounding it with back spaces. +Create in-line monospaced font by surrounding it with backticks. ``` `monospace` ``` diff --git a/model/client.go b/model/client.go index 19183098e..a15cb5eaf 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 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/sql_team_store.go b/store/sql_team_store.go index dfc07d3d8..e0f95fa7e 100644 --- a/store/sql_team_store.go +++ b/store/sql_team_store.go @@ -32,7 +32,7 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore { func (s SqlTeamStore) UpgradeSchemaIfNeeded() { // REMOVE AFTER 1.2 SHIP see PLT-828 s.RemoveColumnIfExists("Teams", "AllowValet") - s.CreateColumnIfNotExists("Teams", "InviteId", "varchar(26)", "varchar(26)", "") + s.CreateColumnIfNotExists("Teams", "InviteId", "varchar(32)", "varchar(32)", "") s.CreateColumnIfNotExists("Teams", "AllowOpenInvite", "tinyint(1)", "boolean", "0") s.CreateColumnIfNotExists("Teams", "AllowTeamListing", "tinyint(1)", "boolean", "0") } 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/utils/textgeneration.go b/utils/textgeneration.go index 420d6fb29..fd0284a2e 100644 --- a/utils/textgeneration.go +++ b/utils/textgeneration.go @@ -15,6 +15,241 @@ const ( // Strings that should pass as acceptable posts var FUZZY_STRINGS_POSTS = []string{ + `**[1] - [Markdown Tests]** +_italics_ +more _italics_ +**bold** +more **bold** +**_bold-italic_** +more **_bold-italic_*8 +~~strikethrough~~ +more ~~strikethrough~~ +` + "```" + ` +multi-line code block<enter here> +multi-line code block +emoji that should not render in code block: :ice_cream: +` + "```" + ` +` + "`monospace`" + ` +[Link to Mattermost](www.mattermost.com) +Inline Image with link, alt text, and hover text: ![Build Status](https://travis-ci.org/mattermost/platform.svg?branch=master)](https://travis-ci.org/mattermost/platform) + +Three types of lines: +*** +___ +--- +`, + + ` **[2] - **[More Markdown Tests]** +> i am a blockquote! + +> i am a 2nd multiline +> quote. +i am text right after a multiline quote, but not in the quote + +* list item +* another list item + * indented list item + +1. numbered list, item number 1 +2. item number two + +`, + + ` **[3]** - **[More Markdown Tests]** + +Table + +| Left-Aligned | Center Aligned | Right Aligned | +| :------------ |:---------------:| -----:| +| Left column 1 | this text | $100 | +| Left column 2 | is | $10 | +| Left column 3 | centered | $1 | + +Ugly table + +Markdown | Less | Pretty +--- | --- | --- +*Still* | ~~renders~~ | **nicely** +1 | 2 | 3 + +# Large heading +## Smaller heading +### Even smaller heading +# Large heading +## Smaller heading +### Even smaller heading + +`, + + /* `**[2] [Username Linking Test]** + I saw @alice--and I said "Hi @alice!" then "What's up @alice?" and then @alice, was totally @alice; she just "@alice"'d me and walked on by. That's @alice... + @alice‽‽ + `, + + `**[3] [Mention Highlighting Test]** + `,*/ + + `**[4] [Emoji Display Test 1]** +:+1: :-1: :100: :1234: :8ball: :a: :ab: :abc: :abcd: :accept: +:aerial_tramway: :airplane: :alarm_clock: :ambulance: :anchor: :angel: :anger: :angry: :anguished: :ant: +:apple: :aquarius: :aries: :arrow_backward: :arrow_double_down: :arrow_double_up: :arrow_down: :arrow_down_small: :arrow_forward: :arrow_heading_down: +:arrow_heading_up: :arrow_left: :arrow_lower_left: :arrow_lower_right: :arrow_right: :arrow_right_hook: :arrow_up: :arrow_up_down: +:arrow_upper_left: :arrow_upper_right: :arrows_clockwise: :arrows_counterclockwise: :art: :articulated_lorry: :astonished: :atm: :arrow_up_small: :b: +:baby: :baby_bottle: :baby_chick: :baby_symbol: :back: :baggage_claim: :balloon: :ballot_box_with_check: :bamboo: :banana: +:bangbang: :bank: :bar_chart: :barber: :baseball: :basketball: :bath: :bathtub: :battery: :bear: +:bee: :beer: :beers: :beetle: :beginner: :bell: :bento: :bicyclist: :bike: :bikini: +:bird: :birthday: :black_circle: :black_joker: :black_medium_small_square: :black_medium_square: :black_nib: :black_small_square: :black_square: :black_square_button: +:blossom: :blowfish: :blue_book: :blue_car: :blue_heart: :blush: :boar: :boat: :bomb: :book: +:bookmark: :bookmark_tabs: :books: :boom: :boot: :bouquet: :bow: :bowling: :bowtie: :boy: +:bread: :bride_with_veil: :bridge_at_night: :briefcase: :broken_heart: :bug: :bulb: :bullettrain_front: :bullettrain_side: :bus: +:busstop: :bust_in_silhouette: :busts_in_silhouette: :cactus: :cake: :calendar: :calling: :camel: :camera: :cancer: +:candy: :capital_abcd: :capricorn: :car: :card_index: :carousel_horse: :cat: :cat2: :cd: :chart: +:chart_with_downwards_trend: :chart_with_upwards_trend: :checkered_flag: :cherries: :cherry_blossom: :chestnut: :chicken: :children_crossing: :chocolate_bar: :christmas_tree: +:church: :cinema: :circus_tent: :city_sunrise: :city_sunset: :cl: :clap: :clapper: :clipboard: :clock1: +:clock10: :clock1030: :clock11: :clock1130: :clock12: :clock1230: :clock130: :clock2: :clock230: :clock3: +:clock330: :clock4: :clock430: :clock5: :clock530: :clock6: :clock630: :clock7: :clock730: :clock8: +:clock830: :clock9: :clock930: :closed_book: :closed_lock_with_key: :closed_umbrella: :cloud: :clubs: :cn: :cocktail: +:coffee: :cold_sweat: :collision: :computer: :confetti_ball: :confounded: :confused: :congratulations: :construction: :construction_worker: +:convenience_store: :cookie: :cool: :cop: :copyright: :corn: :couple: :couple_with_heart: :couplekiss: :cow: +:cow2: :credit_card: :crescent_moon: :crocodile: :crossed_flags: :crown: :cry: :crying_cat_face: :crystal_ball: :cupid: +:curly_loop: :currency_exchange: :curry: :custard: :customs: :cyclone: :dancer: :dancers: :dango: :dart: +:dash: :date: :de: :deciduous_tree: :department_store: :diamond_shape_with_a_dot_inside: :diamonds: :disappointed: :disappointed_relieved: :dizzy: +:dizzy_face: :do_not_litter: :dog: :dog2: :dollar: :dolls: :dolphin: :donut: :door: :doughnut: +:dragon: :dragon_face: :dress: :dromedary_camel: :droplet: :dvd: :e-mail: :ear: :ear_of_rice: :earth_africa: +:earth_americas: :earth_asia: :egg: :eggplant: :eight: :eight_pointed_black_star: :eight_spoked_asterisk: :electric_plug: :elephant: :email: + :end: :envelope: :es: :euro: :european_castle: :european_post_office: :evergreen_tree: :exclamation: :expressionless: :eyeglasses: +:eyes: :facepunch: :factory: :fallen_leaf: :family: :fast_forward: :fax: :fearful: :feelsgood: :feet: +:ferris_wheel: :file_folder: :finnadie: :fire: :fire_engine: :fireworks: :first_quarter_moon: :first_quarter_moon_with_face: :fish: :fish_cake: +:fishing_pole_and_fish: :fist: :five: :flags: :flashlight: :floppy_disk: :flower_playing_cards: :flushed: :foggy: :football: +:fork_and_knife: :fountain: :four: :four_leaf_clover: :fr: :free: :fried_shrimp: :fries: :frog: :frowning: +:fu: :fuelpump: :full_moon: :full_moon_with_face: :game_die: :gb: :gem: :gemini: :ghost: :gift:`, + + `**[5] [Emoji Display Test 2]** +:gift_heart: :girl: :globe_with_meridians: :goat: :goberserk: :godmode: :golf: :grapes: :green_apple: :green_book: +:green_heart: :grey_exclamation: :grey_question: :grimacing: :grin: :grinning: :guardsman: :guitar: :gun: :haircut: +:hamburger: :hammer: :hamster: :hand: :handbag: :hankey: :hash: :hatched_chick: :hatching_chick: :headphones: +:hear_no_evil: :heart: :heart_decoration: :heart_eyes: :heart_eyes_cat: :heartbeat: :heartpulse: :hearts: :heavy_check_mark: :heavy_division_sign: +:heavy_dollar_sign: :heavy_exclamation_mark: :heavy_minus_sign: :heavy_multiplication_x: :heavy_plus_sign: :helicopter: :herb: :hibiscus: :high_brightness: :high_heel: +:hocho: :honey_pot: :honeybee: :horse: :horse_racing: :hospital: :hotel: :hotsprings: :hourglass: :hourglass_flowing_sand: +:house: :house_with_garden: :hurtrealbad: :hushed: :ice_cream: :icecream: :id: :ideograph_advantage: :imp: :inbox_tray: +:incoming_envelope: :information_desk_person: :information_source: :innocent: :interrobang: :iphone: :it: :izakaya_lantern: :jack_o_lantern: +:japan: :japanese_castle: :japanese_goblin: :japanese_ogre: :jeans: :joy: :joy_cat: :jp: :key: :keycap_ten: +:kimono: :kiss: :kissing: :kissing_cat: :kissing_closed_eyes: :kissing_face: :kissing_heart: :kissing_smiling_eyes: :koala: :koko: +:kr: :large_blue_circle: :large_blue_diamond: :large_orange_diamond: :last_quarter_moon: :last_quarter_moon_with_face: :laughing: :leaves: :ledger: :left_luggage: +:left_right_arrow: :leftwards_arrow_with_hook: :lemon: :leo: :leopard: :libra: :light_rail: :link: :lips: :lipstick: +:lock: :lock_with_ink_pen: :lollipop: :loop: :loudspeaker: :love_hotel: :love_letter: :low_brightness: :m: :mag: +:mag_right: :mahjong: :mailbox: :mailbox_closed: :mailbox_with_mail: :mailbox_with_no_mail: :man: :man_with_gua_pi_mao: :man_with_turban: :mans_shoe: +:maple_leaf: :mask: :massage: :meat_on_bone: :mega: :melon: :memo: :mens: :metal: :metro: +:microphone: :microscope: :milky_way: :minibus: :minidisc: :mobile_phone_off: :money_with_wings: :moneybag: :monkey: :monkey_face: +:monorail: :mortar_board: :mount_fuji: :mountain_bicyclist: :mountain_cableway: :mountain_railway: :mouse: :mouse2: :movie_camera: :moyai: +:muscle: :mushroom: :musical_keyboard: :musical_note: :musical_score: :mute: :nail_care: :name_badge: :neckbeard: :necktie: +:negative_squared_cross_mark: :neutral_face: :new: :new_moon: :new_moon_with_face: :newspaper: :ng: :nine: :no_bell: +:no_bicycles: :no_entry: :no_entry_sign: :no_good: :no_mobile_phones: :no_mouth: :no_pedestrians: :no_smoking: :non-potable_water: :nose: +:notebook: :notebook_with_decorative_cover: :notes: :nut_and_bolt: :o: :o2: :ocean: :octocat: :octopus: :oden: +:office: :ok: :ok_hand: :ok_woman: :older_man: :older_woman: :on: :oncoming_automobile: :oncoming_bus: :oncoming_police_car: +:oncoming_taxi: :one: :open_file_folder: :open_hands: :open_mouth: :ophiuchus: :orange_book: :outbox_tray: :ox: :package: +:page_facing_up: :page_with_curl: :pager: :palm_tree: :panda_face: :paperclip: :parking: :part_alternation_mark: :partly_sunny: :passport_control: +:paw_prints: :peach: :pear: :pencil: :pencil2: :penguin: :pensive: :performing_arts: :persevere: :person_frowning: +:person_with_blond_hair: :person_with_pouting_face: :phone: :pig: :pig2: :pig_nose: :pill: :pineapple: :pisces: :pizza: +`, + + `**[6] [Emoji Display Test 3]** +:plus1: :point_down: :point_left: :point_right: :point_up: :point_up_2: :police_car: :poodle: :poop: :post_office: +:postal_horn: :postbox: :potable_water: :pouch: :poultry_leg: :pound: :pouting_cat: :pray: :princess: :punch: +:purple_heart: :purse: :pushpin: :put_litter_in_its_place: :question: :rabbit: :rabbit2: :racehorse: :radio: :radio_button: +:rage: :rage1: :rage2: :rage3: :rage4: :railway_car: :rainbow: :raised_hand: :raised_hands: :raising_hand: +:ram: :ramen: :rat: :recycle: :red_car: :red_circle: :registered: :relaxed: :relieved: :repeat: +:repeat_one: :restroom: :revolving_hearts: :rewind: :ribbon: :rice: :rice_ball: :rice_cracker: :rice_scene: :ring: +:rocket: :roller_coaster: :rooster: :rose: :rotating_light: :round_pushpin: :rowboat: :ru: +:rugby_football: :runner: :running: :running_shirt_with_sash: :sa: :sagittarius: :sailboat: :sake: :sandal: :santa: +:satellite: :satisfied: :saxophone: :school: :school_satchel: :scissors: :scorpius: :scream: :scream_cat: :scroll: +:seat: :secret: :see_no_evil: :seedling: :seven: :shaved_ice: :sheep: :shell: :ship: :shipit: +:shirt: :shit: :shoe: :shower: :signal_strength: :six: :six_pointed_star: :ski: :skull: :sleeping: +:sleepy: :slot_machine: :small_blue_diamond: :small_orange_diamond: :small_red_triangle: :small_red_triangle_down: :smile: :smile_cat: :smiley: :smiley_cat: +:smiling_imp: :smirk: :smirk_cat: :smoking: :snail: :snake: :snowboarder: :snowflake: :snowman: :sob: +:soccer: :soon: :sos: :sound: :space_invader: :spades: :spaghetti: :sparkle: :sparkler: :sparkles: +:sparkling_heart: :speak_no_evil: :speaker: :speech_balloon: :speedboat: :squirrel: :star: :star2: :stars: :station: +:statue_of_liberty: :steam_locomotive: :stew: :straight_ruler: :strawberry: :stuck_out_tongue: :stuck_out_tongue_closed_eyes: :stuck_out_tongue_winking_eye: :sun_with_face: :sunflower: + :sunglasses: :sunny: :sunrise: :sunrise_over_mountains: :surfer: :sushi: :suspect: :suspension_railway: :sweat: :sweat_drops: +:sweat_smile: :sweet_potato: :swimmer: :symbols: :syringe: :tada: :tanabata_tree: :tangerine: :taurus: :taxi: +:tea: :telephone: :telephone_receiver: :telescope: :tennis: :tent: :thought_balloon: :three: :thumbsdown: :thumbsup: +:ticket: :tiger: :tiger2: :tired_face: :tm: :toilet: :tokyo_tower: :tomato: :tongue: :top: +:tophat: :tractor: :traffic_light: :train: :train2: :tram: :triangular_flag_on_post: :triangular_ruler: :trident: :triumph: +:trolleybus: :trollface: :trophy: :tropical_drink: :tropical_fish: :truck: :trumpet: :tshirt: :tulip: :turtle: +:tv: :twisted_rightwards_arrows: :two: :two_hearts: :two_men_holding_hands: :two_women_holding_hands: +:uk: :umbrella: :unamused: :underage: :unlock: :up: :us: :v: :vertical_traffic_light: :vhs: +:vibration_mode: :video_camera: :video_game: :violin: :virgo: :volcano: :vs: :walking: :waning_crescent_moon: :waning_gibbous_moon: +:warning: :watch: :water_buffalo: :watermelon: :wave: :wavy_dash: :waxing_crescent_moon: :waxing_gibbous_moon: :wc: :weary: +:wedding: :whale: :whale2: :wheelchair: :white_check_mark: :white_circle: :white_flower: :white_large_square: :white_medium_small_square: :white_medium_square: +:white_small_square: :white_square_button: :wind_chime: :wine_glass: :wink: :wolf: :woman: :womans_clothes: :womans_hat: :womens: +:worried: :wrench: :x: :yellow_heart: :yen: :yum: :zap: :zero: :zzz: +Unnamed: :u5272: :u5408: :u55b6: :u6307: :u6708: :u6709: :u6e80: :u7121: :u7533: :u7981: :u7a7a: +`, + + `**[7] [Auto Linking]** +#### should be turned into links: +http://example.com +https://example.com +www.example.com +www.example.com/index +www.example.com/index.html +www.example.com/index/sub +www.example.com/index?params=1 +www.example.com/index?params=1&other=2 +www.example.com/index?params=1;other=2 +http://example.com:8065 +<http://example.com> +<www.example.com> +http://www.example.com/_/page +www.example.com/_/page +https://en.wikipedia.org/wiki/🐬 +https://en.wikipedia.org/wiki/Rendering_(computer_graphics) +http://127.0.0.1 +http://192.168.1.1:4040 +http://[::1]:80 +http://[::1]:8065 +https://[::1]:80 +http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80 +http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:8065 +https://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:443 +http://username:password@example.com +http://username:password@127.0.0.1 +http://username:password@[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80 +test@example.com + +#### should be turned into links which link to the correct place: +[example link](example.com) links to ` + "`" + `http://example.com` + "`" + ` +[example.com](example.com) links to ` + "`" + `http://example.com` + "`" + ` +[example.com/other](example.com) links to ` + "`" + `http://example.com` + "`" + ` +[example.com/other_link](example.com/example) links to ` + "`" + `http://example.com/example` + "`" + ` +www.example.com links to ` + "`" + `http://www.example.com` + "`" + ` +https://example.com links to ` + "`" + `https://example.com` + "`" + `and not ` + "`" + `http://example.com` + "`" + ` +https://en.wikipedia.org/wiki/🐬 links to the Wikipedia article on dolphins +https://en.wikipedia.org/wiki/URLs#Syntax links to the Syntax section of the Wikipedia article on URLs +test@example.com links to ` + "`" + `mailto:test@example.com` + "`" + ` +[email link](mailto:test@example.com) links to ` + "`" + `mailto:test@example.com` + "`" + `and not ` + "`" + `http://mailto:test@example.com` + "`" + ` +[other link](ts3server://example.com) links to ` + "`" + `ts3server://example.com` + "`" + `and not ` + "`" + `http://ts3server://example.com` + "`" + ` + +#### should not be turned into links: +example.com +readme.md +<example.com> +http:// +@example.com + +#### should only turn the actual link into a link and not change surrounding text +(http://example.com) +(test@example.com) +This is a sentence with a http://example.com in it. +This is a sentence with a [link](http://example.com) in it. +This is a sentence with a http://example.com/_/underscore in it. +This is a sentence with a link (http://example.com) in it. +This is a sentence with a (https://en.wikipedia.org/wiki/Rendering_(computer_graphics)) in it. +This is a sentence with a http://192.168.1.1:4040 in it. +This is a sentence with a https://::1 in it. +This is a link to http://example.com. +`, + "*", "?", ".", "}{][)(><", "{}[]()<>", "qahwah ( قهوة)", @@ -238,5 +473,5 @@ func RandomText(length Range, hashtags Range, mentions Range, users []string) st } func FuzzPost() string { - return FUZZY_STRINGS_POSTS[RandIntFromRange(Range{0, len(FUZZY_STRINGS_NAMES) - 1})] + return FUZZY_STRINGS_POSTS[RandIntFromRange(Range{0, len(FUZZY_STRINGS_POSTS) - 1})] } diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index d309ced2e..8e0ab0555 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -6,6 +6,7 @@ var AdminStore = require('../../stores/admin_store.jsx'); var TeamStore = require('../../stores/team_store.jsx'); var AsyncClient = require('../../utils/async_client.jsx'); var LoadingScreen = require('../loading_screen.jsx'); +var Utils = require('../../utils/utils.jsx'); var EmailSettingsTab = require('./email_settings.jsx'); var LogSettingsTab = require('./log_settings.jsx'); @@ -46,7 +47,8 @@ export default class AdminController extends React.Component { }; if (!props.tab) { - history.replaceState(null, null, `/admin_console/${this.state.selected}`); + var tokenIndex = Utils.getUrlParameter('session_token_index'); + history.replaceState(null, null, `/admin_console/${this.state.selected}?session_token_index=${tokenIndex}`); } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index f2fb1c96d..0d52ae347 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -3,6 +3,7 @@ var AdminSidebarHeader = require('./admin_sidebar_header.jsx'); var SelectTeamModal = require('./select_team_modal.jsx'); +var Utils = require('../../utils/utils.jsx'); export default class AdminSidebar extends React.Component { constructor(props) { @@ -24,12 +25,13 @@ export default class AdminSidebar extends React.Component { handleClick(name, teamId, e) { e.preventDefault(); this.props.selectTab(name, teamId); - history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}`); + var tokenIndex = Utils.getUrlParameter('session_token_index'); + history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}?session_token_index=${tokenIndex}`); } isSelected(name, teamId) { if (this.props.selected === name) { - if (name === 'team_users') { + if (name === 'team_users' || name === 'team_analytics') { if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) { return 'active'; } diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index c519959af..2b9ce67ca 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -185,7 +185,7 @@ export default class Login extends React.Component { if (this.props.inviteId) { userSignUp = ( <div> - <span>{'Do not have an account? '} + <span>{`Don't have an account? `} <a href={'/signup_user_complete/?id=' + this.props.inviteId} className='signup-team-login' diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index e1f495d54..e4094daf3 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -77,12 +77,12 @@ export default class PostBody extends React.Component { this.isGifLoading = true; const gif = new Image(); - gif.src = src; gif.onload = ( () => { this.setState({gifLoaded: true}); } ); + gif.src = src; } createGifEmbed(link) { @@ -92,7 +92,12 @@ export default class PostBody extends React.Component { if (!this.state.gifLoaded) { this.loadGif(link); - return null; + return ( + <img + className='gif-div placeholder' + height='500px' + /> + ); } return ( diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx index ce19c48f0..b56a7b006 100644 --- a/web/react/components/search_results.jsx +++ b/web/react/components/search_results.jsx @@ -83,7 +83,16 @@ export default class SearchResults extends React.Component { var ctls = null; if (noResults) { - ctls = <div className='sidebar--right__subheader'>No results</div>; + ctls = + ( + <div className='sidebar--right__subheader'> + <h4>{'NO RESULTS'}</h4> + <ul> + <li>If you're searching a partial phrase (ex. searching "rea", looking for "reach" or "reaction"), append a * to your search term</li> + <li>Due to the volume of results, two letter searches and common words like "this", "a" and "is" won't appear in search results</li> + </ul> + </div> + ); } else { ctls = results.order.map(function mymap(id) { var post = results.posts[id]; diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx index 4af46c35a..68d9cea48 100644 --- a/web/react/components/settings_sidebar.jsx +++ b/web/react/components/settings_sidebar.jsx @@ -1,10 +1,14 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +var utils = require('../utils/utils.jsx'); export default class SettingsSidebar extends React.Component { componentDidUpdate() { $('.settings-modal').find('.modal-body').scrollTop(0); $('.settings-modal').find('.modal-body').perfectScrollbar('update'); + if (utils.isSafari()) { + $('.settings-modal .settings-links .nav').addClass('absolute'); + } } constructor(props) { super(props); 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 f926f5cbb..37760a2a2 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -12,11 +12,6 @@ export default class TeamSignUp extends React.Component { this.updatePage = this.updatePage.bind(this); - if (global.window.mm_config.EnableTeamListing === 'true') { - this.state = {page: 'team_listing'}; - return; - } - var count = 0; if (global.window.mm_config.EnableSignUpWithEmail === 'true') { @@ -41,50 +36,83 @@ export default class TeamSignUp extends React.Component { } render() { - if (this.state.page === 'team_listing') { - return ( - <div> - <h3>{'Choose a Team'}</h3> - <div className='signup-team-all'> - { - this.props.teams.map((team) => { - return ( - <div - key={'team_' + team.name} - className='signup-team-dir' - > - <a - href={'/' + team.name} + var teamListing = null; + + if (global.window.mm_config.EnableTeamListing === 'true') { + if (this.props.teams.length === 0) { + if (global.window.mm_config.EnableTeamCreation !== 'true') { + teamListing = (<div>{'There are no teams include in the Team Directory and team creation has been disabled.'}</div>); + } + } else { + teamListing = ( + <div> + <h3>{'Choose a Team'}</h3> + <div className='signup-team-all'> + { + this.props.teams.map((team) => { + return ( + <div + key={'team_' + team.name} + className='signup-team-dir' > - <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> - </a> - </div> - ); - }) - } + <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> + </a> + </div> + ); + }) + } + </div> </div> + ); + } + } + + if (global.window.mm_config.EnableTeamCreation !== 'true') { + if (teamListing == null) { + return (<div>{'Team creation has been disabled. Please contact an administrator for access.'}</div>); + } + + return ( + <div> + {teamListing} </div> ); } if (this.state.page === 'choose') { return ( - <ChoosePage - updatePage={this.updatePage} - /> + <div> + {teamListing} + <ChoosePage + updatePage={this.updatePage} + /> + </div> ); } if (this.state.page === 'email') { - return <EmailSignUpPage />; + return ( + <div> + {teamListing} + <EmailSignUpPage /> + </div> + ); } else if (this.state.page === 'gitlab') { - return <SSOSignupPage service={Constants.GITLAB_SERVICE} />; + return ( + <div> + {teamListing} + <SSOSignupPage service={Constants.GITLAB_SERVICE} /> + </div> + ); } } } diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx index 69ba44664..587ef5ec2 100644 --- a/web/react/components/team_general_tab.jsx +++ b/web/react/components/team_general_tab.jsx @@ -54,7 +54,6 @@ export default class GeneralTab extends React.Component { handleTeamListingRadio(listing) { if (global.window.mm_config.EnableTeamListing !== 'true' && listing) { - ReactDOM.findDOMNode(this.refs.teamListingRadioNo).checked = true; this.setState({clientError: 'Team directory has been disabled. Please ask a system admin to enable it.'}); } else { this.setState({allow_team_listing: listing}); @@ -278,13 +277,13 @@ export default class GeneralTab extends React.Component { </label> <br/> </div> - <div><br/>{'When allowed the team will appear on the main page as part of team directory.'}</div> + <div><br/>{'Including this team will display the team name from the Team Directory section of the Home Page, and provide a link to the sign-in page.'}</div> </div> ]; teamListingSection = ( <SettingItemMax - title='Allow in Team Directory' + title='Include this team in the Team Directory' inputs={inputs} submit={this.handleTeamListingSubmit} server_error={serverError} @@ -302,7 +301,7 @@ export default class GeneralTab extends React.Component { teamListingSection = ( <SettingItemMin - title='Allow in Team Directory' + title='Include this team in the Team Directory' describe={describe} updateSection={this.onUpdateTeamListingSection} /> @@ -337,13 +336,13 @@ export default class GeneralTab extends React.Component { </label> <br/> </div> - <div><br/>{'When allowed the team signup link will be included on the login page and anyone can signup to this team.'}</div> + <div><br/>{'When allowed, a link to account creation will be included on the sign-in page of this team and allow any visitor to sign-up.'}</div> </div> ]; openInviteSection = ( <SettingItemMax - title='Allow Open Invitations' + title='Allow anyone to sign-up from login page' inputs={inputs} submit={this.handleOpenInviteSubmit} server_error={serverError} @@ -360,7 +359,7 @@ export default class GeneralTab extends React.Component { openInviteSection = ( <SettingItemMin - title='Allow Open Invitations' + title='Allow anyone to sign-up from login page' describe={describe} updateSection={this.onUpdateOpenInviteSection} /> @@ -373,29 +372,28 @@ export default class GeneralTab extends React.Component { const inputs = []; inputs.push( - <div - key='teamInviteSetting' - className='form-group' - > - <label className='col-sm-5 control-label'>{'Invite Code'}</label> - <div className='col-sm-7'> - <input - className='form-control' - type='text' - onChange={this.updateInviteId} - value={this.state.invite_id} - maxLength='32' - /> - </div> - <div><br/>{'When allowing open invites this code is used as part of the signup process. Changing this code will invalidate the previous open signup link.'}</div> - <div className='help-text'> - <button - className='btn btn-default' - onClick={this.handleGenerateInviteId} - > - {'Re-Generate'} - </button> + <div key='teamInviteSetting'> + <div className='row'> + <label className='col-sm-5 control-label'>{'Invite Code'}</label> + <div className='col-sm-7'> + <input + className='form-control' + type='text' + onChange={this.updateInviteId} + value={this.state.invite_id} + maxLength='32' + /> + <div className='padding-top x2'> + <a + href='#' + onClick={this.handleGenerateInviteId} + > + {'Re-Generate'} + </a> + </div> + </div> </div> + <div className='setting-list__hint'>{'When allowing open invites this code is used as part of the signup process. Changing this code will invalidate the previous open signup link.'}</div> </div> ); @@ -413,7 +411,7 @@ export default class GeneralTab extends React.Component { inviteSection = ( <SettingItemMin title={`Invite Code`} - describe={`Click 'Edit' to re-generate invite Code.`} + describe={`Click 'Edit' to regenerate Invite Code.`} updateSection={this.onUpdateInviteIdSection} /> ); @@ -494,8 +492,11 @@ export default class GeneralTab extends React.Component { <h3 className='tab-header'>{'General Settings'}</h3> <div className='divider-dark first'/> {nameSection} + <div className='divider-light'/> {openInviteSection} + <div className='divider-light'/> {teamListingSection} + <div className='divider-light'/> {inviteSection} <div className='divider-dark'/> </div> diff --git a/web/react/components/tutorial/tutorial_intro_screens.jsx b/web/react/components/tutorial/tutorial_intro_screens.jsx index c7abccae3..a99e9fe28 100644 --- a/web/react/components/tutorial/tutorial_intro_screens.jsx +++ b/web/react/components/tutorial/tutorial_intro_screens.jsx @@ -35,6 +35,9 @@ export default class TutorialIntroScreens extends React.Component { preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue); AsyncClient.savePreferences([preference]); } + componentDidMount() { + $('.tutorials__scroll').perfectScrollbar(); + } createScreen() { switch (this.state.currentScreen) { case 0: @@ -50,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'/> @@ -65,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'/> @@ -120,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'/> @@ -130,20 +133,26 @@ export default class TutorialIntroScreens extends React.Component { ); } render() { + const height = Utils.windowHeight() - 100; const screen = this.createScreen(); return ( - <div className='tutorial-steps__container'> - <div className='tutorial__content'> - <div className='tutorial__steps'> - {screen} - <button - className='btn btn-primary' - tabIndex='1' - onClick={this.handleNext} - > - {'Next'} - </button> + <div + className='tutorials__scroll' + style={{height}} + > + <div className='tutorial-steps__container'> + <div className='tutorial__content'> + <div className='tutorial__steps'> + {screen} + <button + className='btn btn-primary' + tabIndex='1' + onClick={this.handleNext} + > + {'Next'} + </button> + </div> </div> </div> </div> diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index 179416ea0..f9416b2de 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -34,6 +34,11 @@ const highlightJsIni = require('highlight.js/lib/languages/ini.js'); const Constants = require('../utils/constants.jsx'); const HighlightedLanguages = Constants.HighlightedLanguages; +function markdownImageLoaded(image) { + image.style.height = 'auto'; +} +window.markdownImageLoaded = markdownImageLoaded; + class MattermostInlineLexer extends marked.InlineLexer { constructor(links, options) { super(links, options); @@ -132,6 +137,16 @@ class MattermostMarkdownRenderer extends marked.Renderer { return super.br(); } + image(href, title, text) { + let out = '<img src="' + href + '" alt="' + text + '"'; + if (title) { + out += ' title="' + title + '"'; + } + out += ' onload="window.markdownImageLoaded(this)" class="markdown-inline-img"'; + out += this.options.xhtml ? '/>' : '>'; + return out; + } + heading(text, level, raw) { const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`; return `<h${level} id="${id}" class="markdown__heading">${text}</h${level}>`; diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 35c6e29b6..e8d34dccd 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -59,6 +59,20 @@ export function isTestDomain() { return false; } +export function isChrome() { + if (navigator.userAgent.indexOf('Chrome') > -1) { + return true; + } + return false; +} + +export function isSafari() { + if (navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1) { + return true; + } + return false; +} + export function isInRole(roles, inRole) { var parts = roles.split(' '); for (var i = 0; i < parts.length; i++) { @@ -500,16 +514,16 @@ export function applyTheme(theme) { changeCss('#post-create', 'background:' + theme.centerChannelBg, 1); changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1); changeCss('.post-image__column .post-image__details', 'background:' + theme.centerChannelBg, 1); - changeCss('.sidebar--right, .dropdown-menu, .popover', 'background:' + theme.centerChannelBg, 1); + changeCss('.sidebar--right, .dropdown-menu, .popover, .tip-overlay', 'background:' + theme.centerChannelBg, 1); changeCss('.popover.bottom>.arrow:after', 'border-bottom-color:' + theme.centerChannelBg, 1); - changeCss('.popover.right>.arrow:after', 'border-right-color:' + theme.centerChannelBg, 1); + changeCss('.popover.right>.arrow:after, .tip-overlay.tip-overlay--sidebar .arrow, .tip-overlay.tip-overlay--header .arrow', 'border-right-color:' + theme.centerChannelBg, 1); changeCss('.popover.left>.arrow:after', 'border-left-color:' + theme.centerChannelBg, 1); - changeCss('.popover.top>.arrow:after', 'border-top-color:' + theme.centerChannelBg, 1); + changeCss('.popover.top>.arrow:after, .tip-overlay.tip-overlay--chat .arrow', 'border-top-color:' + theme.centerChannelBg, 1); changeCss('.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1); } if (theme.centerChannelColor) { - changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name', 'color:' + theme.centerChannelColor, 1); + changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name, .tip-overlay', 'color:' + theme.centerChannelColor, 1); changeCss('#post-create', 'color:' + theme.centerChannelColor, 2); changeCss('.channel-header__links a', 'fill:' + changeOpacity(theme.centerChannelColor, 0.7), 1); changeCss('.channel-header__links a:hover, .channel-header__links a:active', 'fill:' + theme.centerChannelColor, 2); @@ -519,7 +533,7 @@ export function applyTheme(theme) { changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3); changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2); changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1); - changeCss('.post-body hr, .loading-screen .loading__content .round', 'background:' + theme.centerChannelColor, 1); + changeCss('.post-body hr, .loading-screen .loading__content .round, .tutorial__circles .circle, .tip-overlay .tutorial__circles .circle.active', 'background:' + theme.centerChannelColor, 1); changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1); changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); @@ -568,7 +582,7 @@ export function applyTheme(theme) { } if (theme.buttonBg) { - changeCss('.btn.btn-primary', 'background:' + theme.buttonBg, 1); + changeCss('.btn.btn-primary, .tutorial__circles .circle.active', 'background:' + theme.buttonBg, 1); changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25), 1); changeCss('.file-playback-controls', 'color:' + changeColor(theme.buttonBg, -0.25), 1); } diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index c286927a2..2830026c9 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -133,9 +133,6 @@ a:focus, a:hover { &.no-resize { resize: none; } - &.min-height { - min-height: 100px; - } } .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index feb392234..5c8313454 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -1,5 +1,6 @@ #channel-header { padding: 3px 0; + height: 58px; } .row { &.header { @@ -42,6 +43,9 @@ text-overflow: ellipsis; margin-top: 2px; max-height: 45px; + .markdown-inline-img { + max-height: 45px + } } &.popover { white-space: normal; diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss index 70c99f504..87e809694 100644 --- a/web/sass-files/sass/partials/_markdown.scss +++ b/web/sass-files/sass/partials/_markdown.scss @@ -8,6 +8,10 @@ margin-left: 4px; } } +.markdown-inline-img { + max-height: 500px; + height: 500px; +} .post-body { hr { height: 4px; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index a49a98952..8f25f58cd 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -275,6 +275,14 @@ } } @media screen and (max-width: 768px) { + .form-control { + &.min-height { + min-height: 100px; + } + } + .gif-div { + max-width: 100%; + } .tip-div { left: 15px; right: auto; @@ -460,7 +468,7 @@ } } .settings-table { - .nav { + .nav, .nav.absolute { position: relative; top: auto; width: 100%; diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index fbbd07485..96a6cf2ab 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -72,6 +72,10 @@ position: fixed; top: 57px; width: 179px; + &.absolute { + position: absolute; + top: 0; + } } .security-links { margin-right: 20px; diff --git a/web/sass-files/sass/partials/_sidebar--right.scss b/web/sass-files/sass/partials/_sidebar--right.scss index c954b03d8..a4267294c 100644 --- a/web/sass-files/sass/partials/_sidebar--right.scss +++ b/web/sass-files/sass/partials/_sidebar--right.scss @@ -80,13 +80,18 @@ } .sidebar--right__subheader { font-size: 1em; - text-transform: uppercase; - color: #999; - font-weight: 400; - color: #888; - height: 44px; - line-height: 44px; - padding: 0 1em; + padding: 1em 1em 0; + h4 { + font-size: 1em; + } + ul { + @include opacity(0.7); + padding: 10px 0 0 30px; + } + li { + font-size: 0.95em; + padding-bottom: 10px; + } } } diff --git a/web/sass-files/sass/partials/_signup.scss b/web/sass-files/sass/partials/_signup.scss index 14c676f82..84f9892f4 100644 --- a/web/sass-files/sass/partials/_signup.scss +++ b/web/sass-files/sass/partials/_signup.scss @@ -316,7 +316,7 @@ .signup-team-all { width: 280px; box-shadow: 3px 3px 1px #d5d5d5; - margin: 0px 0px 0px 5px; + margin: 0px 0px 50px 5px; } .signup-team-dir { diff --git a/web/sass-files/sass/partials/_tutorial.scss b/web/sass-files/sass/partials/_tutorial.scss index 42183d599..70216aa97 100644 --- a/web/sass-files/sass/partials/_tutorial.scss +++ b/web/sass-files/sass/partials/_tutorial.scss @@ -146,11 +146,12 @@ text-align: center; width: 100%; display: table; + height: 100%; .tutorial__content { display: table-cell; vertical-align: middle; padding-bottom: 100px; - padding: 0 40px; + padding: 20px 40px 40px; .tutorial__steps { max-width: 310px; min-height: 420px; @@ -176,7 +177,7 @@ width: 9px; height: 9px; @include border-radius(9px); - @include opacity(0.1); + @include opacity(0.2); background: #000; display: inline-block; margin: 0 5px; diff --git a/web/sass-files/sass/partials/_videos.scss b/web/sass-files/sass/partials/_videos.scss index f6999d15c..bcfc28f19 100644 --- a/web/sass-files/sass/partials/_videos.scss +++ b/web/sass-files/sass/partials/_videos.scss @@ -56,5 +56,8 @@ max-width: 450px; max-height: 500px; margin-bottom: 8px; - border-radius:5px + border-radius:5px; + &.placeholder { + height: 500px; + } } diff --git a/web/templates/head.html b/web/templates/head.html index 9d8dcdd66..24f9862c0 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -84,15 +84,14 @@ if (window.mm_config.SegmentDeveloperKey != null && window.mm_config.SegmentDeveloperKey !== "") { !function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1"; analytics.load(window.mm_config.SegmentDeveloperKey); - var user = window.UserStore.getCurrentUser(true); - if (user) { + if (window.mm_user) { analytics.identify(user.id, { - name: user.nickname, - email: user.email, - createdAt: user.create_at, - username: user.username, - team_id: user.team_id, - id: user.id + name: window.mm_user.nickname, + email: window.mm_user.email, + createdAt: window.mm_user.create_at, + username: window.mm_user.username, + team_id: window.mm_user.team_id, + id: window.mm_user.id }); } analytics.page(); diff --git a/web/web.go b/web/web.go index 34f4df08f..51f6664b6 100644 --- a/web/web.go +++ b/web/web.go @@ -170,7 +170,7 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { page.Props[team.Name] = team.DisplayName } - if len(teams) == 1 && *utils.Cfg.TeamSettings.EnableTeamListing { + if len(teams) == 1 && *utils.Cfg.TeamSettings.EnableTeamListing && !utils.Cfg.TeamSettings.EnableTeamCreation { http.Redirect(w, r, c.GetSiteURL()+"/"+teams[0].Name, http.StatusTemporaryRedirect) return } |