diff options
64 files changed, 1078 insertions, 684 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ffce2a9e..db8c90023 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,50 @@ -# Contributing +# Code Contribution Guidelines -## Contributing Code +Thank you for your interest in contributing to Mattermost. This guide provides an overview of important information for contributors to know. -Please see [Mattermost Code Contribution Guidelines](https://github.com/mattermost/platform/blob/master/doc/developer/Code-Contribution-Guidelines.md) +## Choose a Ticket + +1. Review the list of [Good First Contribution](https://mattermost.atlassian.net/issues/?filter=10206) tickets listed in Jira + - You are welcome to work on any ticket, even if it is assigned, so long as it is not yet marked "in progress" + - (optional) Comment on the ticket that you're starting so no one else inadvertently duplicates your work + +2. These projects are intended to be a straight forward first pull requests from new contributors + - If you don't find something appropriate for your interests, please see the full list of tickets [Accepting Pull Requests](https://mattermost.atlassian.net/issues/?filter=10101) + - Also, feel free to fix bugs you find, or items in GitHub issues that the core team has approved, but not yet added to Jira + +3. If you have any questions at all about a ticket, there are several options to ask: + 1. Start a topic in the [Mattermost forum](http://forum.mattermost.org/) + 2. Join the [Mattermost core team discussion](https://pre-release.mattermost.com/signup_user_complete/?id=rcgiyftm7jyrxnma1osd8zswby) and post in the "Tickets" channel + +## Install Mattermost and set up a Fork + +1. Follow [developer setup instructions](https://github.com/mattermost/platform/blob/master/doc/developer/Setup.md) to install Mattermost + +2. Create a branch with <branch name> set to the ID of the ticket you're working on, for example ```PLT-394```, using command: + +``` +git checkout -b <branch name> +``` + +## Programming and Testing + +1. Please review the [Mattermost Style Guide](doc/developer/Style-Guide.md) prior to making changes + + To keep code clean and well structured, Mattermost uses ESLint to check that pull requests adhere to style guidelines for React. Code will need to follow Mattermost's React style guidelines in order to pass the automated build tests when a pull request is submitted + +2. Please make sure to thoroughly test your change before submitting a pull request + + Please review the ["Fast, Obvious, Forgiving" experience design principles](http://www.mattermost.org/design-principles/) for Mattermost and check that your feature meets the criteria. Also, for any changes to user interface or help text, please read the changes out loud, as a quick and easy way to catch any inconsitencies + + +## Submitting a Pull Request + +1. Please add yourself to the Mattermost [approved contributor list](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true) prior to submitting by completing the [contributor license agreement](http://www.mattermost.org/mattermost-contributor-agreement/). + +2. When you submit your pull request please make it against `master` and include the Ticket ID at the beginning of your pull request comment, followed by a colon + + - For example, for a ticket ID `PLT-394` start your comment with: `PLT-394:`. See [previously closed pull requests](https://github.com/mattermost/platform/pulls?q=is%3Apr+is%3Aclosed) for examples + +3. Once submitted, your pull request will be checked via an automated build process and will be reviewed by at least two members of the Mattermost core team, who may either accept the PR or follow-up with feedback. It would then get merged into `master` for the next release + +4. If you've included your mailing address in Step 1, you'll be receiving a [Limited Edition Mattermost Mug](http://forum.mattermost.org/t/limited-edition-mattermost-mugs/143) as a thank you gift after your first pull request has been accepted diff --git a/api/channel.go b/api/channel.go index a8c8505e9..44be1cf97 100644 --- a/api/channel.go +++ b/api/channel.go @@ -22,7 +22,8 @@ func InitChannel(r *mux.Router) { sr.Handle("/create", ApiUserRequired(createChannel)).Methods("POST") sr.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST") sr.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST") - sr.Handle("/update_desc", ApiUserRequired(updateChannelDesc)).Methods("POST") + sr.Handle("/update_header", ApiUserRequired(updateChannelHeader)).Methods("POST") + sr.Handle("/update_purpose", ApiUserRequired(updateChannelPurpose)).Methods("POST") sr.Handle("/update_notify_props", ApiUserRequired(updateNotifyProps)).Methods("POST") sr.Handle("/{id:[A-Za-z0-9]+}/", ApiUserRequiredActivity(getChannel, false)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET") @@ -124,7 +125,7 @@ func CreateDirectChannel(c *Context, otherUserId string) (*model.Channel, *model channel.Name = model.GetDMNameFromIds(otherUserId, c.Session.UserId) channel.TeamId = c.Session.TeamId - channel.Description = "" + channel.Header = "" channel.Type = model.CHANNEL_DIRECT if uresult := <-uc; uresult.Err != nil { @@ -209,7 +210,8 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - oldChannel.Description = channel.Description + oldChannel.Header = channel.Header + oldChannel.Purpose = channel.Purpose if len(channel.DisplayName) > 0 { oldChannel.DisplayName = channel.DisplayName @@ -233,18 +235,18 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) { } } -func updateChannelDesc(c *Context, w http.ResponseWriter, r *http.Request) { +func updateChannelHeader(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) channelId := props["channel_id"] if len(channelId) != 26 { - c.SetInvalidParam("updateChannelDesc", "channel_id") + c.SetInvalidParam("updateChannelHeader", "channel_id") return } - channelDesc := props["channel_description"] - if len(channelDesc) > 1024 { - c.SetInvalidParam("updateChannelDesc", "channel_description") + channelHeader := props["channel_header"] + if len(channelHeader) > 1024 { + c.SetInvalidParam("updateChannelHeader", "channel_header") return } @@ -261,11 +263,54 @@ func updateChannelDesc(c *Context, w http.ResponseWriter, r *http.Request) { channel := cresult.Data.(*model.Channel) // Don't need to do anything channel member, just wanted to confirm it exists - if !c.HasPermissionsToTeam(channel.TeamId, "updateChannelDesc") { + if !c.HasPermissionsToTeam(channel.TeamId, "updateChannelHeader") { return } - channel.Description = channelDesc + channel.Header = channelHeader + + if ucresult := <-Srv.Store.Channel().Update(channel); ucresult.Err != nil { + c.Err = ucresult.Err + return + } else { + c.LogAudit("name=" + channel.Name) + w.Write([]byte(channel.ToJson())) + } + } +} + +func updateChannelPurpose(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + channelId := props["channel_id"] + if len(channelId) != 26 { + c.SetInvalidParam("updateChannelPurpose", "channel_id") + return + } + + channelPurpose := props["channel_purpose"] + if len(channelPurpose) > 1024 { + c.SetInvalidParam("updateChannelPurpose", "channel_purpose") + return + } + + sc := Srv.Store.Channel().Get(channelId) + cmc := Srv.Store.Channel().GetMember(channelId, c.Session.UserId) + + if cresult := <-sc; cresult.Err != nil { + c.Err = cresult.Err + return + } else if cmcresult := <-cmc; cmcresult.Err != nil { + c.Err = cmcresult.Err + return + } else { + channel := cresult.Data.(*model.Channel) + // Don't need to do anything channel member, just wanted to confirm it exists + + if !c.HasPermissionsToTeam(channel.TeamId, "updateChannelPurpose") { + return + } + + channel.Purpose = channelPurpose if ucresult := <-Srv.Store.Channel().Update(channel); ucresult.Err != nil { c.Err = ucresult.Err diff --git a/api/channel_benchmark_test.go b/api/channel_benchmark_test.go index 58e3fa18d..fb8dd61bc 100644 --- a/api/channel_benchmark_test.go +++ b/api/channel_benchmark_test.go @@ -61,8 +61,8 @@ func BenchmarkCreateDirectChannel(b *testing.B) { func BenchmarkUpdateChannel(b *testing.B) { var ( - NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} - CHANNEL_DESCRIPTION_LEN = 50 + NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} + CHANNEL_HEADER_LEN = 50 ) team, _, _ := SetupBenchmark() @@ -73,7 +73,7 @@ func BenchmarkUpdateChannel(b *testing.B) { } for i := range channels { - channels[i].Description = utils.RandString(CHANNEL_DESCRIPTION_LEN, utils.ALPHANUMERIC) + channels[i].Header = utils.RandString(CHANNEL_HEADER_LEN, utils.ALPHANUMERIC) } // Benchmark Start diff --git a/api/channel_test.go b/api/channel_test.go index 899016065..a41f63b1b 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -8,6 +8,7 @@ import ( "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "net/http" + "strings" "testing" "time" ) @@ -173,12 +174,17 @@ func TestUpdateChannel(t *testing.T) { Client.AddChannelMember(channel1.Id, userTeamAdmin.Id) - desc := "a" + model.NewId() + "a" - upChannel1 := &model.Channel{Id: channel1.Id, Description: desc} + header := "a" + model.NewId() + "a" + purpose := "a" + model.NewId() + "a" + upChannel1 := &model.Channel{Id: channel1.Id, Header: header, Purpose: purpose} upChannel1 = Client.Must(Client.UpdateChannel(upChannel1)).Data.(*model.Channel) - if upChannel1.Description != desc { - t.Fatal("Channel admin failed to update desc") + if upChannel1.Header != header { + t.Fatal("Channel admin failed to update header") + } + + if upChannel1.Purpose != purpose { + t.Fatal("Channel admin failed to update purpose") } if upChannel1.DisplayName != channel1.DisplayName { @@ -187,12 +193,17 @@ func TestUpdateChannel(t *testing.T) { Client.LoginByEmail(team.Name, userTeamAdmin.Email, "pwd") - desc = "b" + model.NewId() + "b" - upChannel1 = &model.Channel{Id: channel1.Id, Description: desc} + header = "b" + model.NewId() + "b" + purpose = "b" + model.NewId() + "b" + upChannel1 = &model.Channel{Id: channel1.Id, Header: header, Purpose: purpose} upChannel1 = Client.Must(Client.UpdateChannel(upChannel1)).Data.(*model.Channel) - if upChannel1.Description != desc { - t.Fatal("Team admin failed to update desc") + if upChannel1.Header != header { + t.Fatal("Team admin failed to update header") + } + + if upChannel1.Purpose != purpose { + t.Fatal("Team admin failed to update purpose") } if upChannel1.DisplayName != channel1.DisplayName { @@ -203,7 +214,7 @@ func TestUpdateChannel(t *testing.T) { data := rget.Data.(*model.ChannelList) for _, c := range data.Channels { if c.Name == model.DEFAULT_CHANNEL { - c.Description = "new desc" + c.Header = "new header" if _, err := Client.UpdateChannel(c); err == nil { t.Fatal("should have errored on updating default channel") } @@ -218,7 +229,7 @@ func TestUpdateChannel(t *testing.T) { } } -func TestUpdateChannelDesc(t *testing.T) { +func TestUpdateChannelHeader(t *testing.T) { Setup() team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} @@ -235,36 +246,92 @@ func TestUpdateChannelDesc(t *testing.T) { data := make(map[string]string) data["channel_id"] = channel1.Id - data["channel_description"] = "new desc" + data["channel_header"] = "new header" var upChannel1 *model.Channel - if result, err := Client.UpdateChannelDesc(data); err != nil { + if result, err := Client.UpdateChannelHeader(data); err != nil { t.Fatal(err) } else { upChannel1 = result.Data.(*model.Channel) } - if upChannel1.Description != data["channel_description"] { - t.Fatal("Failed to update desc") + if upChannel1.Header != data["channel_header"] { + t.Fatal("Failed to update header") } data["channel_id"] = "junk" - if _, err := Client.UpdateChannelDesc(data); err == nil { + if _, err := Client.UpdateChannelHeader(data); err == nil { t.Fatal("should have errored on junk channel id") } data["channel_id"] = "12345678901234567890123456" - if _, err := Client.UpdateChannelDesc(data); err == nil { + if _, err := Client.UpdateChannelHeader(data); err == nil { t.Fatal("should have errored on non-existent channel id") } data["channel_id"] = channel1.Id - data["channel_description"] = "" - for i := 0; i < 1050; i++ { - data["channel_description"] += "a" + data["channel_header"] = strings.Repeat("a", 1050) + if _, err := Client.UpdateChannelHeader(data); err == nil { + t.Fatal("should have errored on bad channel header") + } + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + + Client.LoginByEmail(team.Name, user2.Email, "pwd") + + data["channel_id"] = channel1.Id + data["channel_header"] = "new header" + if _, err := Client.UpdateChannelHeader(data); err == nil { + t.Fatal("should have errored non-channel member trying to update header") } - if _, err := Client.UpdateChannelDesc(data); err == nil { - t.Fatal("should have errored on bad channel desc") +} + +func TestUpdateChannelPurpose(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) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user.Id)) + + Client.LoginByEmail(team.Name, user.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + data := make(map[string]string) + data["channel_id"] = channel1.Id + data["channel_purpose"] = "new purpose" + + var upChannel1 *model.Channel + if result, err := Client.UpdateChannelPurpose(data); err != nil { + t.Fatal(err) + } else { + upChannel1 = result.Data.(*model.Channel) + } + + if upChannel1.Purpose != data["channel_purpose"] { + t.Fatal("Failed to update purpose") + } + + data["channel_id"] = "junk" + if _, err := Client.UpdateChannelPurpose(data); err == nil { + t.Fatal("should have errored on junk channel id") + } + + data["channel_id"] = "12345678901234567890123456" + if _, err := Client.UpdateChannelPurpose(data); err == nil { + t.Fatal("should have errored on non-existent channel id") + } + + data["channel_id"] = channel1.Id + data["channel_purpose"] = strings.Repeat("a", 150) + if _, err := Client.UpdateChannelPurpose(data); err == nil { + t.Fatal("should have errored on bad channel purpose") } user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} @@ -274,9 +341,9 @@ func TestUpdateChannelDesc(t *testing.T) { Client.LoginByEmail(team.Name, user2.Email, "pwd") data["channel_id"] = channel1.Id - data["channel_description"] = "new desc" - if _, err := Client.UpdateChannelDesc(data); err == nil { - t.Fatal("should have errored non-channel member trying to update desc") + data["channel_purpose"] = "new purpose" + if _, err := Client.UpdateChannelPurpose(data); err == nil { + t.Fatal("should have errored non-channel member trying to update purpose") } } diff --git a/api/post.go b/api/post.go index b8588fe6a..c98bf2d71 100644 --- a/api/post.go +++ b/api/post.go @@ -249,7 +249,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team } for _, hook := range relevantHooks { - go func() { + go func(hook *model.OutgoingWebhook) { p := url.Values{} p.Set("token", hook.Token) @@ -270,7 +270,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team client := &http.Client{} for _, url := range hook.CallbackURLs { - go func() { + go func(url string) { req, _ := http.NewRequest("POST", url, strings.NewReader(p.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") @@ -289,10 +289,10 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team } } } - }() + }(url) } - }() + }(hook) } }() diff --git a/api/slackimport.go b/api/slackimport.go index 06032c068..cab4c6184 100644 --- a/api/slackimport.go +++ b/api/slackimport.go @@ -182,7 +182,7 @@ func SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[str Type: model.CHANNEL_OPEN, DisplayName: sChannel.Name, Name: SlackConvertChannelName(sChannel.Name), - Description: sChannel.Topic["value"], + Purpose: sChannel.Topic["value"], } mChannel := ImportChannel(&newChannel) if mChannel == nil { diff --git a/api/user.go b/api/user.go index 3796a50ee..c9958767f 100644 --- a/api/user.go +++ b/api/user.go @@ -652,6 +652,12 @@ func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { for k, p := range profiles { options := utils.SanitizeOptions options["passwordupdate"] = false + + if c.HasSystemAdminPermissions("getProfiles") { + options["fullname"] = true + options["email"] = true + } + p.Sanitize(options) profiles[k] = p } diff --git a/doc/developer/Code-Contribution-Guidelines.md b/doc/developer/Code-Contribution-Guidelines.md index 48bbf2491..18be4aa0b 100644 --- a/doc/developer/Code-Contribution-Guidelines.md +++ b/doc/developer/Code-Contribution-Guidelines.md @@ -1,48 +1,5 @@ # Code Contribution Guidelines -Thank you for your interest in contributing to Mattermost. This guide provides an overview of important information for contributors to know. - -## Choose a Ticket - -1. Review the list of [Good First Contribution](https://mattermost.atlassian.net/issues/?filter=10206) tickets listed in Jira. -2. These projects are intended to be a straight forward first pull requests from new contributors. -If you don't find something appropriate for your interests, please see the full list of tickets [Accepting Pull Requests](https://mattermost.atlassian.net/issues/?filter=10101). - -3. If you have any questions at all about a ticket, please post to the [Contributor Discussion section](http://forum.mattermost.org/) of the Mattermost forum, or email the [Mattermost Developer Mailing list](https://groups.google.com/a/mattermost.com/forum/#!forum/developer/join). - -## Install Mattermost and set up a Fork - -1. Follow [developer setup instructions](https://github.com/mattermost/platform/blob/master/doc/developer/Setup.md) to install Mattermost. - -2. Create a branch with <branch name> set to the ID of the ticket you're working on, for example ```PLT-394```, using command: - -``` -git checkout -b <branch name> -``` - -## Programming and Testing - -1. Please review the [Mattermost Style Guide](Style-Guide.md) prior to making changes. - - To keep code clean and well structured, Mattermost uses ESLint to check that pull requests adhere to style guidelines for React. Code will need to follow Mattermost's React style guidelines in order to pass the automated build tests when a pull request is submitted. - -2. Please make sure to thoroughly test your change before submitting a pull request. - - Please review the ["Fast, Obvious, Forgiving" experience design principles](http://www.mattermost.org/design-principles/) for Mattermost and check that your feature meets the criteria. Also, for any changes to user interface or help text, please read the changes out loud, as a quick and easy way to catch any inconsitencies. - - -## Submitting a Pull Request - -1. Please add yourself to the Mattermost [approved contributor list](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true) prior to submitting by completing the [contributor license agreement](http://www.mattermost.org/mattermost-contributor-agreement/). - -2. When you submit your pull request please make it against `master` and include the Ticket ID at the beginning of your pull request comment, followed by a colon. - - For example, for a ticket ID `PLT-394` start your comment with: `PLT-394:`. See [previously closed pull requests](https://github.com/mattermost/platform/pulls?q=is%3Apr+is%3Aclosed) for examples. - -3. Once submitted, your pull request will be checked via an automated build process and will be reviewed by at least two members of the Mattermost core team, who may either accept the PR or follow-up with feedback. It would then get merged into `master` for the next release. - -4. If you've included your mailing address in Step 1, you'll be receiving a [Limited Edition Mattermost Mug](http://forum.mattermost.org/t/limited-edition-mattermost-mugs/143) as a thank you gift after your first pull request has been accepted. - - +Please see [CONTRIBUTING.md](https://github.com/mattermost/platform/blob/master/CONTRIBUTING.md) diff --git a/doc/install/Troubleshooting.md b/doc/install/Troubleshooting.md index 284303949..7166f9978 100644 --- a/doc/install/Troubleshooting.md +++ b/doc/install/Troubleshooting.md @@ -17,8 +17,14 @@ The following is a list of common error messages and solutions: ###### `Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.` -- Message appears in blue bar on team site. Check that [your websocket port is properly configured](https://github.com/mattermost/platform/blob/master/doc/install/Production-Ubuntu.md#set-up-nginx-server). +- Message appears in blue bar on team site. +- **Solution:** Check that [your websocket port is properly configured](https://github.com/mattermost/platform/blob/master/doc/install/Production-Ubuntu.md#set-up-nginx-server). ###### `x509: certificate signed by unknown authority` in server logs when attempting to sign-up - - This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost. You can resolve this issue by setting up a load balancer like Ngnix. A ticket exists to [add support for self-signed certificates in future](x509: certificate signed by unknown authority). + - This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost. You + - **Solution:** Set up a load balancer like Ngnix [per production install guide](https://github.com/mattermost/platform/blob/master/doc/install/Production-Ubuntu.md#set-up-nginx-with-ssl-recommended). A ticket exists to [add support for self-signed certificates in future](x509: certificate signed by unknown authority). + +###### `panic: runtime error: invalid memory address or nil pointer dereference` + - This error can occur if you have manually manipulated the Mattermost database, typically with deletions. Mattermost is designed to serve as a searchable archive, and manual manipulation of the database elements compromises integrity and may prevent upgrade. + - **Solution:** Restore from database backup created prior to manual database updates, or reinstall the system. diff --git a/model/channel.go b/model/channel.go index 076ddf0b5..ac54a7e44 100644 --- a/model/channel.go +++ b/model/channel.go @@ -24,7 +24,8 @@ type Channel struct { Type string `json:"type"` DisplayName string `json:"display_name"` Name string `json:"name"` - Description string `json:"description"` + Header string `json:"header"` + Purpose string `json:"purpose"` LastPostAt int64 `json:"last_post_at"` TotalMsgCount int64 `json:"total_msg_count"` ExtraUpdateAt int64 `json:"extra_update_at"` @@ -89,8 +90,12 @@ func (o *Channel) IsValid() *AppError { return NewAppError("Channel.IsValid", "Invalid type", "id="+o.Id) } - if len(o.Description) > 1024 { - return NewAppError("Channel.IsValid", "Invalid description", "id="+o.Id) + if len(o.Header) > 1024 { + return NewAppError("Channel.IsValid", "Invalid header", "id="+o.Id) + } + + if len(o.Purpose) > 128 { + return NewAppError("Channel.IsValid", "Invalid purpose", "id="+o.Id) } if len(o.CreatorId) > 26 { diff --git a/model/channel_test.go b/model/channel_test.go index e5dfa3734..590417cfe 100644 --- a/model/channel_test.go +++ b/model/channel_test.go @@ -67,6 +67,26 @@ func TestChannelIsValid(t *testing.T) { if err := o.IsValid(); err != nil { t.Fatal(err) } + + o.Header = strings.Repeat("01234567890", 100) + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Header = "1234" + if err := o.IsValid(); err != nil { + t.Fatal(err) + } + + o.Purpose = strings.Repeat("01234567890", 20) + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Purpose = "1234" + if err := o.IsValid(); err != nil { + t.Fatal(err) + } } func TestChannelPreSave(t *testing.T) { diff --git a/model/client.go b/model/client.go index 4d2c49e70..5533c117f 100644 --- a/model/client.go +++ b/model/client.go @@ -452,8 +452,17 @@ func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) { } } -func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/channels/update_desc", MapToJson(data)); err != nil { +func (c *Client) UpdateChannelHeader(data map[string]string) (*Result, *AppError) { + if r, err := c.DoApiPost("/channels/update_header", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil + } +} + +func (c *Client) UpdateChannelPurpose(data map[string]string) (*Result, *AppError) { + if r, err := c.DoApiPost("/channels/update_purpose", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go index 80fe75130..14896c0a0 100644 --- a/store/sql_channel_store.go +++ b/store/sql_channel_store.go @@ -25,7 +25,8 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore { table.ColMap("DisplayName").SetMaxSize(64) table.ColMap("Name").SetMaxSize(64) table.SetUniqueTogether("Name", "TeamId") - table.ColMap("Description").SetMaxSize(1024) + table.ColMap("Header").SetMaxSize(1024) + table.ColMap("Purpose").SetMaxSize(128) table.ColMap("CreatorId").SetMaxSize(26) tablem := db.AddTableWithName(model.ChannelMember{}, "ChannelMembers").SetKeys(false, "ChannelId", "UserId") @@ -83,6 +84,11 @@ func (s SqlChannelStore) UpgradeSchemaIfNeeded() { s.RemoveColumnIfExists("ChannelMembers", "NotifyLevel") } + + // BEGIN REMOVE AFTER 1.2.0 + s.RenameColumnIfExists("Channels", "Description", "Header", "varchar(1024)") + s.CreateColumnIfNotExists("Channels", "Purpose", "varchar(1024)", "varchar(1024)", "") + // END REMOVE AFTER 1.2.0 } func (s SqlChannelStore) CreateIndexesIfNotExists() { diff --git a/store/sql_store.go b/store/sql_store.go index d5c84d522..8965fef64 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -364,27 +364,26 @@ func (ss SqlStore) RemoveColumnIfExists(tableName string, columnName string) boo return true } -// func (ss SqlStore) RenameColumnIfExists(tableName string, oldColumnName string, newColumnName string, colType string) bool { - -// // XXX TODO FIXME this should be removed after 0.6.0 -// if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { -// return false -// } - -// if !ss.DoesColumnExist(tableName, oldColumnName) { -// return false -// } +func (ss SqlStore) RenameColumnIfExists(tableName string, oldColumnName string, newColumnName string, colType string) bool { + if !ss.DoesColumnExist(tableName, oldColumnName) { + return false + } -// _, err := ss.GetMaster().Exec("ALTER TABLE " + tableName + " CHANGE " + oldColumnName + " " + newColumnName + " " + colType) + var err error + if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL { + _, err = ss.GetMaster().Exec("ALTER TABLE " + tableName + " CHANGE " + oldColumnName + " " + newColumnName + " " + colType) + } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { + _, err = ss.GetMaster().Exec("ALTER TABLE " + tableName + " RENAME COLUMN " + oldColumnName + " TO " + newColumnName) + } -// if err != nil { -// l4g.Critical("Failed to rename column %v", err) -// time.Sleep(time.Second) -// panic("Failed to drop column " + err.Error()) -// } + if err != nil { + l4g.Critical("Failed to rename column %v", err) + time.Sleep(time.Second) + panic("Failed to drop column " + err.Error()) + } -// return true -// } + return true +} func (ss SqlStore) CreateIndexIfNotExists(indexName string, tableName string, columnName string) { ss.createIndexIfNotExists(indexName, tableName, columnName, INDEX_TYPE_DEFAULT) diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index c8af2553d..f0a31ce90 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -90,8 +90,9 @@ export default class AccessHistoryModal extends React.Component { case '/channels/update': currentAuditDesc = 'Updated the ' + channelName + ' channel/group name'; break; - case '/channels/update_desc': - currentAuditDesc = 'Updated the ' + channelName + ' channel/group description'; + case '/channels/update_desc': // support the old path + case '/channels/update_header': + currentAuditDesc = 'Updated the ' + channelName + ' channel/group header'; break; default: let userIdField = []; diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx index dd8812ad0..a945a551c 100644 --- a/web/react/components/admin_console/team_analytics.jsx +++ b/web/react/components/admin_console/team_analytics.jsx @@ -56,6 +56,8 @@ export default class TeamAnalytics extends React.Component { teamId, 'post_counts_day', (data) => { + data.reverse(); + var chartData = { labels: [], datasets: [{ @@ -89,6 +91,8 @@ export default class TeamAnalytics extends React.Component { teamId, 'user_counts_with_posts_day', (data) => { + data.reverse(); + var chartData = { labels: [], datasets: [{ diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx index ffb412159..b44aba56e 100644 --- a/web/react/components/admin_console/team_users.jsx +++ b/web/react/components/admin_console/team_users.jsx @@ -33,14 +33,6 @@ export default class UserList extends React.Component { this.getTeamProfiles(this.props.team.id); } - // this.setState({ - // teamId: this.state.teamId, - // users: this.state.users, - // serverError: this.state.serverError, - // showPasswordModal: this.state.showPasswordModal, - // user: this.state.user - // }); - getTeamProfiles(teamId) { Client.getProfilesForTeam( teamId, @@ -95,8 +87,6 @@ export default class UserList extends React.Component { } doPasswordResetDismiss() { - this.state.showPasswordModal = false; - this.state.user = null; this.setState({ teamId: this.state.teamId, users: this.state.users, diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index d66777cc6..101fd85e5 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -11,6 +11,7 @@ const TextFormatting = require('../utils/text_formatting.jsx'); const Utils = require('../utils/utils.jsx'); const MessageWrapper = require('./message_wrapper.jsx'); const PopoverListMembers = require('./popover_list_members.jsx'); +const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx'); const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); const Constants = require('../utils/constants.jsx'); @@ -27,7 +28,9 @@ export default class ChannelHeader extends React.Component { this.handleLeave = this.handleLeave.bind(this); this.searchMentions = this.searchMentions.bind(this); - this.state = this.getStateFromStores(); + const state = this.getStateFromStores(); + state.showEditChannelPurposeModal = false; + this.state = state; } getStateFromStores() { return { @@ -110,11 +113,11 @@ export default class ChannelHeader extends React.Component { bSize='large' placement='bottom' className='description' - onMouseOver={() => this.refs.descriptionOverlay.show()} - onMouseOut={() => this.refs.descriptionOverlay.hide()} + onMouseOver={() => this.refs.headerOverlay.show()} + onMouseOut={() => this.refs.headerOverlay.hide()} > <MessageWrapper - message={channel.description} + message={channel.header} /> </Popover> ); @@ -144,7 +147,7 @@ export default class ChannelHeader extends React.Component { if (isDirect) { dropdownContents.push( <li - key='edit_description_direct' + key='edit_header_direct' role='presentation' > <a @@ -152,11 +155,11 @@ export default class ChannelHeader extends React.Component { href='#' data-toggle='modal' data-target='#edit_channel' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} > - Set Channel Description... + Set Channel Header... </a> </li> ); @@ -216,7 +219,7 @@ export default class ChannelHeader extends React.Component { dropdownContents.push( <li - key='set_channel_description' + key='set_channel_header' role='presentation' > <a @@ -224,11 +227,25 @@ export default class ChannelHeader extends React.Component { href='#' data-toggle='modal' data-target='#edit_channel' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} > - Set {channelTerm} Description... + Set {channelTerm} Header... + </a> + </li> + ); + dropdownContents.push( + <li + key='set_channel_purpose' + role='presentation' + > + <a + role='menuitem' + href='#' + onClick={() => this.setState({showEditChannelPurposeModal: true})} + > + Set {channelTerm} Purpose... </a> </li> ); @@ -307,84 +324,91 @@ export default class ChannelHeader extends React.Component { } return ( - <table className='channel-header alt'> - <tbody> - <tr> - <th> - <div className='channel-header__info'> - <div className='dropdown'> + <div> + <table className='channel-header alt'> + <tbody> + <tr> + <th> + <div className='channel-header__info'> + <div className='dropdown'> + <a + href='#' + className='dropdown-toggle theme' + type='button' + id='channel_header_dropdown' + data-toggle='dropdown' + aria-expanded='true' + > + <strong className='heading'>{channelTitle} </strong> + <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' /> + </a> + <ul + className='dropdown-menu' + role='menu' + aria-labelledby='channel_header_dropdown' + > + {dropdownContents} + </ul> + </div> + <OverlayTrigger + trigger={['hover', 'focus']} + placement='bottom' + overlay={popoverContent} + ref='headerOverlay' + > + <div + onClick={TextFormatting.handleClick} + className='description' + dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}} + /> + </OverlayTrigger> + </div> + </th> + <th> + <PopoverListMembers + members={this.state.users} + channelId={channel.id} + /> + </th> + <th className='search-bar__container'><NavbarSearchBox /></th> + <th> + <div className='dropdown channel-header__links'> <a href='#' className='dropdown-toggle theme' type='button' - id='channel_header_dropdown' + id='channel_header_right_dropdown' data-toggle='dropdown' aria-expanded='true' > - <strong className='heading'>{channelTitle} </strong> - <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' /> + <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> </a> <ul - className='dropdown-menu' + className='dropdown-menu dropdown-menu-right' role='menu' - aria-labelledby='channel_header_dropdown' + aria-labelledby='channel_header_right_dropdown' > - {dropdownContents} + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.searchMentions} + > + Recent Mentions + </a> + </li> </ul> </div> - <OverlayTrigger - trigger={['hover', 'focus']} - placement='bottom' - overlay={popoverContent} - ref='descriptionOverlay' - > - <div - onClick={TextFormatting.handleClick} - className='description' - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.description, {singleline: true, mentionHighlight: false})}} - /> - </OverlayTrigger> - </div> - </th> - <th> - <PopoverListMembers - members={this.state.users} - channelId={channel.id} - /> - </th> - <th className='search-bar__container'><NavbarSearchBox /></th> - <th> - <div className='dropdown channel-header__links'> - <a - href='#' - className='dropdown-toggle theme' - type='button' - id='channel_header_right_dropdown' - data-toggle='dropdown' - aria-expanded='true' - > - <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> - </a> - <ul - className='dropdown-menu dropdown-menu-right' - role='menu' - aria-labelledby='channel_header_right_dropdown' - > - <li role='presentation'> - <a - role='menuitem' - href='#' - onClick={this.searchMentions} - > - Recent Mentions - </a> - </li> - </ul> - </div> - </th> - </tr> - </tbody> - </table> + </th> + </tr> + </tbody> + </table> + <EditChannelPurposeModal + show={this.state.showEditChannelPurposeModal} + onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})} + channel={channel} + /> + </div> ); } } diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 18936e808..058594165 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -8,6 +8,7 @@ const SocketStore = require('../stores/socket_store.jsx'); const ChannelStore = require('../stores/channel_store.jsx'); const UserStore = require('../stores/user_store.jsx'); const PostStore = require('../stores/post_store.jsx'); +const PreferenceStore = require('../stores/preference_store.jsx'); const Textbox = require('./textbox.jsx'); const MsgTyping = require('./msg_typing.jsx'); const FileUpload = require('./file_upload.jsx'); @@ -27,7 +28,7 @@ export default class CreateComment extends React.Component { this.handleSubmit = this.handleSubmit.bind(this); this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this); this.handleUserInput = this.handleUserInput.bind(this); - this.handleArrowUp = this.handleArrowUp.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); this.handleUploadStart = this.handleUploadStart.bind(this); this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this); this.handleUploadError = this.handleUploadError.bind(this); @@ -36,6 +37,7 @@ export default class CreateComment extends React.Component { this.handleSubmit = this.handleSubmit.bind(this); this.getFileCount = this.getFileCount.bind(this); this.handleResize = this.handleResize.bind(this); + this.onPreferenceChange = this.onPreferenceChange.bind(this); PostStore.clearCommentDraftUploads(); @@ -45,15 +47,23 @@ export default class CreateComment extends React.Component { uploadsInProgress: draft.uploadsInProgress, previews: draft.previews, submitting: false, - windowWidth: Utils.windowWidth() + windowWidth: Utils.windowWidth(), + ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value }; } componentDidMount() { + PreferenceStore.addChangeListener(this.onPreferenceChange); window.addEventListener('resize', this.handleResize); } componentWillUnmount() { + PreferenceStore.removeChangeListener(this.onPreferenceChange); window.removeEventListener('resize', this.handleResize); } + onPreferenceChange() { + this.setState({ + ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value + }); + } handleResize() { this.setState({windowWidth: Utils.windowWidth()}); } @@ -140,10 +150,12 @@ export default class CreateComment extends React.Component { this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); } commentMsgKeyPress(e) { - if (e.which === 13 && !e.shiftKey && !e.altKey) { - e.preventDefault(); - ReactDOM.findDOMNode(this.refs.textbox).blur(); - this.handleSubmit(e); + if (this.state.ctrlSend === 'true' && e.ctrlKey || this.state.ctrlSend === 'false') { + if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) { + e.preventDefault(); + ReactDOM.findDOMNode(this.refs.textbox).blur(); + this.handleSubmit(e); + } } const t = Date.now(); @@ -161,7 +173,12 @@ export default class CreateComment extends React.Component { $('.post-right__scroll').perfectScrollbar('update'); this.setState({messageText: messageText}); } - handleArrowUp(e) { + handleKeyDown(e) { + if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) { + this.commentMsgKeyPress(e); + return; + } + if (e.keyCode === KeyCodes.UP && this.state.messageText === '') { e.preventDefault(); @@ -313,7 +330,7 @@ export default class CreateComment extends React.Component { <Textbox onUserInput={this.handleUserInput} onKeyPress={this.commentMsgKeyPress} - onKeyDown={this.handleArrowUp} + onKeyDown={this.handleKeyDown} messageText={this.state.messageText} createMessage='Add a comment...' initialText='' diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 32ee31efe..cdbc3bc6d 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -8,6 +8,7 @@ const ChannelStore = require('../stores/channel_store.jsx'); const PostStore = require('../stores/post_store.jsx'); const UserStore = require('../stores/user_store.jsx'); const SocketStore = require('../stores/socket_store.jsx'); +const PreferenceStore = require('../stores/preference_store.jsx'); const MsgTyping = require('./msg_typing.jsx'); const Textbox = require('./textbox.jsx'); const FileUpload = require('./file_upload.jsx'); @@ -36,9 +37,10 @@ export default class CreatePost extends React.Component { this.removePreview = this.removePreview.bind(this); this.onChange = this.onChange.bind(this); this.getFileCount = this.getFileCount.bind(this); - this.handleArrowUp = this.handleArrowUp.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); this.handleResize = this.handleResize.bind(this); this.sendMessage = this.sendMessage.bind(this); + this.onPreferenceChange = this.onPreferenceChange.bind(this); PostStore.clearDraftUploads(); @@ -52,8 +54,16 @@ export default class CreatePost extends React.Component { submitting: false, initialText: draft.messageText, windowWidth: Utils.windowWidth(), - windowHeight: Utils.windowHeight() + windowHeight: Utils.windowHeight(), + ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value }; + + PreferenceStore.addChangeListener(this.onPreferenceChange); + } + onPreferenceChange() { + this.setState({ + ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value + }); } handleResize() { this.setState({ @@ -201,10 +211,12 @@ export default class CreatePost extends React.Component { ); } postMsgKeyPress(e) { - if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) { - e.preventDefault(); - ReactDOM.findDOMNode(this.refs.textbox).blur(); - this.handleSubmit(e); + if (this.state.ctrlSend === 'true' && e.ctrlKey || this.state.ctrlSend === 'false') { + if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) { + e.preventDefault(); + ReactDOM.findDOMNode(this.refs.textbox).blur(); + this.handleSubmit(e); + } } const t = Date.now(); @@ -328,7 +340,12 @@ export default class CreatePost extends React.Component { const draft = PostStore.getDraft(channelId); return draft.previews.length + draft.uploadsInProgress.length; } - handleArrowUp(e) { + handleKeyDown(e) { + if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) { + this.postMsgKeyPress(e); + return; + } + if (e.keyCode === KeyCodes.UP && this.state.messageText === '') { e.preventDefault(); @@ -393,7 +410,7 @@ export default class CreatePost extends React.Component { <Textbox onUserInput={this.handleUserInput} onKeyPress={this.postMsgKeyPress} - onKeyDown={this.handleArrowUp} + onKeyDown={this.handleKeyDown} onHeightChange={this.resizePostHolder} messageText={this.state.messageText} createMessage='Write a message...' diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx index d63a1db30..6f3826f75 100644 --- a/web/react/components/edit_channel_modal.jsx +++ b/web/react/components/edit_channel_modal.jsx @@ -14,7 +14,7 @@ export default class EditChannelModal extends React.Component { this.onShow = this.onShow.bind(this); this.state = { - description: '', + header: '', title: '', channelId: '', serverError: '' @@ -28,32 +28,32 @@ export default class EditChannelModal extends React.Component { return; } - data.channel_description = this.state.description.trim(); + data.channel_header = this.state.header.trim(); - Client.updateChannelDesc(data, - function handleUpdateSuccess() { + Client.updateChannelHeader(data, + () => { this.setState({serverError: ''}); AsyncClient.getChannel(this.state.channelId); $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide'); - }.bind(this), - function handleUpdateError(err) { - if (err.message === 'Invalid channel_description parameter') { - this.setState({serverError: 'This description is too long, please enter a shorter one'}); + }, + (err) => { + if (err.message === 'Invalid channel_header parameter') { + this.setState({serverError: 'This channel header is too long, please enter a shorter one'}); } else { this.setState({serverError: err.message}); } - }.bind(this) + } ); } handleUserInput(e) { - this.setState({description: e.target.value}); + this.setState({header: e.target.value}); } handleClose() { - this.setState({description: '', serverError: ''}); + this.setState({header: '', serverError: ''}); } onShow(e) { const button = e.relatedTarget; - this.setState({description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''}); + this.setState({header: $(button).attr('data-header'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''}); } componentDidMount() { $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow); @@ -73,7 +73,7 @@ export default class EditChannelModal extends React.Component { className='modal-title' ref='title' > - Edit Description + Edit Header </h4> ); if (this.state.title) { @@ -82,7 +82,7 @@ export default class EditChannelModal extends React.Component { className='modal-title' ref='title' > - Edit Description for <span className='name'>{this.state.title}</span> + Edit Header for <span className='name'>{this.state.title}</span> </h4> ); } @@ -113,9 +113,8 @@ export default class EditChannelModal extends React.Component { <textarea className='form-control no-resize' rows='6' - ref='channelDesc' maxLength='1024' - value={this.state.description} + value={this.state.header} onChange={this.handleUserInput} /> {serverError} diff --git a/web/react/components/edit_channel_purpose_modal.jsx b/web/react/components/edit_channel_purpose_modal.jsx new file mode 100644 index 000000000..d8102642e --- /dev/null +++ b/web/react/components/edit_channel_purpose_modal.jsx @@ -0,0 +1,118 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +const AsyncClient = require('../utils/async_client.jsx'); +const Client = require('../utils/client.jsx'); +const Modal = ReactBootstrap.Modal; + +export default class EditChannelPurposeModal extends React.Component { + constructor(props) { + super(props); + + this.handleHide = this.handleHide.bind(this); + this.handleSave = this.handleSave.bind(this); + + this.state = {serverError: ''}; + } + + handleHide() { + this.setState({serverError: ''}); + + if (this.props.onModalDismissed) { + this.props.onModalDismissed(); + } + } + + handleSave() { + if (!this.props.channel) { + return; + } + + const data = { + channel_id: this.props.channel.id, + channel_purpose: ReactDOM.findDOMNode(this.refs.purpose).value.trim() + }; + + Client.updateChannelPurpose(data, + () => { + AsyncClient.getChannel(this.props.channel.id); + + this.handleHide(); + }, + (err) => { + if (err.message === 'Invalid channel_purpose parameter') { + this.setState({serverError: 'This channel purpose is too long, please enter a shorter one'}); + } else { + this.setState({serverError: err.message}); + } + } + ); + } + + render() { + if (!this.props.show) { + return null; + } + + let serverError = null; + if (this.state.serverError) { + serverError = ( + <div className='form-group has-error'> + <br/> + <label className='control-label'>{this.state.serverError}</label> + </div> + ); + } + + let title = <span>{'Edit Purpose'}</span>; + if (this.props.channel.display_name) { + title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>; + } + + return ( + <Modal + className='modal-edit-channel-purpose' + show={this.props.show} + onHide={this.handleHide} + > + <Modal.Header closeButton={true}> + <Modal.Title> + {title} + </Modal.Title> + </Modal.Header> + <Modal.Body> + <textarea + ref='purpose' + className='form-control no-resize' + rows='6' + maxLength='128' + defaultValue={this.props.channel.purpose} + /> + {serverError} + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-default' + onClick={this.handleHide} + > + {'Cancel'} + </button> + <button + type='button' + className='btn btn-primary' + onClick={this.handleSave} + > + {'Save'} + </button> + </Modal.Footer> + </Modal> + ); + } +} + +EditChannelPurposeModal.propTypes = { + show: React.PropTypes.bool.isRequired, + channel: React.PropTypes.object, + onModalDismissed: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index 4d4e8390c..e707e32f5 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -270,7 +270,7 @@ export default class FileAttachment extends React.Component { href={fileUrl} download={filenameString} data-toggle='tooltip' - title={filenameString} + title={'Download ' + filenameString} className='post-image__name' > {trimmedFilename} diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx index 325e86f3d..8839bc3c7 100644 --- a/web/react/components/get_link_modal.jsx +++ b/web/react/components/get_link_modal.jsx @@ -98,7 +98,7 @@ export default class GetLinkModal extends React.Component { <br /><br /> </p> <textarea - className='form-control no-resize' + className='form-control no-resize min-height' readOnly='true' ref='textarea' value={this.state.value} diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx index a0084ad30..c4f831c2e 100644 --- a/web/react/components/more_channels.jsx +++ b/web/react/components/more_channels.jsx @@ -109,7 +109,7 @@ export default class MoreChannels extends React.Component { <tr key={channel.id}> <td> <p className='more-name'>{channel.display_name}</p> - <p className='more-description'>{channel.description}</p> + <p className='more-purpose'>{channel.purpose}</p> </td> <td className='td--action'> {joinButton} diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index f9cd525fd..f7778f25f 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -8,6 +8,7 @@ var ChannelStore = require('../stores/channel_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); var MessageWrapper = require('./message_wrapper.jsx'); var NotifyCounts = require('./notify_counts.jsx'); +const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx'); const Utils = require('../utils/utils.jsx'); var Constants = require('../utils/constants.jsx'); @@ -26,7 +27,9 @@ export default class Navbar extends React.Component { this.createCollapseButtons = this.createCollapseButtons.bind(this); this.createDropdown = this.createDropdown.bind(this); - this.state = this.getStateFromStores(); + const state = this.getStateFromStores(); + state.showEditChannelPurposeModal = false; + this.state = state; } getStateFromStores() { return { @@ -106,22 +109,35 @@ export default class Navbar extends React.Component { </li> ); - var setChannelDescriptionOption = ( + var setChannelHeaderOption = ( <li role='presentation'> <a role='menuitem' href='#' data-toggle='modal' data-target='#edit_channel' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} > - Set Channel Description... + Set Channel Header... </a> </li> ); + var setChannelPurposeOption = null; + if (!isDirect) { + setChannelPurposeOption = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={() => this.setState({showEditChannelPurposeModal: true})} + /> + </li> + ); + } + var addMembersOption; var leaveChannelOption; if (!isDirect && !ChannelStore.isDefault(channel)) { @@ -249,7 +265,8 @@ export default class Navbar extends React.Component { {viewInfoOption} {addMembersOption} {manageMembersOption} - {setChannelDescriptionOption} + {setChannelHeaderOption} + {setChannelPurposeOption} {notificationPreferenceOption} {renameChannelOption} {deleteChannelOption} @@ -335,10 +352,10 @@ export default class Navbar extends React.Component { <Popover bsStyle='info' placement='bottom' - id='description-popover' + id='header-popover' > <MessageWrapper - message={channel.description} + message={channel.header} options={{singleline: true, mentionHighlight: false}} /> </Popover> @@ -360,20 +377,20 @@ export default class Navbar extends React.Component { } } - if (channel.description.length === 0) { + if (channel.header.length === 0) { popoverContent = ( <Popover bsStyle='info' placement='bottom' - id='description-popover' + id='header-popover' > <div> - {'No channel description yet.'} + {'No channel header yet.'} <br/> <a href='#' data-toggle='modal' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} data-target='#edit_channel' @@ -392,17 +409,24 @@ export default class Navbar extends React.Component { var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent); return ( - <nav - className='navbar navbar-default navbar-fixed-top' - role='navigation' - > - <div className='container-fluid theme'> - <div className='navbar-header'> - {collapseButtons} - {channelMenuDropdown} + <div> + <nav + className='navbar navbar-default navbar-fixed-top' + role='navigation' + > + <div className='container-fluid theme'> + <div className='navbar-header'> + {collapseButtons} + {channelMenuDropdown} + </div> </div> - </div> - </nav> + </nav> + <EditChannelPurposeModal + show={this.state.showEditChannelPurposeModal} + onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})} + channel={channel} + /> + </div> ); } } diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 2b68645e5..2b0f3c40e 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -58,6 +58,7 @@ export default class NavbarDropdown extends React.Component { TeamStore.addChangeListener(this.onListenerChange); $(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => { + $('.sidebar--left .dropdown-menu').scrollTop(0); this.blockToggle = true; setTimeout(() => { this.blockToggle = false; diff --git a/web/react/components/new_channel_flow.jsx b/web/react/components/new_channel_flow.jsx index 186cfc2b0..d6280d118 100644 --- a/web/react/components/new_channel_flow.jsx +++ b/web/react/components/new_channel_flow.jsx @@ -30,7 +30,7 @@ export default class NewChannelFlow extends React.Component { flowState: SHOW_NEW_CHANNEL, channelDisplayName: '', channelName: '', - channelDescription: '', + channelPurpose: '', nameModified: false }; } @@ -43,7 +43,7 @@ export default class NewChannelFlow extends React.Component { flowState: SHOW_NEW_CHANNEL, channelDisplayName: '', channelName: '', - channelDescription: '', + channelPurpose: '', nameModified: false }); } @@ -65,7 +65,7 @@ export default class NewChannelFlow extends React.Component { const cu = UserStore.getCurrentUser(); channel.team_id = cu.team_id; - channel.description = this.state.channelDescription; + channel.purpose = this.state.channelPurpose; channel.type = this.state.channelType; Client.createChannel(channel, @@ -109,7 +109,7 @@ export default class NewChannelFlow extends React.Component { channelDataChanged(data) { this.setState({ channelDisplayName: data.displayName, - channelDescription: data.description + channelPurpose: data.purpose }); if (!this.state.nameModified) { this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())}); @@ -119,7 +119,7 @@ export default class NewChannelFlow extends React.Component { const channelData = { name: this.state.channelName, displayName: this.state.channelDisplayName, - description: this.state.channelDescription + purpose: this.state.channelPurpose }; let showChannelModal = false; diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx index 4e6280c99..c0cea496f 100644 --- a/web/react/components/new_channel_modal.jsx +++ b/web/react/components/new_channel_modal.jsx @@ -36,7 +36,7 @@ export default class NewChannelModal extends React.Component { handleChange() { const newData = { displayName: ReactDOM.findDOMNode(this.refs.display_name).value, - description: ReactDOM.findDOMNode(this.refs.channel_desc).value + purpose: ReactDOM.findDOMNode(this.refs.channel_purpose).value }; this.props.onDataChanged(newData); } @@ -136,22 +136,22 @@ export default class NewChannelModal extends React.Component { </div> <div className='form-group less'> <div className='col-sm-3'> - <label className='form__label control-label'>{'Description'}</label> + <label className='form__label control-label'>{'Purpose'}</label> <label className='form__label light'>{'(optional)'}</label> </div> <div className='col-sm-9'> <textarea className='form-control no-resize' - ref='channel_desc' + ref='channel_purpose' rows='4' - placeholder='Description' - maxLength='1024' - value={this.props.channelData.description} + placeholder='Purpose' + maxLength='128' + value={this.props.channelData.purpose} onChange={this.handleChange} tabIndex='2' /> <p className='input__help'> - {'Description helps others decide whether to join this channel.'} + {`Describe how this ${channelTerm} should be used.`} </p> {serverError} </div> diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 45eae8c6a..7138e2cb4 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -297,7 +297,7 @@ export default class PostBody extends React.Component { } let embed; - if (filenames.length === 0 && this.state.links) { + if (filenames.length === 0 && this.state.links && this.state.links.length > 0) { embed = this.createEmbed(this.state.links[0]); } diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 3ceef478c..0d69e56bf 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -358,11 +358,11 @@ export default class PostList extends React.Component { href='#' data-toggle='modal' data-target='#edit_channel' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} > - <i className='fa fa-pencil'></i>{'Set a description'} + <i className='fa fa-pencil'></i>{'Set a header'} </a> </div> ); @@ -413,11 +413,11 @@ export default class PostList extends React.Component { href='#' data-toggle='modal' data-target='#edit_channel' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} > - <i className='fa fa-pencil'></i>{'Set a description'} + <i className='fa fa-pencil'></i>{'Set a header'} </a> <a className='intro-links' @@ -479,11 +479,11 @@ export default class PostList extends React.Component { href='#' data-toggle='modal' data-target='#edit_channel' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} > - <i className='fa fa-pencil'></i>{'Set a description'} + <i className='fa fa-pencil'></i>{'Set a header'} </a> <a className='intro-links' diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx index 3d4d9bf45..c40409dcc 100644 --- a/web/react/components/register_app_modal.jsx +++ b/web/react/components/register_app_modal.jsx @@ -96,75 +96,74 @@ export default class RegisterAppModal extends React.Component { var body = ''; if (this.state.clientId === '') { body = ( - <div className='form-group user-settings'> - <h3>{'Register a New Application'}</h3> - <br/> - <label className='col-sm-4 control-label'>{'Application Name'}</label> - <div className='col-sm-7'> - <input - ref='name' - className='form-control' - type='text' - placeholder='Required' - /> - {nameError} - </div> - <br/> - <br/> - <label className='col-sm-4 control-label'>{'Homepage URL'}</label> - <div className='col-sm-7'> - <input - ref='homepage' - className='form-control' - type='text' - placeholder='Required' - /> - {homepageError} - </div> - <br/> - <br/> - <label className='col-sm-4 control-label'>{'Description'}</label> - <div className='col-sm-7'> - <input - ref='desc' - className='form-control' - type='text' - placeholder='Optional' - /> - </div> - <br/> - <br/> - <label className='col-sm-4 control-label'>{'Callback URL'}</label> - <div className='col-sm-7'> - <textarea - ref='callback' - className='form-control' - type='text' - placeholder='Required' - rows='5' - /> - {callbackError} + <div className='settings-modal'> + <div className='form-horizontal user-settings'> + <h4 className='padding-bottom x3'>{'Register a New Application'}</h4> + <div className='row'> + <label className='col-sm-4 control-label'>{'Application Name'}</label> + <div className='col-sm-7'> + <input + ref='name' + className='form-control' + type='text' + placeholder='Required' + /> + {nameError} + </div> + </div> + <div className='row padding-top x2'> + <label className='col-sm-4 control-label'>{'Homepage URL'}</label> + <div className='col-sm-7'> + <input + ref='homepage' + className='form-control' + type='text' + placeholder='Required' + /> + {homepageError} + </div> + </div> + <div className='row padding-top x2'> + <label className='col-sm-4 control-label'>{'Description'}</label> + <div className='col-sm-7'> + <input + ref='desc' + className='form-control' + type='text' + placeholder='Optional' + /> + </div> + </div> + <div className='row padding-top padding-bottom x2'> + <label className='col-sm-4 control-label'>{'Callback URL'}</label> + <div className='col-sm-7'> + <textarea + ref='callback' + className='form-control' + type='text' + placeholder='Required' + rows='5' + /> + {callbackError} + </div> + </div> + {serverError} + <hr /> + <a + className='btn btn-sm theme pull-right' + href='#' + data-dismiss='modal' + aria-label='Close' + > + {'Cancel'} + </a> + <a + className='btn btn-sm btn-primary pull-right' + onClick={this.register} + > + {'Register'} + </a> </div> - <br/> - <br/> - <br/> - <br/> - <br/> - {serverError} - <a - className='btn btn-sm theme pull-right' - href='#' - data-dismiss='modal' - aria-label='Close' - > - {'Cancel'} - </a> - <a - className='btn btn-sm btn-primary pull-right' - onClick={this.register} - > - {'Register'} - </a> </div> ); } else { diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx index 03c7b894c..f7d772677 100644 --- a/web/react/components/search_autocomplete.jsx +++ b/web/react/components/search_autocomplete.jsx @@ -10,6 +10,7 @@ const patterns = new Map([ ['channels', /\b(?:in|channel):\s*(\S*)$/i], ['users', /\bfrom:\s*(\S*)$/i] ]); +const Popover = ReactBootstrap.Popover; export default class SearchAutocomplete extends React.Component { constructor(props) { @@ -36,6 +37,11 @@ export default class SearchAutocomplete extends React.Component { $(document).on('click', this.handleDocumentClick); } + componentDidUpdate() { + $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').perfectScrollbar(); + $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').css('max-height', $(window).height() - 200); + } + componentWillUnmount() { $(document).off('click', this.handleDocumentClick); } @@ -193,7 +199,7 @@ export default class SearchAutocomplete extends React.Component { if (this.state.mode === 'channels') { suggestions = this.state.suggestions.map((channel, index) => { - let className = 'search-autocomplete__channel'; + let className = 'search-autocomplete__item'; if (this.state.selection === index) { className += ' selected'; } @@ -211,7 +217,7 @@ export default class SearchAutocomplete extends React.Component { }); } else if (this.state.mode === 'users') { suggestions = this.state.suggestions.map((user, index) => { - let className = 'search-autocomplete__user'; + let className = 'search-autocomplete__item'; if (this.state.selection === index) { className += ' selected'; } @@ -224,7 +230,7 @@ export default class SearchAutocomplete extends React.Component { className={className} > <img - className='profile-img' + className='profile-img rounded' src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at} /> {user.username} @@ -234,12 +240,15 @@ export default class SearchAutocomplete extends React.Component { } return ( - <div - ref='container' - className='search-autocomplete' + <Popover + ref='searchPopover' + onShow={this.componentDidMount} + id='search-autocomplete__popover' + className='search-help-popover autocomplete visible' + placement='bottom' > {suggestions} - </div> + </Popover> ); } } diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx index 774f98a43..d6c4b0d4b 100644 --- a/web/react/components/setting_item_max.jsx +++ b/web/react/components/setting_item_max.jsx @@ -35,8 +35,10 @@ export default class SettingItemMax extends React.Component { var widthClass; if (this.props.width === 'full') { widthClass = 'col-sm-12'; - } else { + } else if (this.props.width === 'medium') { widthClass = 'col-sm-10 col-sm-offset-2'; + } else { + widthClass = 'col-sm-9 col-sm-offset-3'; } return ( diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx index b6bcb13a6..e69412cca 100644 --- a/web/react/components/setting_picture.jsx +++ b/web/react/components/setting_picture.jsx @@ -42,7 +42,7 @@ export default class SettingPicture extends React.Component { img = ( <img ref='image' - className='profile-img' + className='profile-img rounded' src='' /> ); @@ -50,7 +50,7 @@ export default class SettingPicture extends React.Component { img = ( <img ref='image' - className='profile-img' + className='profile-img rounded' src={this.props.src} /> ); diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index de28a8374..65e4c6d7e 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -5,6 +5,9 @@ var NavbarDropdown = require('./navbar_dropdown.jsx'); var UserStore = require('../stores/user_store.jsx'); const Utils = require('../utils/utils.jsx'); +const Tooltip = ReactBootstrap.Tooltip; +const OverlayTrigger = ReactBootstrap.OverlayTrigger; + export default class SidebarHeader extends React.Component { constructor(props) { super(props); @@ -47,7 +50,15 @@ export default class SidebarHeader extends React.Component { {profilePicture} <div className='header__info'> <div className='user__name'>{'@' + me.username}</div> + <OverlayTrigger + trigger={['hover', 'focus']} + delayShow={1000} + placement='bottom' + overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDisplayName}</Tooltip>} + ref='descriptionOverlay' + > <div className='team__name'>{this.props.teamDisplayName}</div> + </OverlayTrigger> </div> </a> <NavbarDropdown diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx index ff4ccd4d8..021713f04 100644 --- a/web/react/components/team_signup_with_email.jsx +++ b/web/react/components/team_signup_with_email.jsx @@ -71,7 +71,7 @@ export default class EmailSignUpPage extends React.Component { className='btn btn-md btn-primary' type='submit' > - {'Sign up'} + {'Create Team'} </button> {serverError} </div> diff --git a/web/react/components/user_settings/code_theme_chooser.jsx b/web/react/components/user_settings/code_theme_chooser.jsx deleted file mode 100644 index eef4b24ba..000000000 --- a/web/react/components/user_settings/code_theme_chooser.jsx +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -var Constants = require('../../utils/constants.jsx'); - -export default class CodeThemeChooser extends React.Component { - constructor(props) { - super(props); - this.state = {}; - } - render() { - const theme = this.props.theme; - - const premadeThemes = []; - for (const k in Constants.CODE_THEMES) { - if (Constants.CODE_THEMES.hasOwnProperty(k)) { - let activeClass = ''; - if (k === theme.codeTheme) { - activeClass = 'active'; - } - - premadeThemes.push( - <div - className='col-xs-6 col-sm-3 premade-themes' - key={'premade-theme-key' + k} - > - <div - className={activeClass} - onClick={() => this.props.updateTheme(k)} - > - <label> - <img - className='img-responsive' - src={'/static/images/themes/code_themes/' + k + '.png'} - /> - <div className='theme-label'>{Constants.CODE_THEMES[k]}</div> - </label> - </div> - </div> - ); - } - } - - return ( - <div className='row'> - {premadeThemes} - </div> - ); - } -} - -CodeThemeChooser.propTypes = { - theme: React.PropTypes.object.isRequired, - updateTheme: React.PropTypes.func.isRequired -}; diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx index 6e9b2205d..4c56db0a1 100644 --- a/web/react/components/user_settings/manage_outgoing_hooks.jsx +++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx @@ -236,6 +236,7 @@ export default class ManageOutgoingHooks extends React.Component { return ( <div key='addOutgoingHook'> <label className='control-label'>{'Add a new outgoing webhook'}</label> + <div className='padding-top divider-light'></div> <div className='padding-top'> <div> <label className='control-label'>{'Channel'}</label> diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx index 15bf961d6..546e26ca3 100644 --- a/web/react/components/user_settings/user_settings.jsx +++ b/web/react/components/user_settings/user_settings.jsx @@ -10,6 +10,7 @@ var AppearanceTab = require('./user_settings_appearance.jsx'); var DeveloperTab = require('./user_settings_developer.jsx'); var IntegrationsTab = require('./user_settings_integrations.jsx'); var DisplayTab = require('./user_settings_display.jsx'); +var AdvancedTab = require('./user_settings_advanced.jsx'); export default class UserSettings extends React.Component { constructor(props) { @@ -110,6 +111,17 @@ export default class UserSettings extends React.Component { /> </div> ); + } else if (this.props.activeTab === 'advanced') { + return ( + <div> + <AdvancedTab + user={this.state.user} + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + updateTab={this.props.updateTab} + /> + </div> + ); } return <div/>; diff --git a/web/react/components/user_settings/user_settings_advanced.jsx b/web/react/components/user_settings/user_settings_advanced.jsx new file mode 100644 index 000000000..910444735 --- /dev/null +++ b/web/react/components/user_settings/user_settings_advanced.jsx @@ -0,0 +1,169 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +const Client = require('../../utils/client.jsx'); +const SettingItemMin = require('../setting_item_min.jsx'); +const SettingItemMax = require('../setting_item_max.jsx'); +const Constants = require('../../utils/constants.jsx'); +const PreferenceStore = require('../../stores/preference_store.jsx'); + +export default class AdvancedSettingsDisplay extends React.Component { + constructor(props) { + super(props); + + this.updateSection = this.updateSection.bind(this); + this.updateSetting = this.updateSetting.bind(this); + this.handleClose = this.handleClose.bind(this); + this.setupInitialState = this.setupInitialState.bind(this); + + this.state = this.setupInitialState(); + } + + setupInitialState() { + const sendOnCtrlEnter = PreferenceStore.getPreference( + Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, + 'send_on_ctrl_enter', + {value: 'false'} + ).value; + + return { + settings: {send_on_ctrl_enter: sendOnCtrlEnter} + }; + } + + updateSetting(setting, value) { + const settings = this.state.settings; + settings[setting] = value; + this.setState(settings); + } + + handleSubmit(setting) { + const preference = PreferenceStore.setPreference( + Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, + setting, + this.state.settings[setting] + ); + + Client.savePreferences([preference], + () => { + PreferenceStore.emitChange(); + this.updateSection(''); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + updateSection(section) { + this.props.updateSection(section); + } + + handleClose() { + this.updateSection(''); + } + + componentDidMount() { + $('#user_settings').on('hidden.bs.modal', this.handleClose); + } + + componentWillUnmount() { + $('#user_settings').off('hidden.bs.modal', this.handleClose); + } + + render() { + const serverError = this.state.serverError || null; + let ctrlSendSection; + + if (this.props.activeSection === 'advancedCtrlSend') { + const ctrlSendActive = [ + this.state.settings.send_on_ctrl_enter === 'true', + this.state.settings.send_on_ctrl_enter === 'false' + ]; + + const inputs = [ + <div key='ctrlSendSetting'> + <div className='radio'> + <label> + <input + type='radio' + checked={ctrlSendActive[0]} + onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'true')} + /> + {'On'} + </label> + <br/> + </div> + <div className='radio'> + <label> + <input + type='radio' + checked={ctrlSendActive[1]} + onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'false')} + /> + {'Off'} + </label> + <br/> + </div> + <div><br/>{'If enabled \'Enter\' inserts a new line and \'Ctrl + Enter\' submits the message.'}</div> + </div> + ]; + + ctrlSendSection = ( + <SettingItemMax + title='Send messages on Ctrl + Enter' + inputs={inputs} + submit={() => this.handleSubmit('send_on_ctrl_enter')} + server_error={serverError} + updateSection={(e) => { + this.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + ctrlSendSection = ( + <SettingItemMin + title='Send messages on Ctrl + Enter' + describe={this.state.settings.send_on_ctrl_enter === 'true' ? 'On' : 'Off'} + updateSection={() => this.props.updateSection('advancedCtrlSend')} + /> + ); + } + + return ( + <div> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>{'×'}</span> + </button> + <h4 + className='modal-title' + ref='title' + > + <i className='modal-back'></i> + {'Advanced Settings'} + </h4> + </div> + <div className='user-settings'> + <h3 className='tab-header'>{'Advanced Settings'}</h3> + <div className='divider-dark first'/> + {ctrlSendSection} + <div className='divider-dark'/> + </div> + </div> + ); + } +} + +AdvancedSettingsDisplay.propTypes = { + user: React.PropTypes.object, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func, + activeSection: React.PropTypes.string +}; diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx index e94894a1d..8c62a189d 100644 --- a/web/react/components/user_settings/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -7,7 +7,6 @@ var Utils = require('../../utils/utils.jsx'); const CustomThemeChooser = require('./custom_theme_chooser.jsx'); const PremadeThemeChooser = require('./premade_theme_chooser.jsx'); -const CodeThemeChooser = require('./code_theme_chooser.jsx'); const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx'); const Constants = require('../../utils/constants.jsx'); const ActionTypes = Constants.ActionTypes; @@ -19,14 +18,12 @@ export default class UserSettingsAppearance extends React.Component { this.onChange = this.onChange.bind(this); this.submitTheme = this.submitTheme.bind(this); this.updateTheme = this.updateTheme.bind(this); - this.updateCodeTheme = this.updateCodeTheme.bind(this); this.handleClose = this.handleClose.bind(this); this.handleImportModal = this.handleImportModal.bind(this); this.state = this.getStateFromStores(); this.originalTheme = this.state.theme; - this.originalCodeTheme = this.state.theme.codeTheme; } componentDidMount() { UserStore.addChangeListener(this.onChange); @@ -61,10 +58,6 @@ export default class UserSettingsAppearance extends React.Component { type = 'custom'; } - if (!theme.codeTheme) { - theme.codeTheme = Constants.DEFAULT_CODE_THEME; - } - return {theme, type}; } onChange() { @@ -100,13 +93,6 @@ export default class UserSettingsAppearance extends React.Component { ); } updateTheme(theme) { - theme.codeTheme = this.state.theme.codeTheme; - this.setState({theme}); - Utils.applyTheme(theme); - } - updateCodeTheme(codeTheme) { - var theme = this.state.theme; - theme.codeTheme = codeTheme; this.setState({theme}); Utils.applyTheme(theme); } @@ -116,7 +102,6 @@ export default class UserSettingsAppearance extends React.Component { handleClose() { const state = this.getStateFromStores(); state.serverError = null; - state.theme.codeTheme = this.originalCodeTheme; Utils.applyTheme(state.theme); @@ -185,13 +170,7 @@ export default class UserSettingsAppearance extends React.Component { </div> {custom} <hr /> - <strong className='radio'>{'Code Theme'}</strong> - <CodeThemeChooser - theme={this.state.theme} - updateTheme={this.updateCodeTheme} - /> - <hr /> - {serverError} + {serverError} <a className='btn btn-sm btn-primary' href='#' diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index 1c8ce3c79..3adac197a 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -570,6 +570,7 @@ export default class UserSettingsGeneralTab extends React.Component { /> ); } + return ( <div> <div className='modal-header'> diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx index 4b1e5e532..9bee74343 100644 --- a/web/react/components/user_settings/user_settings_integrations.jsx +++ b/web/react/components/user_settings/user_settings_integrations.jsx @@ -43,6 +43,7 @@ export default class UserSettingsIntegrationsTab extends React.Component { incomingHooksSection = ( <SettingItemMax title='Incoming Webhooks' + width='medium' inputs={inputs} updateSection={(e) => { this.updateSection(''); @@ -54,6 +55,7 @@ export default class UserSettingsIntegrationsTab extends React.Component { incomingHooksSection = ( <SettingItemMin title='Incoming Webhooks' + width='medium' describe='Manage your incoming webhooks (Developer feature)' updateSection={() => { this.updateSection('incoming-hooks'); @@ -72,6 +74,7 @@ export default class UserSettingsIntegrationsTab extends React.Component { outgoingHooksSection = ( <SettingItemMax title='Outgoing Webhooks' + width='medium' inputs={inputs} updateSection={(e) => { this.updateSection(''); @@ -83,6 +86,7 @@ export default class UserSettingsIntegrationsTab extends React.Component { outgoingHooksSection = ( <SettingItemMin title='Outgoing Webhooks' + width='medium' describe='Manage your outgoing webhooks' updateSection={() => { this.updateSection('outgoing-hooks'); diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 5449ae91e..18dd490e7 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -43,6 +43,7 @@ export default class UserSettingsModal extends React.Component { tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'}); } tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'}); + tabs.push({name: 'advanced', uiName: 'Advanced', icon: 'glyphicon glyphicon-list-alt'}); return ( <div diff --git a/web/react/package.json b/web/react/package.json index 9af6f5880..e6a662375 100644 --- a/web/react/package.json +++ b/web/react/package.json @@ -6,7 +6,6 @@ "autolinker": "0.18.1", "babel-runtime": "5.8.24", "flux": "2.1.1", - "highlight.js": "^8.9.1", "keymirror": "0.1.1", "marked": "0.3.5", "object-assign": "3.0.0", diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index bf117b3b3..aeb39d8a8 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -589,21 +589,38 @@ export function updateChannel(channel, success, error) { track('api', 'api_channels_update'); } -export function updateChannelDesc(data, success, error) { +export function updateChannelHeader(data, success, error) { $.ajax({ - url: '/api/v1/channels/update_desc', + url: '/api/v1/channels/update_header', dataType: 'json', contentType: 'application/json', type: 'POST', data: JSON.stringify(data), success, error: function onError(xhr, status, err) { - var e = handleError('updateChannelDesc', xhr, status, err); + var e = handleError('updateChannelHeader', xhr, status, err); error(e); } }); - track('api', 'api_channels_desc'); + track('api', 'api_channels_header'); +} + +export function updateChannelPurpose(data, success, error) { + $.ajax({ + url: '/api/v1/channels/update_purpose', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: function onError(xhr, status, err) { + var e = handleError('updateChannelPurpose', xhr, status, err); + error(e); + } + }); + + track('api', 'api_channels_purpose'); } export function updateNotifyProps(data, success, error) { diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 0e89b9470..69e3b007d 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -302,16 +302,10 @@ module.exports = { uiName: 'Mention Highlight Link' } ], - CODE_THEMES: { - github: 'GitHub', - solarized_light: 'Solarized light', - monokai: 'Monokai', - solarized_dark: 'Solarized Dark' - }, - DEFAULT_CODE_THEME: 'github', Preferences: { CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show', - CATEGORY_DISPLAY_SETTINGS: 'display_settings' + CATEGORY_DISPLAY_SETTINGS: 'display_settings', + CATEGORY_ADVANCED_SETTINGS: 'advanced_settings' }, KeyCodes: { UP: 38, @@ -322,30 +316,5 @@ module.exports = { ENTER: 13, ESCAPE: 27, SPACE: 32 - }, - HighlightedLanguages: { - diff: 'Diff', - apache: 'Apache', - makefile: 'Makefile', - http: 'HTTP', - json: 'JSON', - markdown: 'Markdown', - javascript: 'JavaScript', - css: 'CSS', - nginx: 'nginx', - objectivec: 'Objective-C', - python: 'Python', - xml: 'XML', - perl: 'Perl', - bash: 'Bash', - php: 'PHP', - coffeescript: 'CoffeeScript', - cs: 'C#', - cpp: 'C++', - sql: 'SQL', - go: 'Go', - ruby: 'Ruby', - java: 'Java', - ini: 'ini' } }; diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index ad11a95ac..26587dd6e 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -6,34 +6,6 @@ const Utils = require('./utils.jsx'); const marked = require('marked'); -const highlightJs = require('highlight.js/lib/highlight.js'); -const highlightJsDiff = require('highlight.js/lib/languages/diff.js'); -const highlightJsApache = require('highlight.js/lib/languages/apache.js'); -const highlightJsMakefile = require('highlight.js/lib/languages/makefile.js'); -const highlightJsHttp = require('highlight.js/lib/languages/http.js'); -const highlightJsJson = require('highlight.js/lib/languages/json.js'); -const highlightJsMarkdown = require('highlight.js/lib/languages/markdown.js'); -const highlightJsJavascript = require('highlight.js/lib/languages/javascript.js'); -const highlightJsCss = require('highlight.js/lib/languages/css.js'); -const highlightJsNginx = require('highlight.js/lib/languages/nginx.js'); -const highlightJsObjectivec = require('highlight.js/lib/languages/objectivec.js'); -const highlightJsPython = require('highlight.js/lib/languages/python.js'); -const highlightJsXml = require('highlight.js/lib/languages/xml.js'); -const highlightJsPerl = require('highlight.js/lib/languages/perl.js'); -const highlightJsBash = require('highlight.js/lib/languages/bash.js'); -const highlightJsPhp = require('highlight.js/lib/languages/php.js'); -const highlightJsCoffeescript = require('highlight.js/lib/languages/coffeescript.js'); -const highlightJsCs = require('highlight.js/lib/languages/cs.js'); -const highlightJsCpp = require('highlight.js/lib/languages/cpp.js'); -const highlightJsSql = require('highlight.js/lib/languages/sql.js'); -const highlightJsGo = require('highlight.js/lib/languages/go.js'); -const highlightJsRuby = require('highlight.js/lib/languages/ruby.js'); -const highlightJsJava = require('highlight.js/lib/languages/java.js'); -const highlightJsIni = require('highlight.js/lib/languages/ini.js'); - -const Constants = require('../utils/constants.jsx'); -const HighlightedLanguages = Constants.HighlightedLanguages; - export class MattermostMarkdownRenderer extends marked.Renderer { constructor(options, formattingOptions = {}) { super(options); @@ -43,43 +15,6 @@ export class MattermostMarkdownRenderer extends marked.Renderer { this.text = this.text.bind(this); this.formattingOptions = formattingOptions; - - highlightJs.registerLanguage('diff', highlightJsDiff); - highlightJs.registerLanguage('apache', highlightJsApache); - highlightJs.registerLanguage('makefile', highlightJsMakefile); - highlightJs.registerLanguage('http', highlightJsHttp); - highlightJs.registerLanguage('json', highlightJsJson); - highlightJs.registerLanguage('markdown', highlightJsMarkdown); - highlightJs.registerLanguage('javascript', highlightJsJavascript); - highlightJs.registerLanguage('css', highlightJsCss); - highlightJs.registerLanguage('nginx', highlightJsNginx); - highlightJs.registerLanguage('objectivec', highlightJsObjectivec); - highlightJs.registerLanguage('python', highlightJsPython); - highlightJs.registerLanguage('xml', highlightJsXml); - highlightJs.registerLanguage('perl', highlightJsPerl); - highlightJs.registerLanguage('bash', highlightJsBash); - highlightJs.registerLanguage('php', highlightJsPhp); - highlightJs.registerLanguage('coffeescript', highlightJsCoffeescript); - highlightJs.registerLanguage('cs', highlightJsCs); - highlightJs.registerLanguage('cpp', highlightJsCpp); - highlightJs.registerLanguage('sql', highlightJsSql); - highlightJs.registerLanguage('go', highlightJsGo); - highlightJs.registerLanguage('ruby', highlightJsRuby); - highlightJs.registerLanguage('java', highlightJsJava); - highlightJs.registerLanguage('ini', highlightJsIni); - } - - code(code, language) { - if (!language || highlightJs.listLanguages().indexOf(language) < 0) { - let parsed = super.code(code, language); - return '<code class="hljs">' + $(parsed).text() + '</code>'; - } - - let parsed = highlightJs.highlight(language, code); - return '<div class="post-body--code">' + - '<span class="post-body--code__language">' + HighlightedLanguages[language] + '</span>' + - '<code style="white-space: pre;" class="hljs">' + parsed.value + '</code>' + - '</div>'; } br() { diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index fadab27a7..35ce49ae2 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -211,11 +211,15 @@ export function displayDateTime(ticks) { } interval = Math.floor(seconds / 60); - if (interval > 1) { + if (interval >= 2) { return interval + ' minutes ago'; } - return '1 minute ago'; + if (interval >= 1) { + return '1 minute ago'; + } + + return 'just now'; } export function displayCommentDateTime(ticks) { @@ -227,46 +231,62 @@ export function getTimestamp() { return Date.now(); } -function testUrlMatch(text) { - var urlMatcher = new Autolinker.matchParser.MatchParser({ +// extracts links not styled by Markdown +export function extractLinks(text) { + const urlMatcher = new Autolinker.matchParser.MatchParser({ urls: true, emails: false, twitter: false, phone: false, hashtag: false }); - var result = []; + const links = []; + let replaceText = text; + + // pull out the Markdown code blocks + const codeBlocks = []; + const splitText = replaceText.split('`'); // also handles ``` + for (let i = 1; i < splitText.length; i += 2) { + if (splitText[i].trim() !== '') { + codeBlocks.push(splitText[i]); + } + } + function replaceFn(match) { - var linkData = {}; - var matchText = match.getMatchedText(); + let link = ''; + const matchText = match.getMatchedText(); + const tempText = replaceText; + + const start = replaceText.indexOf(matchText); + const end = start + matchText.length; + + replaceText = replaceText.substring(0, start) + replaceText.substring(end); + + // if it's a Markdown link, just skip it + if (start > 1) { + if (tempText.charAt(start - 2) === ']' && tempText.charAt(start - 1) === '(' && tempText.charAt(end) === ')') { + return; + } + } + + // if it's in a Markdown code block, skip it + for (const i in codeBlocks) { + if (codeBlocks[i].indexOf(matchText) === 0) { + codeBlocks[i] = codeBlocks[i].replace(matchText, ''); + return; + } + } - linkData.text = matchText; if (matchText.trim().indexOf('http') === 0) { - linkData.link = matchText; + link = matchText; } else { - linkData.link = 'http://' + matchText; + link = 'http://' + matchText; } - result.push(linkData); + links.push(link); } urlMatcher.replace(text, replaceFn, this); - return result; -} - -export function extractLinks(text) { - var repRegex = new RegExp('<br>', 'g'); - var matches = testUrlMatch(text.replace(repRegex, '\n')); - - if (!matches.length) { - return {links: null, text: text}; - } - - var links = []; - for (var i = 0; i < matches.length; i++) { - links.push(matches[i].link); - } - - return {links: links, text: text}; + return {links, text}; } export function escapeRegExp(string) { @@ -404,11 +424,6 @@ export function toTitleCase(str) { } export function applyTheme(theme) { - if (!theme.codeTheme) { - theme.codeTheme = Constants.DEFAULT_CODE_THEME; - } - updateCodeTheme(theme.codeTheme); - if (theme.sidebarBg) { changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1); } @@ -439,6 +454,8 @@ export function applyTheme(theme) { if (theme.sidebarTextActiveColor) { changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2); changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.1), 1); + changeCss('.search-help-popover .search-autocomplete__item:hover', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.05), 1); + changeCss('.search-help-popover .search-autocomplete__item.selected', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.15), 1); } if (theme.sidebarHeaderBg) { @@ -595,27 +612,6 @@ export function rgb2hex(rgbIn) { return '#' + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]); } -export function updateCodeTheme(theme) { - const path = '/static/css/highlight/' + theme + '.css'; - const $link = $('link.code_theme'); - if (path !== $link.attr('href')) { - changeCss('code.hljs', 'visibility: hidden'); - var xmlHTTP = new XMLHttpRequest(); - xmlHTTP.open('GET', path, true); - xmlHTTP.onload = function onLoad() { - $link.attr('href', path); - if (isBrowserFirefox()) { - $link.one('load', () => { - changeCss('code.hljs', 'visibility: visible'); - }); - } else { - changeCss('code.hljs', 'visibility: visible'); - } - }; - xmlHTTP.send(); - } -} - export function placeCaretAtEnd(el) { el.focus(); if (typeof window.getSelection != 'undefined' && typeof document.createRange != 'undefined') { diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss index d0241f795..9c65e2d1e 100644 --- a/web/sass-files/sass/partials/_admin-console.scss +++ b/web/sass-files/sass/partials/_admin-console.scss @@ -38,7 +38,6 @@ .recent-active-users { width: 365px; - height: 375px; border: 1px solid #ddd; padding: 5px 10px 10px 10px; margin: 10px 10px 10px 10px; diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index 6399b8fd8..635928fe3 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -37,6 +37,9 @@ body { img { max-width: 100%; height: auto; + &.rounded { + @include border-radius(100%); + } } .popover { @@ -122,6 +125,9 @@ 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/_modal.scss b/web/sass-files/sass/partials/_modal.scss index 1dcdbf348..9314b4980 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -113,11 +113,10 @@ text-align: center; padding: 2em 1em; .primary-message { - font-size: 1.2em; + font-size: 1.25em; } .secondary-message { - font-size: 1.25em; - color: #888; + @include opacity(0.8); margin: 1em 0 0; } } @@ -377,7 +376,7 @@ @include opacity(0.8); } - .more-description { + .more-purpose { @include opacity(0.7); } diff --git a/web/sass-files/sass/partials/_popover.scss b/web/sass-files/sass/partials/_popover.scss index 4a2ad2748..4f5f1d215 100644 --- a/web/sass-files/sass/partials/_popover.scss +++ b/web/sass-files/sass/partials/_popover.scss @@ -28,6 +28,36 @@ @include single-transition(opacity, 0.3s, ease-in); font-size: em(13px); + &.autocomplete { + display: block; + .popover-content { + padding: 10px; + position: relative; + } + } + + .search-autocomplete__item { + cursor: pointer; + padding: 6px 8px; + margin: 3px 0; + @include border-radius(2px); + + &:hover { + background: rgba(black, 0.1); + } + + &.selected { + background: rgba(black, 0.2); + } + + .profile-img { + margin-top: -1px; + height: 16px; + margin-right: 6px; + width: 16px; + } + } + &.bottom > .arrow { top: -18px; border-width: 9px; diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index a53e2dd62..414ab0554 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -467,22 +467,6 @@ body.ios { white-space: nowrap; cursor: pointer; } - .post-body--code { - font-size: .97em; - position:relative; - .post-body--code__language { - position: absolute; - right: 0; - background: #fff; - cursor: default; - padding: 0.3em 0.5em 0.1em; - border-bottom-left-radius: 4px; - @include opacity(.3); - } - code { - white-space: pre; - } - } } .create-reply-form-wrap { width: 100%; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 2cd5560ef..b85fa638a 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -379,7 +379,9 @@ } .btn { &.btn-primary { - margin: 8px 0 0 -10px; + display: block; + margin: 10px 0 6px; + width: 100%; float: none; } } @@ -401,7 +403,13 @@ &.minimize-settings { display: block; .section-edit { - text-align: left; + position: absolute; + top: 7px; + right: 0; + width: 50px; + .fa { + display: inline-block; + } } } .no-padding--left { diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss index ce3563885..e50dc398a 100644 --- a/web/sass-files/sass/partials/_search.scss +++ b/web/sass-files/sass/partials/_search.scss @@ -108,44 +108,4 @@ .search-highlight { background-color: #FFF2BB; -} - -.search-autocomplete { - background-color: #fff; - border: $border-gray; - line-height: 36px; - overflow-x: hidden; - overflow-y: scroll; - position: absolute; - text-align: left; - width: 100%; - z-index: 100; - @extend %popover-box-shadow; -} - -.search-autocomplete__channel { - cursor: pointer; - height: 36px; - padding: 0px 6px; - - &.selected { - background-color:rgba(51, 51, 51, 0.15); - } -} - -.search-autocomplete__user { - cursor: pointer; - height: 36px; - padding: 0px; - - .profile-img { - height: 32px; - margin-right: 6px; - width: 32px; - @include border-radius(16px); - } - - &.selected { - background-color:rgba(51, 51, 51, 0.15); - } -} +}
\ No newline at end of file diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index c881f9073..fbbd07485 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -33,6 +33,33 @@ label { font-weight: 600; } + .no-padding--left { + padding-left: 0; + } + .padding-top { + padding-top: 7px; + &.x2 { + padding-top: 14px; + } + &.x3 { + padding-top: 21px; + } + } + .padding-bottom { + padding-bottom: 7px; + &.x2 { + padding-bottom: 14px; + } + &.x3 { + padding-bottom: 21px; + } + .control-label { + font-weight: 600; + &.text-left { + text-align: left; + } + } + } .settings-table { display: table; table-layout: fixed; @@ -44,7 +71,7 @@ .nav { position: fixed; top: 57px; - width: 180px; + width: 179px; } .security-links { margin-right: 20px; @@ -66,6 +93,7 @@ padding: 1em 0; margin-bottom: 0; cursor: pointer; + position: relative; @include clearfix; &:hover { background: #f9f9f9; @@ -170,33 +198,6 @@ .has-error { color: #a94442; } - .no-padding--left { - padding-left: 0; - } - .padding-top { - padding-top: 7px; - &.x2 { - padding-top: 14px; - } - &.x3 { - padding-top: 21px; - } - } - .padding-bottom { - padding-bottom: 7px; - &.x2 { - padding-bottom: 14px; - } - &.x3 { - padding-bottom: 21px; - } - .control-label { - font-weight: 600; - &.text-left { - text-align: left; - } - } - } .file-status { font-size: 13px; diff --git a/web/static/css/highlight b/web/static/css/highlight deleted file mode 120000 index c774cf397..000000000 --- a/web/static/css/highlight +++ /dev/null @@ -1 +0,0 @@ -../../react/node_modules/highlight.js/styles/
\ No newline at end of file diff --git a/web/static/images/themes/code_themes/github.png b/web/static/images/themes/code_themes/github.png Binary files differdeleted file mode 100644 index d0538d6c0..000000000 --- a/web/static/images/themes/code_themes/github.png +++ /dev/null diff --git a/web/static/images/themes/code_themes/monokai.png b/web/static/images/themes/code_themes/monokai.png Binary files differdeleted file mode 100644 index 8f92d2a18..000000000 --- a/web/static/images/themes/code_themes/monokai.png +++ /dev/null diff --git a/web/static/images/themes/code_themes/solarized_dark.png b/web/static/images/themes/code_themes/solarized_dark.png Binary files differdeleted file mode 100644 index 76055c678..000000000 --- a/web/static/images/themes/code_themes/solarized_dark.png +++ /dev/null diff --git a/web/static/images/themes/code_themes/solarized_light.png b/web/static/images/themes/code_themes/solarized_light.png Binary files differdeleted file mode 100644 index b9595c22d..000000000 --- a/web/static/images/themes/code_themes/solarized_light.png +++ /dev/null diff --git a/web/templates/head.html b/web/templates/head.html index fdc371af4..041831ed7 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -24,7 +24,6 @@ <link rel="stylesheet" href="/static/css/bootstrap-colorpicker.min.css"> <link rel="stylesheet" href="/static/css/styles.css"> <link rel="stylesheet" href="/static/css/google-fonts.css"> - <link rel="stylesheet" class="code_theme" href=""> <link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon"> <link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon"> |