diff --git a/README.md b/README.md index d6b721dd16..58f4887f61 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Please see the [features pages of the Mattermost website](http://www.mattermost. - [Mattermost Forum](http://forum.mattermost.org/) - For technical questions and answers - [Issue Tracker](http://www.mattermost.org/filing-issues/) - For reporting bugs - [Feature Ideas Forum](http://www.mattermost.org/feature-requests/) - For sharing ideas for future versions -- [Contributuion Guidelines](http://www.mattermost.org/contribute-to-mattermost/) - For contributing code or feedback to the project +- [Contribution Guidelines](http://www.mattermost.org/contribute-to-mattermost/) - For contributing code or feedback to the project Follow us on Twitter at [@MattermostHQ](https://twitter.com/mattermosthq). diff --git a/api/export.go b/api/export.go index c6bc626f99..aff34073f0 100644 --- a/api/export.go +++ b/api/export.go @@ -278,11 +278,11 @@ func copyDirToExportWriter(writer ExportWriter, inPath string, outPath string) * } func ExportLocalStorage(writer ExportWriter, options *ExportOptions, teamId string) *model.AppError { - teamDir := utils.Cfg.ImageSettings.Directory + "teams/" + teamId + teamDir := utils.Cfg.FileSettings.Directory + "teams/" + teamId - if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { return model.NewAppError("ExportLocalStorage", "S3 is not supported for local storage export.", "") - } else if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { if err := copyDirToExportWriter(writer, teamDir, EXPORT_LOCAL_STORAGE_FOLDER); err != nil { return err } diff --git a/api/file.go b/api/file.go index acb18a7d4b..694fc734c1 100644 --- a/api/file.go +++ b/api/file.go @@ -69,7 +69,7 @@ func InitFile(r *mux.Router) { } func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { - if len(utils.Cfg.ImageSettings.DriverName) == 0 { + if len(utils.Cfg.FileSettings.DriverName) == 0 { c.Err = model.NewAppError("uploadFile", "Unable to upload file. Image storage is not configured.", "") c.Err.StatusCode = http.StatusNotImplemented return @@ -217,8 +217,8 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch // Create thumbnail go func() { - thumbWidth := float64(utils.Cfg.ImageSettings.ThumbnailWidth) - thumbHeight := float64(utils.Cfg.ImageSettings.ThumbnailHeight) + thumbWidth := float64(utils.Cfg.FileSettings.ThumbnailWidth) + thumbHeight := float64(utils.Cfg.FileSettings.ThumbnailHeight) imgWidth := float64(width) imgHeight := float64(height) @@ -226,9 +226,9 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch if imgHeight < thumbHeight && imgWidth < thumbWidth { thumbnail = img } else if imgHeight/imgWidth < thumbHeight/thumbWidth { - thumbnail = resize.Resize(0, utils.Cfg.ImageSettings.ThumbnailHeight, img, resize.Lanczos3) + thumbnail = resize.Resize(0, utils.Cfg.FileSettings.ThumbnailHeight, img, resize.Lanczos3) } else { - thumbnail = resize.Resize(utils.Cfg.ImageSettings.ThumbnailWidth, 0, img, resize.Lanczos3) + thumbnail = resize.Resize(utils.Cfg.FileSettings.ThumbnailWidth, 0, img, resize.Lanczos3) } buf := new(bytes.Buffer) @@ -247,8 +247,8 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch // Create preview go func() { var preview image.Image - if width > int(utils.Cfg.ImageSettings.PreviewWidth) { - preview = resize.Resize(utils.Cfg.ImageSettings.PreviewWidth, utils.Cfg.ImageSettings.PreviewHeight, img, resize.Lanczos3) + if width > int(utils.Cfg.FileSettings.PreviewWidth) { + preview = resize.Resize(utils.Cfg.FileSettings.PreviewWidth, utils.Cfg.FileSettings.PreviewHeight, img, resize.Lanczos3) } else { preview = img } @@ -294,7 +294,7 @@ type ImageGetResult struct { } func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) { - if len(utils.Cfg.ImageSettings.DriverName) == 0 { + if len(utils.Cfg.FileSettings.DriverName) == 0 { c.Err = model.NewAppError("uploadFile", "Unable to get file info. Image storage is not configured.", "") c.Err.StatusCode = http.StatusNotImplemented return @@ -357,7 +357,7 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) { } func getFile(c *Context, w http.ResponseWriter, r *http.Request) { - if len(utils.Cfg.ImageSettings.DriverName) == 0 { + if len(utils.Cfg.FileSettings.DriverName) == 0 { c.Err = model.NewAppError("uploadFile", "Unable to get file. Image storage is not configured.", "") c.Err.StatusCode = http.StatusNotImplemented return @@ -400,7 +400,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) { asyncGetFile(path, fileData) if len(hash) > 0 && len(data) > 0 && len(teamId) == 26 { - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ImageSettings.PublicLinkSalt)) { + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt)) { c.Err = model.NewAppError("getFile", "The public link does not appear to be valid", "") return } @@ -442,13 +442,13 @@ func asyncGetFile(path string, fileData chan []byte) { } func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) { - if len(utils.Cfg.ImageSettings.DriverName) == 0 { + if len(utils.Cfg.FileSettings.DriverName) == 0 { c.Err = model.NewAppError("uploadFile", "Unable to get link. Image storage is not configured.", "") c.Err.StatusCode = http.StatusNotImplemented return } - if !utils.Cfg.ImageSettings.EnablePublicLink { + if !utils.Cfg.FileSettings.EnablePublicLink { c.Err = model.NewAppError("getPublicLink", "Public links have been disabled", "") c.Err.StatusCode = http.StatusForbidden } @@ -478,7 +478,7 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) { newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) data := model.MapToJson(newProps) - hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ImageSettings.PublicLinkSalt)) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt)) url := fmt.Sprintf("%s/api/v1/files/get/%s/%s/%s?d=%s&h=%s&t=%s", c.GetSiteURL(), channelId, userId, filename, url.QueryEscape(data), url.QueryEscape(hash), c.Session.TeamId) @@ -511,13 +511,13 @@ func getExport(c *Context, w http.ResponseWriter, r *http.Request) { func writeFile(f []byte, path string) *model.AppError { - if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { var auth aws.Auth - auth.AccessKey = utils.Cfg.ImageSettings.AmazonS3AccessKeyId - auth.SecretKey = utils.Cfg.ImageSettings.AmazonS3SecretAccessKey + auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey - s := s3.New(auth, aws.Regions[utils.Cfg.ImageSettings.AmazonS3Region]) - bucket := s.Bucket(utils.Cfg.ImageSettings.AmazonS3Bucket) + s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket) ext := filepath.Ext(path) @@ -534,12 +534,12 @@ func writeFile(f []byte, path string) *model.AppError { if err != nil { return model.NewAppError("writeFile", "Encountered an error writing to S3", err.Error()) } - } else if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_LOCAL { - if err := os.MkdirAll(filepath.Dir(utils.Cfg.ImageSettings.Directory+path), 0774); err != nil { + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil { return model.NewAppError("writeFile", "Encountered an error creating the directory for the new file", err.Error()) } - if err := ioutil.WriteFile(utils.Cfg.ImageSettings.Directory+path, f, 0644); err != nil { + if err := ioutil.WriteFile(utils.Cfg.FileSettings.Directory+path, f, 0644); err != nil { return model.NewAppError("writeFile", "Encountered an error writing to local server storage", err.Error()) } } else { @@ -551,13 +551,13 @@ func writeFile(f []byte, path string) *model.AppError { func readFile(path string) ([]byte, *model.AppError) { - if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { var auth aws.Auth - auth.AccessKey = utils.Cfg.ImageSettings.AmazonS3AccessKeyId - auth.SecretKey = utils.Cfg.ImageSettings.AmazonS3SecretAccessKey + auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey - s := s3.New(auth, aws.Regions[utils.Cfg.ImageSettings.AmazonS3Region]) - bucket := s.Bucket(utils.Cfg.ImageSettings.AmazonS3Bucket) + s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket) // try to get the file from S3 with some basic retry logic tries := 0 @@ -573,8 +573,8 @@ func readFile(path string) ([]byte, *model.AppError) { } time.Sleep(3000 * time.Millisecond) } - } else if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_LOCAL { - if f, err := ioutil.ReadFile(utils.Cfg.ImageSettings.Directory + path); err != nil { + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if f, err := ioutil.ReadFile(utils.Cfg.FileSettings.Directory + path); err != nil { return nil, model.NewAppError("readFile", "Encountered an error reading from local server storage", err.Error()) } else { return f, nil @@ -585,14 +585,14 @@ func readFile(path string) ([]byte, *model.AppError) { } func openFileWriteStream(path string) (io.Writer, *model.AppError) { - if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { return nil, model.NewAppError("openFileWriteStream", "S3 is not supported.", "") - } else if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_LOCAL { - if err := os.MkdirAll(filepath.Dir(utils.Cfg.ImageSettings.Directory+path), 0774); err != nil { + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil { return nil, model.NewAppError("openFileWriteStream", "Encountered an error creating the directory for the new file", err.Error()) } - if fileHandle, err := os.Create(utils.Cfg.ImageSettings.Directory + path); err != nil { + if fileHandle, err := os.Create(utils.Cfg.FileSettings.Directory + path); err != nil { return nil, model.NewAppError("openFileWriteStream", "Encountered an error writing to local server storage", err.Error()) } else { fileHandle.Chmod(0644) diff --git a/api/file_benchmark_test.go b/api/file_benchmark_test.go index f7d5de1d90..47f8bff43d 100644 --- a/api/file_benchmark_test.go +++ b/api/file_benchmark_test.go @@ -38,7 +38,7 @@ func BenchmarkGetFile(b *testing.B) { newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) data := model.MapToJson(newProps) - hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ImageSettings.PublicLinkSalt)) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt)) // wait a bit for files to ready time.Sleep(5 * time.Second) diff --git a/api/file_test.go b/api/file_test.go index 072a3fab15..657c081310 100644 --- a/api/file_test.go +++ b/api/file_test.go @@ -68,7 +68,7 @@ func TestUploadFile(t *testing.T) { } resp, appErr := Client.UploadFile("/files/upload", body.Bytes(), writer.FormDataContentType()) - if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { if appErr != nil { t.Fatal(appErr) } @@ -81,11 +81,11 @@ func TestUploadFile(t *testing.T) { fileId := strings.Split(filename, ".")[0] var auth aws.Auth - auth.AccessKey = utils.Cfg.ImageSettings.AmazonS3AccessKeyId - auth.SecretKey = utils.Cfg.ImageSettings.AmazonS3SecretAccessKey + auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey - s := s3.New(auth, aws.Regions[utils.Cfg.ImageSettings.AmazonS3Region]) - bucket := s.Bucket(utils.Cfg.ImageSettings.AmazonS3Bucket) + s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket) // wait a bit for files to ready time.Sleep(5 * time.Second) @@ -104,7 +104,7 @@ func TestUploadFile(t *testing.T) { if err != nil { t.Fatal(err) } - } else if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/") filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] if strings.Contains(filename, "../") { @@ -115,17 +115,17 @@ func TestUploadFile(t *testing.T) { // wait a bit for files to ready time.Sleep(5 * time.Second) - path := utils.Cfg.ImageSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename + path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } - path = utils.Cfg.ImageSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg" + path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } - path = utils.Cfg.ImageSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg" + path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } @@ -151,7 +151,7 @@ func TestGetFile(t *testing.T) { channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - if utils.Cfg.ImageSettings.DriverName != "" { + if utils.Cfg.FileSettings.DriverName != "" { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -222,7 +222,7 @@ func TestGetFile(t *testing.T) { newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) data := model.MapToJson(newProps) - hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ImageSettings.PublicLinkSalt)) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt)) Client.LoginByEmail(team2.Name, user2.Email, "pwd") @@ -262,13 +262,13 @@ func TestGetFile(t *testing.T) { t.Fatal("Should have errored - user not logged in and link not public") } - if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { var auth aws.Auth - auth.AccessKey = utils.Cfg.ImageSettings.AmazonS3AccessKeyId - auth.SecretKey = utils.Cfg.ImageSettings.AmazonS3SecretAccessKey + auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey - s := s3.New(auth, aws.Regions[utils.Cfg.ImageSettings.AmazonS3Region]) - bucket := s.Bucket(utils.Cfg.ImageSettings.AmazonS3Bucket) + s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket) filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/") filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] @@ -293,17 +293,17 @@ func TestGetFile(t *testing.T) { filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] fileId := strings.Split(filename, ".")[0] - path := utils.Cfg.ImageSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename + path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } - path = utils.Cfg.ImageSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg" + path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } - path = utils.Cfg.ImageSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg" + path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } @@ -334,7 +334,7 @@ func TestGetPublicLink(t *testing.T) { channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - if utils.Cfg.ImageSettings.DriverName != "" { + if utils.Cfg.FileSettings.DriverName != "" { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -410,14 +410,14 @@ func TestGetPublicLink(t *testing.T) { t.Fatal("should have errored, user not member of channel") } - if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { // perform clean-up on s3 var auth aws.Auth - auth.AccessKey = utils.Cfg.ImageSettings.AmazonS3AccessKeyId - auth.SecretKey = utils.Cfg.ImageSettings.AmazonS3SecretAccessKey + auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey - s := s3.New(auth, aws.Regions[utils.Cfg.ImageSettings.AmazonS3Region]) - bucket := s.Bucket(utils.Cfg.ImageSettings.AmazonS3Bucket) + s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket) filenames := strings.Split(resp.Data.(*model.FileUploadResponse).Filenames[0], "/") filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] @@ -442,17 +442,17 @@ func TestGetPublicLink(t *testing.T) { filename := filenames[len(filenames)-2] + "/" + filenames[len(filenames)-1] fileId := strings.Split(filename, ".")[0] - path := utils.Cfg.ImageSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename + path := utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filename if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } - path = utils.Cfg.ImageSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg" + path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } - path = utils.Cfg.ImageSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg" + path = utils.Cfg.FileSettings.Directory + "teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.jpg" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } diff --git a/api/post.go b/api/post.go index 4294ae03ca..0379f6af5b 100644 --- a/api/post.go +++ b/api/post.go @@ -160,7 +160,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) { channel = result.Data.(*model.Channel) if channel.Type == model.CHANNEL_DIRECT { bodyText = "You have one new message." - subjectText = "New Private Message" + subjectText = "New Direct Message" } else { bodyText = "You have one new mention." subjectText = "New Mention" diff --git a/api/user.go b/api/user.go index 348475e386..5be703bfbe 100644 --- a/api/user.go +++ b/api/user.go @@ -172,9 +172,6 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { } user.MakeNonNil() - if len(user.Props["theme"]) == 0 { - user.AddProp("theme", utils.Cfg.TeamSettings.DefaultThemeColor) - } if result := <-Srv.Store.User().Save(user); result.Err != nil { c.Err = result.Err @@ -663,7 +660,7 @@ func createProfileImage(username string, userId string) ([]byte, *model.AppError initial := string(strings.ToUpper(username)[0]) - fontBytes, err := ioutil.ReadFile(utils.FindDir("web/static/fonts") + utils.Cfg.ImageSettings.InitialFont) + fontBytes, err := ioutil.ReadFile(utils.FindDir("web/static/fonts") + utils.Cfg.FileSettings.InitialFont) if err != nil { return nil, model.NewAppError("createProfileImage", "Could not create default profile image font", err.Error()) } @@ -672,8 +669,8 @@ func createProfileImage(username string, userId string) ([]byte, *model.AppError return nil, model.NewAppError("createProfileImage", "Could not create default profile image font", err.Error()) } - width := int(utils.Cfg.ImageSettings.ProfileWidth) - height := int(utils.Cfg.ImageSettings.ProfileHeight) + width := int(utils.Cfg.FileSettings.ProfileWidth) + height := int(utils.Cfg.FileSettings.ProfileHeight) color := colors[int64(seed)%int64(len(colors))] dstImg := image.NewRGBA(image.Rect(0, 0, width, height)) srcImg := image.White @@ -712,7 +709,7 @@ func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { } else { var img []byte - if len(utils.Cfg.ImageSettings.DriverName) == 0 { + if len(utils.Cfg.FileSettings.DriverName) == 0 { var err *model.AppError if img, err = createProfileImage(result.Data.(*model.User).Username, id); err != nil { c.Err = err @@ -749,7 +746,7 @@ func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { } func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { - if len(utils.Cfg.ImageSettings.DriverName) == 0 { + if len(utils.Cfg.FileSettings.DriverName) == 0 { c.Err = model.NewAppError("uploadProfileImage", "Unable to upload file. Image storage is not configured.", "") c.Err.StatusCode = http.StatusNotImplemented return @@ -792,7 +789,7 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { } // Scale profile image - img = resize.Resize(utils.Cfg.ImageSettings.ProfileWidth, utils.Cfg.ImageSettings.ProfileHeight, img, resize.Lanczos3) + img = resize.Resize(utils.Cfg.FileSettings.ProfileWidth, utils.Cfg.FileSettings.ProfileHeight, img, resize.Lanczos3) buf := new(bytes.Buffer) err = png.Encode(buf, img) diff --git a/api/user_test.go b/api/user_test.go index c2dca752ca..baa567deca 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -373,19 +373,19 @@ func TestUserCreateImage(t *testing.T) { Client.DoApiGet("/users/"+user.Id+"/image", "", "") - if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { var auth aws.Auth - auth.AccessKey = utils.Cfg.ImageSettings.AmazonS3AccessKeyId - auth.SecretKey = utils.Cfg.ImageSettings.AmazonS3SecretAccessKey + auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey - s := s3.New(auth, aws.Regions[utils.Cfg.ImageSettings.AmazonS3Region]) - bucket := s.Bucket(utils.Cfg.ImageSettings.AmazonS3Bucket) + s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket) if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil { t.Fatal(err) } } else { - path := utils.Cfg.ImageSettings.Directory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png" + path := utils.Cfg.FileSettings.Directory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } @@ -403,7 +403,7 @@ func TestUserUploadProfileImage(t *testing.T) { user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - if utils.Cfg.ImageSettings.DriverName != "" { + if utils.Cfg.FileSettings.DriverName != "" { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -471,19 +471,19 @@ func TestUserUploadProfileImage(t *testing.T) { Client.DoApiGet("/users/"+user.Id+"/image", "", "") - if utils.Cfg.ImageSettings.DriverName == model.IMAGE_DRIVER_S3 { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { var auth aws.Auth - auth.AccessKey = utils.Cfg.ImageSettings.AmazonS3AccessKeyId - auth.SecretKey = utils.Cfg.ImageSettings.AmazonS3SecretAccessKey + auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey - s := s3.New(auth, aws.Regions[utils.Cfg.ImageSettings.AmazonS3Region]) - bucket := s.Bucket(utils.Cfg.ImageSettings.AmazonS3Bucket) + s := s3.New(auth, aws.Regions[utils.Cfg.FileSettings.AmazonS3Region]) + bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket) if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil { t.Fatal(err) } } else { - path := utils.Cfg.ImageSettings.Directory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png" + path := utils.Cfg.FileSettings.Directory + "teams/" + user.TeamId + "/users/" + user.Id + "/profile.png" if err := os.Remove(path); err != nil { t.Fatal("Couldn't remove file at " + path) } diff --git a/config/config.json b/config/config.json index c0f33a346f..aa92ccf4eb 100644 --- a/config/config.json +++ b/config/config.json @@ -33,11 +33,11 @@ "FileFormat": "", "FileLocation": "" }, - "ImageSettings": { + "FileSettings": { "DriverName": "local", "Directory": "./data/", "EnablePublicLink": true, - "PublicLinkSalt": "LhaAWC6lYEKHTkBKsvyXNIOfUIT37AXe", + "PublicLinkSalt": "A705AklYF8MFDOfcwh3I488G8vtLlVip", "ThumbnailWidth": 120, "ThumbnailHeight": 100, "PreviewWidth": 1024, diff --git a/doc/developer/code-contribution.md b/doc/developer/code-contribution.md index 708b324a02..51521be1b2 100644 --- a/doc/developer/code-contribution.md +++ b/doc/developer/code-contribution.md @@ -33,25 +33,6 @@ git checkout -b 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/). - For pull requests made by contributors not yet added to the approved contributor list, a reviewer may respond: - - ``` -Thanks @[GITHUB_USERNAME] for the pull request! - -Before we can review, we need to add you to the list of approved contributors for the Mattermost project. - -**Please help complete the Mattermost [contribution license agreement](http://www.mattermost.org/mattermost-contributor-agreement/)?** - -This is a standard procedure for many open source projects. You can view a list of past contributors who have completed the form [here](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true). - -After completing the form, it should be processed within 24 hours and reviewers for your pull request will be able to proceed. - -Please let us know if you have any questions. - -We are very happy to have you join our growing community! -``` - - 2. When you submit your pull request please 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. diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json index e33396214d..16a4007fa0 100644 --- a/docker/dev/config_docker.json +++ b/docker/dev/config_docker.json @@ -33,7 +33,7 @@ "FileFormat": "", "FileLocation": "" }, - "ImageSettings": { + "FileSettings": { "DriverName": "local", "Directory": "/mattermost/data/", "EnablePublicLink": true, diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json index e33396214d..16a4007fa0 100644 --- a/docker/local/config_docker.json +++ b/docker/local/config_docker.json @@ -33,7 +33,7 @@ "FileFormat": "", "FileLocation": "" }, - "ImageSettings": { + "FileSettings": { "DriverName": "local", "Directory": "/mattermost/data/", "EnablePublicLink": true, diff --git a/model/config.go b/model/config.go index 1e95277a2a..69f2127b27 100644 --- a/model/config.go +++ b/model/config.go @@ -58,7 +58,7 @@ type LogSettings struct { FileLocation string } -type ImageSettings struct { +type FileSettings struct { DriverName string Directory string EnablePublicLink bool @@ -123,7 +123,7 @@ type Config struct { TeamSettings TeamSettings SqlSettings SqlSettings LogSettings LogSettings - ImageSettings ImageSettings + FileSettings FileSettings EmailSettings EmailSettings RateLimitSettings RateLimitSettings PrivacySettings PrivacySettings diff --git a/model/user.go b/model/user.go index fdc519b990..3a2c9d56cf 100644 --- a/model/user.go +++ b/model/user.go @@ -47,6 +47,7 @@ type User struct { AllowMarketing bool `json:"allow_marketing"` Props StringMap `json:"props"` NotifyProps StringMap `json:"notify_props"` + ThemeProps StringMap `json:"theme_props"` LastPasswordUpdate int64 `json:"last_password_update"` LastPictureUpdate int64 `json:"last_picture_update"` FailedAttempts int `json:"failed_attempts"` @@ -108,6 +109,10 @@ func (u *User) IsValid() *AppError { return NewAppError("User.IsValid", "Invalid user, password and auth data cannot both be set", "user_id="+u.Id) } + if len(u.ThemeProps) > 2000 { + return NewAppError("User.IsValid", "Invalid theme", "user_id="+u.Id) + } + return nil } diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 778df367e1..3fd1c82b51 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -32,6 +32,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore { table.ColMap("Roles").SetMaxSize(64) table.ColMap("Props").SetMaxSize(4000) table.ColMap("NotifyProps").SetMaxSize(2000) + table.ColMap("ThemeProps").SetMaxSize(2000) table.SetUniqueTogether("Email", "TeamId") table.SetUniqueTogether("Username", "TeamId") } @@ -40,6 +41,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore { } func (us SqlUserStore) UpgradeSchemaIfNeeded() { + us.CreateColumnIfNotExists("Users", "ThemeProps", "varchar(2000)", "character varying(2000)", "{}") } func (us SqlUserStore) CreateIndexesIfNotExists() { diff --git a/utils/config.go b/utils/config.go index c42d5c9df6..5d786699bb 100644 --- a/utils/config.go +++ b/utils/config.go @@ -188,9 +188,9 @@ func getClientProperties(c *model.Config) map[string]string { props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress) - props["EnablePublicLink"] = strconv.FormatBool(c.ImageSettings.EnablePublicLink) - props["ProfileHeight"] = fmt.Sprintf("%v", c.ImageSettings.ProfileHeight) - props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth) + props["EnablePublicLink"] = strconv.FormatBool(c.FileSettings.EnablePublicLink) + props["ProfileHeight"] = fmt.Sprintf("%v", c.FileSettings.ProfileHeight) + props["ProfileWidth"] = fmt.Sprintf("%v", c.FileSettings.ProfileWidth) return props } diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 25476251f2..92f0bbdceb 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -10,7 +10,7 @@ var LoadingScreen = require('../loading_screen.jsx'); var EmailSettingsTab = require('./email_settings.jsx'); var LogSettingsTab = require('./log_settings.jsx'); var LogsTab = require('./logs.jsx'); -var ImageSettingsTab = require('./image_settings.jsx'); +var FileSettingsTab = require('./image_settings.jsx'); var PrivacySettingsTab = require('./privacy_settings.jsx'); var RateSettingsTab = require('./rate_settings.jsx'); var GitLabSettingsTab = require('./gitlab_settings.jsx'); @@ -128,7 +128,7 @@ export default class AdminController extends React.Component { } else if (this.state.selected === 'logs') { tab = ; } else if (this.state.selected === 'image_settings') { - tab = ; + tab = ; } else if (this.state.selected === 'privacy_settings') { tab = ; } else if (this.state.selected === 'rate_settings') { diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index b8413d6c70..375a6a8e99 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -180,7 +180,7 @@ export default class AdminSidebar extends React.Component { className={this.isSelected('image_settings')} onClick={this.handleClick.bind(this, 'image_settings', null)} > - {'Image Settings'} + {'File Settings'}
  • diff --git a/web/react/components/admin_console/image_settings.jsx b/web/react/components/admin_console/image_settings.jsx index f84f1c7358..25d5ad857d 100644 --- a/web/react/components/admin_console/image_settings.jsx +++ b/web/react/components/admin_console/image_settings.jsx @@ -5,7 +5,7 @@ var Client = require('../../utils/client.jsx'); var AsyncClient = require('../../utils/async_client.jsx'); var crypto = require('crypto'); -export default class ImageSettings extends React.Component { +export default class FileSettings extends React.Component { constructor(props) { super(props); @@ -16,7 +16,7 @@ export default class ImageSettings extends React.Component { this.state = { saveNeeded: false, serverError: null, - DriverName: this.props.config.ImageSettings.DriverName + DriverName: this.props.config.FileSettings.DriverName }; } @@ -42,61 +42,61 @@ export default class ImageSettings extends React.Component { $('#save-button').button('loading'); var config = this.props.config; - config.ImageSettings.DriverName = React.findDOMNode(this.refs.DriverName).value; - config.ImageSettings.Directory = React.findDOMNode(this.refs.Directory).value; - config.ImageSettings.AmazonS3AccessKeyId = React.findDOMNode(this.refs.AmazonS3AccessKeyId).value; - config.ImageSettings.AmazonS3SecretAccessKey = React.findDOMNode(this.refs.AmazonS3SecretAccessKey).value; - config.ImageSettings.AmazonS3Bucket = React.findDOMNode(this.refs.AmazonS3Bucket).value; - config.ImageSettings.AmazonS3Region = React.findDOMNode(this.refs.AmazonS3Region).value; - config.ImageSettings.EnablePublicLink = React.findDOMNode(this.refs.EnablePublicLink).checked; + config.FileSettings.DriverName = React.findDOMNode(this.refs.DriverName).value; + config.FileSettings.Directory = React.findDOMNode(this.refs.Directory).value; + config.FileSettings.AmazonS3AccessKeyId = React.findDOMNode(this.refs.AmazonS3AccessKeyId).value; + config.FileSettings.AmazonS3SecretAccessKey = React.findDOMNode(this.refs.AmazonS3SecretAccessKey).value; + config.FileSettings.AmazonS3Bucket = React.findDOMNode(this.refs.AmazonS3Bucket).value; + config.FileSettings.AmazonS3Region = React.findDOMNode(this.refs.AmazonS3Region).value; + config.FileSettings.EnablePublicLink = React.findDOMNode(this.refs.EnablePublicLink).checked; - config.ImageSettings.PublicLinkSalt = React.findDOMNode(this.refs.PublicLinkSalt).value.trim(); + config.FileSettings.PublicLinkSalt = React.findDOMNode(this.refs.PublicLinkSalt).value.trim(); - if (config.ImageSettings.PublicLinkSalt === '') { - config.ImageSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32); - React.findDOMNode(this.refs.PublicLinkSalt).value = config.ImageSettings.PublicLinkSalt; + if (config.FileSettings.PublicLinkSalt === '') { + config.FileSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32); + React.findDOMNode(this.refs.PublicLinkSalt).value = config.FileSettings.PublicLinkSalt; } var thumbnailWidth = 120; if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10))) { thumbnailWidth = parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10); } - config.ImageSettings.ThumbnailWidth = thumbnailWidth; + config.FileSettings.ThumbnailWidth = thumbnailWidth; React.findDOMNode(this.refs.ThumbnailWidth).value = thumbnailWidth; var thumbnailHeight = 100; if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10))) { thumbnailHeight = parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10); } - config.ImageSettings.ThumbnailHeight = thumbnailHeight; + config.FileSettings.ThumbnailHeight = thumbnailHeight; React.findDOMNode(this.refs.ThumbnailHeight).value = thumbnailHeight; var previewWidth = 1024; if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10))) { previewWidth = parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10); } - config.ImageSettings.PreviewWidth = previewWidth; + config.FileSettings.PreviewWidth = previewWidth; React.findDOMNode(this.refs.PreviewWidth).value = previewWidth; var previewHeight = 0; if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10))) { previewHeight = parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10); } - config.ImageSettings.PreviewHeight = previewHeight; + config.FileSettings.PreviewHeight = previewHeight; React.findDOMNode(this.refs.PreviewHeight).value = previewHeight; var profileWidth = 128; if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10))) { profileWidth = parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10); } - config.ImageSettings.ProfileWidth = profileWidth; + config.FileSettings.ProfileWidth = profileWidth; React.findDOMNode(this.refs.ProfileWidth).value = profileWidth; var profileHeight = 128; if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10))) { profileHeight = parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10); } - config.ImageSettings.ProfileHeight = profileHeight; + config.FileSettings.ProfileHeight = profileHeight; React.findDOMNode(this.refs.ProfileHeight).value = profileHeight; Client.saveConfig( @@ -143,7 +143,7 @@ export default class ImageSettings extends React.Component { return (
    -

    {'Image Settings'}

    +

    {'File Settings'}

    @@ -185,7 +185,7 @@ export default class ImageSettings extends React.Component { id='Directory' ref='Directory' placeholder='Ex "./data/"' - defaultValue={this.props.config.ImageSettings.Directory} + defaultValue={this.props.config.FileSettings.Directory} onChange={this.handleChange} disabled={!enableFile} /> @@ -207,7 +207,7 @@ export default class ImageSettings extends React.Component { id='AmazonS3AccessKeyId' ref='AmazonS3AccessKeyId' placeholder='Ex "AKIADTOVBGERKLCBV"' - defaultValue={this.props.config.ImageSettings.AmazonS3AccessKeyId} + defaultValue={this.props.config.FileSettings.AmazonS3AccessKeyId} onChange={this.handleChange} disabled={!enableS3} /> @@ -229,7 +229,7 @@ export default class ImageSettings extends React.Component { id='AmazonS3SecretAccessKey' ref='AmazonS3SecretAccessKey' placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"' - defaultValue={this.props.config.ImageSettings.AmazonS3SecretAccessKey} + defaultValue={this.props.config.FileSettings.AmazonS3SecretAccessKey} onChange={this.handleChange} disabled={!enableS3} /> @@ -251,7 +251,7 @@ export default class ImageSettings extends React.Component { id='AmazonS3Bucket' ref='AmazonS3Bucket' placeholder='Ex "mattermost-media"' - defaultValue={this.props.config.ImageSettings.AmazonS3Bucket} + defaultValue={this.props.config.FileSettings.AmazonS3Bucket} onChange={this.handleChange} disabled={!enableS3} /> @@ -273,7 +273,7 @@ export default class ImageSettings extends React.Component { id='AmazonS3Region' ref='AmazonS3Region' placeholder='Ex "us-east-1"' - defaultValue={this.props.config.ImageSettings.AmazonS3Region} + defaultValue={this.props.config.FileSettings.AmazonS3Region} onChange={this.handleChange} disabled={!enableS3} /> @@ -295,7 +295,7 @@ export default class ImageSettings extends React.Component { id='ThumbnailWidth' ref='ThumbnailWidth' placeholder='Ex "120"' - defaultValue={this.props.config.ImageSettings.ThumbnailWidth} + defaultValue={this.props.config.FileSettings.ThumbnailWidth} onChange={this.handleChange} />

    {'Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'}

    @@ -316,7 +316,7 @@ export default class ImageSettings extends React.Component { id='ThumbnailHeight' ref='ThumbnailHeight' placeholder='Ex "100"' - defaultValue={this.props.config.ImageSettings.ThumbnailHeight} + defaultValue={this.props.config.FileSettings.ThumbnailHeight} onChange={this.handleChange} />

    {'Height of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'}

    @@ -337,7 +337,7 @@ export default class ImageSettings extends React.Component { id='PreviewWidth' ref='PreviewWidth' placeholder='Ex "1024"' - defaultValue={this.props.config.ImageSettings.PreviewWidth} + defaultValue={this.props.config.FileSettings.PreviewWidth} onChange={this.handleChange} />

    {'Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'}

    @@ -358,7 +358,7 @@ export default class ImageSettings extends React.Component { id='PreviewHeight' ref='PreviewHeight' placeholder='Ex "0"' - defaultValue={this.props.config.ImageSettings.PreviewHeight} + defaultValue={this.props.config.FileSettings.PreviewHeight} onChange={this.handleChange} />

    {'Maximum height of preview image ("0": Sets to auto-size). Updating this value changes how preview images render in future, but does not change images created in the past.'}

    @@ -379,7 +379,7 @@ export default class ImageSettings extends React.Component { id='ProfileWidth' ref='ProfileWidth' placeholder='Ex "1024"' - defaultValue={this.props.config.ImageSettings.ProfileWidth} + defaultValue={this.props.config.FileSettings.ProfileWidth} onChange={this.handleChange} />

    {'Width of profile picture.'}

    @@ -400,7 +400,7 @@ export default class ImageSettings extends React.Component { id='ProfileHeight' ref='ProfileHeight' placeholder='Ex "0"' - defaultValue={this.props.config.ImageSettings.ProfileHeight} + defaultValue={this.props.config.FileSettings.ProfileHeight} onChange={this.handleChange} />

    {'Height of profile picture.'}

    @@ -421,7 +421,7 @@ export default class ImageSettings extends React.Component { name='EnablePublicLink' value='true' ref='EnablePublicLink' - defaultChecked={this.props.config.ImageSettings.EnablePublicLink} + defaultChecked={this.props.config.FileSettings.EnablePublicLink} onChange={this.handleChange} /> {'true'} @@ -431,7 +431,7 @@ export default class ImageSettings extends React.Component { type='radio' name='EnablePublicLink' value='false' - defaultChecked={!this.props.config.ImageSettings.EnablePublicLink} + defaultChecked={!this.props.config.FileSettings.EnablePublicLink} onChange={this.handleChange} /> {'false'} @@ -454,7 +454,7 @@ export default class ImageSettings extends React.Component { id='PublicLinkSalt' ref='PublicLinkSalt' placeholder='Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"' - defaultValue={this.props.config.ImageSettings.PublicLinkSalt} + defaultValue={this.props.config.FileSettings.PublicLinkSalt} onChange={this.handleChange} />

    {'32-character salt added to signing of public image links.'}

    @@ -491,6 +491,6 @@ export default class ImageSettings extends React.Component { } } -ImageSettings.propTypes = { +FileSettings.propTypes = { config: React.PropTypes.object }; diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 8d23ec6463..b81936b57c 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -55,7 +55,7 @@ export default class ChannelHeader extends React.Component { if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } - $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover click', html: true, delay: {show: 500, hide: 500}}); + $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}}); } onSocketChange(msg) { if (msg.action === 'new_user') { diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index ce6f60f87e..962ba26ee7 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -12,6 +12,7 @@ var PostStore = require('../stores/post_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var Utils = require('../utils/utils.jsx'); +var Constants = require('../utils/constants.jsx'); export default class ChannelLoader extends React.Component { constructor(props) { @@ -68,33 +69,19 @@ export default class ChannelLoader extends React.Component { /* Update CSS classes to match user theme */ var user = UserStore.getCurrentUser(); - if (user.props && user.props.theme) { - Utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';'); - Utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';'); - Utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';'); - Utils.changeCss('.mention', 'background: ' + user.props.theme + ';'); - Utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';'); - Utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}'); - Utils.changeCss('.search-item-container:hover', 'background: ' + Utils.changeOpacity(user.props.theme, 0.05) + ';'); - } - - if (user.props.theme !== '#000000' && user.props.theme !== '#585858') { - Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, -10) + ';'); - Utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;'); - } else if (user.props.theme === '#000000') { - Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +50) + ';'); - $('.team__header').addClass('theme--black'); - } else if (user.props.theme === '#585858') { - Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +10) + ';'); - $('.team__header').addClass('theme--gray'); + if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) { + Utils.applyTheme(user.theme_props); + } else { + Utils.applyTheme(Constants.THEMES.default); } /* Setup global mouse events */ - $('body').on('click.userpopover', function popOver(e) { - if ($(e.target).attr('data-toggle') !== 'popover' && - $(e.target).parents('.popover.in').length === 0) { - $('.user-popover').popover('hide'); - } + $('body').on('click', function hidePopover(e) { + $('[data-toggle="popover"]').each(function eachPopover() { + if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.popover').has(e.target).length === 0) { + $(this).popover('hide'); + } + }); }); $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) { diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index d9e67836d8..abad601544 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -23,6 +23,7 @@ export default class CreatePost extends React.Component { this.lastTime = 0; + this.getCurrentDraft = this.getCurrentDraft.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.postMsgKeyPress = this.postMsgKeyPress.bind(this); this.handleUserInput = this.handleUserInput.bind(this); @@ -36,23 +37,15 @@ export default class CreatePost extends React.Component { PostStore.clearDraftUploads(); - const draft = PostStore.getCurrentDraft(); - let previews = []; - let messageText = ''; - let uploadsInProgress = []; - if (draft && draft.previews && draft.message) { - previews = draft.previews; - messageText = draft.message; - uploadsInProgress = draft.uploadsInProgress; - } + const draft = this.getCurrentDraft(); this.state = { channelId: ChannelStore.getCurrentId(), - messageText: messageText, - uploadsInProgress: uploadsInProgress, - previews: previews, + messageText: draft.messageText, + uploadsInProgress: draft.uploadsInProgress, + previews: draft.previews, submitting: false, - initialText: messageText + initialText: draft.messageText }; } componentDidUpdate(prevProps, prevState) { @@ -60,6 +53,24 @@ export default class CreatePost extends React.Component { this.resizePostHolder(); } } + getCurrentDraft() { + const draft = PostStore.getCurrentDraft(); + const safeDraft = {previews: [], messageText: '', uploadsInProgress: []}; + + if (draft) { + if (draft.message) { + safeDraft.messageText = draft.message; + } + if (draft.previews) { + safeDraft.previews = draft.previews; + } + if (draft.uploadsInProgress) { + safeDraft.uploadsInProgress = draft.uploadsInProgress; + } + } + + return safeDraft; + } handleSubmit(e) { e.preventDefault(); @@ -253,18 +264,9 @@ export default class CreatePost extends React.Component { onChange() { const channelId = ChannelStore.getCurrentId(); if (this.state.channelId !== channelId) { - let draft = PostStore.getCurrentDraft(); + const draft = this.getCurrentDraft(); - let previews = []; - let messageText = ''; - let uploadsInProgress = []; - if (draft && draft.previews && draft.message) { - previews = draft.previews; - messageText = draft.message; - uploadsInProgress = draft.uploadsInProgress; - } - - this.setState({channelId: channelId, messageText: messageText, initialText: messageText, submitting: false, serverError: null, postError: null, previews: previews, uploadsInProgress: uploadsInProgress}); + this.setState({channelId: channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress}); } } getFileCount(channelId) { diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx index 92123956fd..8d3f15525f 100644 --- a/web/react/components/email_verify.jsx +++ b/web/react/components/email_verify.jsx @@ -10,12 +10,14 @@ export default class EmailVerify extends React.Component { this.state = {}; } handleResend() { - window.location.href = window.location.href + '&resend=true'; + const newAddress = window.location.href.replace('&resend_success=true', ''); + window.location.href = newAddress + '&resend=true'; } render() { var title = ''; var body = ''; var resend = ''; + var resendConfirm = ''; if (this.props.isVerified === 'true') { title = global.window.config.SiteName + ' Email Verified'; body =

    Your email has been verified! Click {' Verification email sent.'}

    ; + } } return ( @@ -41,6 +46,7 @@ export default class EmailVerify extends React.Component {
    {body} {resend} + {resendConfirm}
    @@ -51,10 +57,12 @@ export default class EmailVerify extends React.Component { EmailVerify.defaultProps = { isVerified: 'false', teamURL: '', - userEmail: '' + userEmail: '', + resendSuccess: 'false' }; EmailVerify.propTypes = { isVerified: React.PropTypes.string, teamURL: React.PropTypes.string, - userEmail: React.PropTypes.string + userEmail: React.PropTypes.string, + resendSuccess: React.PropTypes.string }; diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index f27b09ecc6..b7bce9b34b 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -114,7 +114,7 @@ export default class MoreDirectChannels extends React.Component { Close -

    More Private Messages

    +

    More Direct Messages

    - {'This is the start of your private message history with ' + teammateName + '.'}
    - {'Private messages and files shared here are not shown to people outside this area.'} + {'This is the start of your direct message history with ' + teammateName + '.'}
    + {'Direct messages and files shared here are not shown to people outside this area.'}

    -

    {'This is the start of your private message history with this teammate. Private messages and files shared here are not shown to people outside this area.'}

    +

    {'This is the start of your direct message history with this teammate. Direct messages and files shared here are not shown to people outside this area.'}

    ); } diff --git a/web/react/components/post_list_container.jsx b/web/react/components/post_list_container.jsx index 0815ac8833..e59d85d41f 100644 --- a/web/react/components/post_list_container.jsx +++ b/web/react/components/post_list_container.jsx @@ -49,6 +49,7 @@ export default class PostListContainer extends React.Component { for (let i = 0; i <= this.state.postLists.length - 1; i++) { postListCtls.push( diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx index 3dd5c094ed..473ff3f918 100644 --- a/web/react/components/register_app_modal.jsx +++ b/web/react/components/register_app_modal.jsx @@ -228,7 +228,7 @@ export default class RegisterAppModal extends React.Component { data-dismiss='modal' aria-label='Close' > - +

    + ); diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index 2ea697c5bd..e661bdce1e 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -20,7 +20,6 @@ export default class RhsRootPost extends React.Component { } parseEmojis() { twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE}); - global.window.emojify.run(React.findDOMNode(this.refs.message_holder)); } componentDidMount() { this.parseEmojis(); @@ -54,7 +53,7 @@ export default class RhsRootPost extends React.Component { var channelName; if (channel) { if (channel.type === 'D') { - channelName = 'Private Message'; + channelName = 'Direct Message'; } else { channelName = channel.display_name; } diff --git a/web/react/components/search_results_header.jsx b/web/react/components/search_results_header.jsx index 694f0c55de..4e8a3ef100 100644 --- a/web/react/components/search_results_header.jsx +++ b/web/react/components/search_results_header.jsx @@ -50,6 +50,7 @@ export default class SearchResultsHeader extends React.Component { title='Close' onClick={this.handleClose} > + ); diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 0e951f5c66..32b521560e 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -64,7 +64,7 @@ export default class SearchResultsItem extends React.Component { if (channel) { channelName = channel.display_name; if (channel.type === 'D') { - channelName = 'Private Message'; + channelName = 'Direct Message'; } } diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 87007edcc2..14664ed4d5 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -566,7 +566,7 @@ export default class Sidebar extends React.Component { {privateChannelItems}
      -
    • Private Messages

    • +
    • Direct Messages

    • {directMessageItems} {directMessageMore}
    diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index 2671d560b9..f1341d9d7c 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -6,6 +6,10 @@ var client = require('../utils/client.jsx'); var utils = require('../utils/utils.jsx'); export default class SidebarRightMenu extends React.Component { + componentDidMount() { + $('.sidebar--left .dropdown-menu').perfectScrollbar(); + } + constructor(props) { super(props); diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index 7f320e0b2c..4112138fa3 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -4,7 +4,7 @@ const ChoosePage = require('./team_signup_choose_auth.jsx'); const EmailSignUpPage = require('./team_signup_with_email.jsx'); const SSOSignupPage = require('./team_signup_with_sso.jsx'); -var Constants = require('../utils/constants.jsx'); +const Constants = require('../utils/constants.jsx'); export default class TeamSignUp extends React.Component { constructor(props) { diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 4dad1ef4fd..8311747ee4 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); @@ -30,13 +30,26 @@ export default class SignupUserComplete extends React.Component { handleSubmit(e) { e.preventDefault(); + const providedEmail = React.findDOMNode(this.refs.email).value.trim(); + if (!providedEmail) { + this.setState({nameError: '', emailError: 'This field is required', passwordError: ''}); + return; + } + + if (!Utils.isEmail(providedEmail)) { + this.setState({nameError: '', emailError: 'Please enter a valid email address', passwordError: ''}); + return; + } + + this.state.user.email = providedEmail; + this.state.user.username = React.findDOMNode(this.refs.name).value.trim().toLowerCase(); if (!this.state.user.username) { this.setState({nameError: 'This field is required', emailError: '', passwordError: '', serverError: ''}); return; } - var usernameError = utils.isValidUsername(this.state.user.username); + var usernameError = Utils.isValidUsername(this.state.user.username); if (usernameError === 'Cannot use a reserved word as a username.') { this.setState({nameError: 'This username is reserved, please choose a new one.', emailError: '', passwordError: '', serverError: ''}); return; @@ -50,12 +63,6 @@ export default class SignupUserComplete extends React.Component { return; } - this.state.user.email = React.findDOMNode(this.refs.email).value.trim(); - if (!this.state.user.email) { - this.setState({nameError: '', emailError: 'This field is required', passwordError: ''}); - return; - } - this.state.user.password = React.findDOMNode(this.refs.password).value.trim(); if (!this.state.user.password || this.state.user.password .length < 5) { this.setState({nameError: '', emailError: '', passwordError: 'Please enter at least 5 characters', serverError: ''}); diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx new file mode 100644 index 0000000000..44630a318c --- /dev/null +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -0,0 +1,108 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Constants = require('../../utils/constants.jsx'); + +export default class CustomThemeChooser extends React.Component { + constructor(props) { + super(props); + + this.onPickerChange = this.onPickerChange.bind(this); + this.onInputChange = this.onInputChange.bind(this); + this.pasteBoxChange = this.pasteBoxChange.bind(this); + + this.state = {}; + } + componentDidMount() { + $('.color-picker').colorpicker().on('changeColor', this.onPickerChange); + } + onPickerChange(e) { + const theme = this.props.theme; + theme[e.target.id] = e.color.toHex(); + theme.type = 'custom'; + this.props.updateTheme(theme); + } + onInputChange(e) { + const theme = this.props.theme; + theme[e.target.parentNode.id] = e.target.value; + theme.type = 'custom'; + this.props.updateTheme(theme); + } + pasteBoxChange(e) { + const text = e.target.value; + + if (text.length === 0) { + return; + } + + const colors = text.split(','); + + const theme = {type: 'custom'}; + let index = 0; + Constants.THEME_ELEMENTS.forEach((element) => { + if (index < colors.length) { + theme[element.id] = colors[index]; + } + index++; + }); + + this.props.updateTheme(theme); + } + render() { + const theme = this.props.theme; + + const elements = []; + let colors = ''; + Constants.THEME_ELEMENTS.forEach((element) => { + elements.push( +
    + +
    + + +
    +
    + ); + + colors += theme[element.id] + ','; + }); + + const pasteBox = ( +
    + + +
    + ); + + return ( +
    +
    + {elements} +
    +
    + {pasteBox} +
    +
    + ); + } +} + +CustomThemeChooser.propTypes = { + theme: React.PropTypes.object.isRequired, + updateTheme: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx new file mode 100644 index 0000000000..48be83afe7 --- /dev/null +++ b/web/react/components/user_settings/import_theme_modal.jsx @@ -0,0 +1,179 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +const UserStore = require('../../stores/user_store.jsx'); +const Utils = require('../../utils/utils.jsx'); +const Client = require('../../utils/client.jsx'); +const Modal = ReactBootstrap.Modal; + +const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx'); +const Constants = require('../../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; + +export default class ImportThemeModal extends React.Component { + constructor(props) { + super(props); + + this.updateShow = this.updateShow.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleChange = this.handleChange.bind(this); + + this.state = { + inputError: '', + show: false + }; + } + componentDidMount() { + UserStore.addImportModalListener(this.updateShow); + } + componentWillUnmount() { + UserStore.removeImportModalListener(this.updateShow); + } + updateShow(show) { + this.setState({show}); + } + handleSubmit(e) { + e.preventDefault(); + + const text = React.findDOMNode(this.refs.input).value; + + if (!this.isInputValid(text)) { + this.setState({inputError: 'Invalid format, please try copying and pasting in again.'}); + return; + } + + const colors = text.split(','); + const theme = {type: 'custom'}; + + theme.sidebarBg = colors[0]; + theme.sidebarText = colors[5]; + theme.sidebarUnreadText = colors[5]; + theme.sidebarTextHoverBg = colors[4]; + theme.sidebarTextHoverColor = colors[5]; + theme.sidebarTextActiveBg = colors[2]; + theme.sidebarTextActiveColor = colors[3]; + theme.sidebarHeaderBg = colors[1]; + theme.sidebarHeaderTextColor = colors[5]; + theme.onlineIndicator = colors[6]; + theme.mentionBj = colors[7]; + theme.mentionColor = '#ffffff'; + theme.centerChannelBg = '#ffffff'; + theme.centerChannelColor = '#333333'; + theme.linkColor = '#2389d7'; + theme.buttonBg = '#26a970'; + theme.buttonColor = '#ffffff'; + + let user = UserStore.getCurrentUser(); + user.theme_props = theme; + + Client.updateUser(user, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_ME, + me: data + }); + + this.setState({show: false}); + Utils.applyTheme(theme); + $('#user_settings').modal('show'); + }, + (err) => { + var state = this.getStateFromStores(); + state.serverError = err; + this.setState(state); + } + ); + } + isInputValid(text) { + if (text.length === 0) { + return false; + } + + if (text.indexOf(' ') !== -1) { + return false; + } + + if (text.length > 0 && text.indexOf(',') === -1) { + return false; + } + + if (text.length > 0) { + const colors = text.split(','); + + if (colors.length !== 8) { + return false; + } + + for (let i = 0; i < colors.length; i++) { + if (colors[i].length !== 7 && colors[i].length !== 4) { + return false; + } + + if (colors[i].charAt(0) !== '#') { + return false; + } + } + } + + return true; + } + handleChange(e) { + if (this.isInputValid(e.target.value)) { + this.setState({inputError: null}); + } else { + this.setState({inputError: 'Invalid format, please try copying and pasting in again.'}); + } + } + render() { + return ( + + this.setState({show: false})} + > + + {'Import Slack Theme'} + + + +

    + {'To import a theme, go to a Slack team and look for “”Preferences” -> Sidebar Theme”. Open the custom theme option, copy the theme color values and paste them here:'} +

    +
    +
    + + {this.state.inputError} +
    +
    +
    + + + + + +
    +
    + ); + } +} diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx new file mode 100644 index 0000000000..e6aa2f5b9a --- /dev/null +++ b/web/react/components/user_settings/premade_theme_chooser.jsx @@ -0,0 +1,55 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Utils = require('../../utils/utils.jsx'); +var Constants = require('../../utils/constants.jsx'); + +export default class PremadeThemeChooser extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + render() { + const theme = this.props.theme; + + const premadeThemes = []; + for (const k in Constants.THEMES) { + if (Constants.THEMES.hasOwnProperty(k)) { + const premadeTheme = $.extend(true, {}, Constants.THEMES[k]); + + let activeClass = ''; + if (premadeTheme.type === theme.type) { + activeClass = 'active'; + } + + premadeThemes.push( +
    +
    this.props.updateTheme(premadeTheme)} + > + +
    +
    + ); + } + } + + return ( +
    + {premadeThemes} +
    + ); + } +} + +PremadeThemeChooser.propTypes = { + theme: React.PropTypes.object.isRequired, + updateTheme: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx index aec3b319df..7617f04d1e 100644 --- a/web/react/components/user_settings/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -2,64 +2,32 @@ // See License.txt for license information. var UserStore = require('../../stores/user_store.jsx'); -var SettingItemMin = require('../setting_item_min.jsx'); -var SettingItemMax = require('../setting_item_max.jsx'); var Client = require('../../utils/client.jsx'); var Utils = require('../../utils/utils.jsx'); -var ThemeColors = ['#2389d7', '#008a17', '#dc4fad', '#ac193d', '#0072c6', '#d24726', '#ff8f32', '#82ba00', '#03b3b2', '#008299', '#4617b4', '#8c0095', '#004b8b', '#004b8b', '#570000', '#380000', '#585858', '#000000']; +const CustomThemeChooser = require('./custom_theme_chooser.jsx'); +const PremadeThemeChooser = require('./premade_theme_chooser.jsx'); +const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx'); +const Constants = require('../../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; export default class UserSettingsAppearance extends React.Component { constructor(props) { super(props); + this.onChange = this.onChange.bind(this); this.submitTheme = this.submitTheme.bind(this); this.updateTheme = this.updateTheme.bind(this); this.handleClose = this.handleClose.bind(this); + this.handleImportModal = this.handleImportModal.bind(this); this.state = this.getStateFromStores(); - } - getStateFromStores() { - var user = UserStore.getCurrentUser(); - var theme = '#2389d7'; - if (ThemeColors != null) { - theme = ThemeColors[0]; - } - if (user.props && user.props.theme) { - theme = user.props.theme; - } - return {theme: theme.toLowerCase()}; - } - submitTheme(e) { - e.preventDefault(); - var user = UserStore.getCurrentUser(); - if (!user.props) { - user.props = {}; - } - user.props.theme = this.state.theme; - - Client.updateUser(user, - function success() { - this.props.updateSection(''); - window.location.reload(); - }.bind(this), - function fail(err) { - var state = this.getStateFromStores(); - state.serverError = err; - this.setState(state); - }.bind(this) - ); - } - updateTheme(e) { - var hex = Utils.rgb2hex(e.target.style.backgroundColor); - this.setState({theme: hex.toLowerCase()}); - } - handleClose() { - this.setState({serverError: null}); - this.props.updateTab('general'); + this.originalTheme = this.state.theme; } componentDidMount() { + UserStore.addChangeListener(this.onChange); + if (this.props.activeSection === 'theme') { $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); } @@ -72,8 +40,81 @@ export default class UserSettingsAppearance extends React.Component { } } componentWillUnmount() { + UserStore.removeChangeListener(this.onChange); $('#user_settings').off('hidden.bs.modal', this.handleClose); - this.props.updateSection(''); + } + getStateFromStores() { + const user = UserStore.getCurrentUser(); + let theme = null; + + if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) { + theme = user.theme_props; + } else { + theme = $.extend(true, {}, Constants.THEMES.default); + } + + let type = 'premade'; + if (theme.type === 'custom') { + type = 'custom'; + } + + return {theme, type}; + } + onChange() { + const newState = this.getStateFromStores(); + + if (!Utils.areStatesEqual(this.state, newState)) { + this.setState(newState); + } + } + submitTheme(e) { + e.preventDefault(); + var user = UserStore.getCurrentUser(); + user.theme_props = this.state.theme; + + Client.updateUser(user, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_ME, + me: data + }); + + $('#user_settings').off('hidden.bs.modal', this.handleClose); + this.props.updateTab('general'); + $('#user_settings').modal('hide'); + }, + (err) => { + var state = this.getStateFromStores(); + state.serverError = err; + this.setState(state); + } + ); + } + updateTheme(theme) { + this.setState({theme}); + Utils.applyTheme(theme); + } + updateType(type) { + this.setState({type}); + } + handleClose() { + const state = this.getStateFromStores(); + state.serverError = null; + + Utils.applyTheme(state.theme); + + this.setState(state); + + $('.ps-container.modal-body').scrollTop(0); + $('.ps-container.modal-body').perfectScrollbar('update'); + $('#user_settings').modal('hide'); + } + handleImportModal() { + $('#user_settings').modal('hide'); + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_IMPORT_THEME_MODAL, + value: true + }); } render() { var serverError; @@ -81,67 +122,73 @@ export default class UserSettingsAppearance extends React.Component { serverError = this.state.serverError; } - var themeSection; - var self = this; + const displayCustom = this.state.type === 'custom'; - if (ThemeColors != null) { - if (this.props.activeSection === 'theme') { - var themeButtons = []; - - for (var i = 0; i < ThemeColors.length; i++) { - themeButtons.push( -

    - Appearance Settings + {'Appearance Settings'}

    -

    Appearance Settings

    +

    {'Appearance Settings'}

    - {themeSection} + {themeUI}
    +
    + + {'Import from Slack'} +
    ); } @@ -176,6 +230,5 @@ UserSettingsAppearance.defaultProps = { }; UserSettingsAppearance.propTypes = { activeSection: React.PropTypes.string, - updateSection: React.PropTypes.func, updateTab: React.PropTypes.func }; diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx index 1694aaa79d..d9fb43902d 100644 --- a/web/react/components/user_settings/user_settings_developer.jsx +++ b/web/react/components/user_settings/user_settings_developer.jsx @@ -64,7 +64,7 @@ export default class DeveloperTab extends React.Component { data-dismiss='modal' aria-label='Close' > - +

    - +

    - Only for mentions and private messages + Only for mentions and direct messages
    @@ -277,7 +277,7 @@ export default class NotificationsTab extends React.Component { } else { let describe = ''; if (this.state.notifyLevel === 'mention') { - describe = 'Only for mentions and private messages'; + describe = 'Only for mentions and direct messages'; } else if (this.state.notifyLevel === 'none') { describe = 'Never'; } else { @@ -414,7 +414,7 @@ export default class NotificationsTab extends React.Component {

    -

    {'Email notifications are sent for mentions and private messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}
    +

    {'Email notifications are sent for mentions and direct messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}
    ); diff --git a/web/react/package.json b/web/react/package.json index dd7d45f8ae..a9eba6c6cb 100644 --- a/web/react/package.json +++ b/web/react/package.json @@ -4,13 +4,14 @@ "private": true, "dependencies": { "autolinker": "0.18.1", + "babel-runtime": "5.8.24", + "bootstrap-colorpicker": "2.2.0", "flux": "2.1.1", "keymirror": "0.1.1", + "marked": "0.3.5", "object-assign": "3.0.0", "react-zeroclipboard-mixin": "0.1.0", - "twemoji": "1.4.1", - "babel-runtime": "5.8.24", - "marked": "0.3.5" + "twemoji": "1.4.1" }, "devDependencies": { "browserify": "11.0.1", @@ -28,8 +29,15 @@ }, "browserify": { "transform": [ - ["babelify", { "optional": ["runtime"] }], - "envify" + [ + "babelify", + { + "optional": [ + "runtime" + ] + } + ], + "envify" ] }, "jest": { diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index d24fe0b98e..07207c5560 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -34,6 +34,7 @@ var ActivityLogModal = require('../components/activity_log_modal.jsx'); var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx'); var FileUploadOverlay = require('../components/file_upload_overlay.jsx'); var RegisterAppModal = require('../components/register_app_modal.jsx'); +var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; @@ -84,6 +85,11 @@ function setupChannelPage(props) { document.getElementById('user_settings_modal') ); + React.render( + , + document.getElementById('import_theme_modal') + ); + React.render( , document.getElementById('team_settings_modal') diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx index e48471bbd1..16a9846e51 100644 --- a/web/react/pages/verify.jsx +++ b/web/react/pages/verify.jsx @@ -9,6 +9,7 @@ global.window.setupVerifyPage = function setupVerifyPage(props) { isVerified={props.IsVerified} teamURL={props.TeamURL} userEmail={props.UserEmail} + resendSuccess={props.ResendSuccess} />, document.getElementById('verify') ); diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index f75c1d4c33..8842263fa8 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -14,6 +14,7 @@ var CHANGE_EVENT_SESSIONS = 'change_sessions'; var CHANGE_EVENT_AUDITS = 'change_audits'; var CHANGE_EVENT_TEAMS = 'change_teams'; var CHANGE_EVENT_STATUSES = 'change_statuses'; +var TOGGLE_IMPORT_MODAL_EVENT = 'toggle_import_modal'; class UserStoreClass extends EventEmitter { constructor() { @@ -34,6 +35,9 @@ class UserStoreClass extends EventEmitter { this.emitStatusesChange = this.emitStatusesChange.bind(this); this.addStatusesChangeListener = this.addStatusesChangeListener.bind(this); this.removeStatusesChangeListener = this.removeStatusesChangeListener.bind(this); + this.emitToggleImportModal = this.emitToggleImportModal.bind(this); + this.addImportModalListener = this.addImportModalListener.bind(this); + this.removeImportModalListener = this.removeImportModalListener.bind(this); this.setCurrentId = this.setCurrentId.bind(this); this.getCurrentId = this.getCurrentId.bind(this); this.getCurrentUser = this.getCurrentUser.bind(this); @@ -114,6 +118,15 @@ class UserStoreClass extends EventEmitter { removeStatusesChangeListener(callback) { this.removeListener(CHANGE_EVENT_STATUSES, callback); } + emitToggleImportModal(value) { + this.emit(TOGGLE_IMPORT_MODAL_EVENT, value); + } + addImportModalListener(callback) { + this.on(TOGGLE_IMPORT_MODAL_EVENT, callback); + } + removeImportModalListener(callback) { + this.removeListener(TOGGLE_IMPORT_MODAL_EVENT, callback); + } setCurrentId(id) { this.gCurrentId = id; if (id == null) { @@ -321,6 +334,9 @@ UserStore.dispatchToken = AppDispatcher.register(function registry(payload) { UserStore.pSetStatuses(action.statuses); UserStore.emitStatusesChange(); break; + case ActionTypes.TOGGLE_IMPORT_THEME_MODAL: + UserStore.emitToggleImportModal(action.value); + break; default: } diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index d17dfbe304..f58816862b 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -36,7 +36,9 @@ module.exports = { RECIEVED_CONFIG: null, RECIEVED_LOGS: null, - RECIEVED_ALL_TEAMS: null + RECIEVED_ALL_TEAMS: null, + + TOGGLE_IMPORT_THEME_MODAL: null }), PayloadSources: keyMirror({ @@ -110,5 +112,138 @@ module.exports = { ONLINE_ICON_SVG: "", OFFLINE_ICON_SVG: "", MENU_ICON: " ", - COMMENT_ICON: " " + COMMENT_ICON: " ", + THEMES: { + default: { + type: 'Mattermost', + sidebarBg: '#fafafa', + sidebarText: '#999999', + sidebarUnreadText: '#333333', + sidebarTextHoverBg: '#e6f2fa', + sidebarTextHoverColor: '#999999', + sidebarTextActiveBg: '#e1e1e1', + sidebarTextActiveColor: '#111111', + sidebarHeaderBg: '#2389d7', + sidebarHeaderTextColor: '#ffffff', + onlineIndicator: '#7DBE00', + mentionBj: '#2389d7', + mentionColor: '#ffffff', + centerChannelBg: '#ffffff', + centerChannelColor: '#333333', + linkColor: '#2389d7', + buttonBg: '#2389d7', + buttonColor: '#FFFFFF' + }, + slack: { + type: 'Slack', + sidebarBg: '#4D394B', + sidebarText: '#ab9ba9', + sidebarUnreadText: '#FFFFFF', + sidebarTextHoverBg: '#3e313c', + sidebarTextHoverColor: '#ab9ba9', + sidebarTextActiveBg: '#4c9689', + sidebarTextActiveColor: '#FFFFFF', + sidebarHeaderBg: '#4D394B', + sidebarHeaderTextColor: '#FFFFFF', + onlineIndicator: '#4c9689', + mentionBj: '#eb4d5c', + mentionColor: '#FFFFFF', + centerChannelBg: '#FFFFFF', + centerChannelColor: '#333333', + linkColor: '#2389d7', + buttonBg: '#26a970', + buttonColor: '#FFFFFF' + }, + dark: { + type: 'Dark', + sidebarBg: '#1B2C3E', + sidebarText: '#bbbbbb', + sidebarUnreadText: '#fff', + sidebarTextHoverBg: '#4A5664', + sidebarTextHoverColor: '#bbbbbb', + sidebarTextActiveBg: '#39769C', + sidebarTextActiveColor: '#FFFFFF', + sidebarHeaderBg: '#1B2C3E', + sidebarHeaderTextColor: '#FFFFFF', + onlineIndicator: '#4c9689', + mentionBj: '#B74A4A', + mentionColor: '#FFFFFF', + centerChannelBg: '#2F3E4E', + centerChannelColor: '#DDDDDD', + linkColor: '#A4FFEB', + buttonBg: '#2B9C99', + buttonColor: '#FFFFFF' + } + }, + THEME_ELEMENTS: [ + { + id: 'sidebarBg', + uiName: 'Sidebar BG' + }, + { + id: 'sidebarText', + uiName: 'Sidebar text color' + }, + { + id: 'sidebarHeaderBg', + uiName: 'Sidebar Header BG' + }, + { + id: 'sidebarHeaderTextColor', + uiName: 'Sidebar Header text color' + }, + { + id: 'sidebarUnreadText', + uiName: 'Sidebar unread text color' + }, + { + id: 'sidebarTextHoverBg', + uiName: 'Sidebar text hover BG' + }, + { + id: 'sidebarTextHoverColor', + uiName: 'Sidebar text hover color' + }, + { + id: 'sidebarTextActiveBg', + uiName: 'Sidebar text active BG' + }, + { + id: 'sidebarTextActiveColor', + uiName: 'Sidebar text active color' + }, + { + id: 'onlineIndicator', + uiName: 'Online indicator' + }, + { + id: 'mentionBj', + uiName: 'Mention jewel BG' + }, + { + id: 'mentionColor', + uiName: 'Mention jewel text color' + }, + { + id: 'centerChannelBg', + uiName: 'Center channel BG' + }, + { + id: 'centerChannelColor', + uiName: 'Center channel text color' + }, + { + id: 'linkColor', + uiName: 'Link color' + }, + { + id: 'buttonBg', + uiName: 'Button BG' + }, + + { + id: 'buttonColor', + uiName: 'Button Color' + } + ] }; diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx new file mode 100644 index 0000000000..7210201ff5 --- /dev/null +++ b/web/react/utils/emoticons.jsx @@ -0,0 +1,159 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +const emoticonPatterns = { + smile: /:-?\)/g, // :) + open_mouth: /:o/gi, // :o + scream: /:-o/gi, // :-o + smirk: /[:;]-?]/g, // :] + grinning: /[:;]-?d/gi, // :D + stuck_out_tongue_closed_eyes: /x-d/gi, // x-d + stuck_out_tongue_winking_eye: /[:;]-?p/gi, // ;p + rage: /:-?[\[@]/g, // :@ + frowning: /:-?\(/g, // :( + sob: /:['’]-?\(|:'\(/g, // :`( + kissing_heart: /:-?\*/g, // :* + wink: /;-?\)/g, // ;) + pensive: /:-?\//g, // :/ + confounded: /:-?s/gi, // :s + flushed: /:-?\|/g, // :| + relaxed: /:-?\$/g, // :$ + mask: /:-x/gi, // :-x + heart: /<3|<3/g, // <3 + broken_heart: /<\/3|</3/g, // `, + originalText: match + }); + + return alias; + } + + return match; + } + + output = output.replace(/:([a-zA-Z0-9_-]+):/g, replaceEmoticonWithToken); + + $.each(emoticonPatterns, (name, pattern) => { + // this might look a bit funny, but since the name isn't contained in the actual match + // like with the named emoticons, we need to add it in manually + output = output.replace(pattern, (match) => replaceEmoticonWithToken(match, name)); + }); + + return output; +} + +function getImagePathForEmoticon(name) { + return `/static/images/emoji/${name}.png`; +} diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index 96da542175..347024e1a2 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -1,9 +1,25 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. +const TextFormatting = require('./text_formatting.jsx'); + const marked = require('marked'); export class MattermostMarkdownRenderer extends marked.Renderer { + constructor(options, formattingOptions = {}) { + super(options); + + this.heading = this.heading.bind(this); + this.text = this.text.bind(this); + + this.formattingOptions = formattingOptions; + } + + heading(text, level, raw) { + const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`; + return `${text}`; + } + link(href, title, text) { let outHref = href; @@ -11,7 +27,7 @@ export class MattermostMarkdownRenderer extends marked.Renderer { outHref = `http://${outHref}`; } - let output = '${header}${body}`; + } + + text(text) { + return TextFormatting.doFormatText(text, this.formattingOptions); + } } diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 4e390f708c..56bf49c3ff 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -3,41 +3,58 @@ const Autolinker = require('autolinker'); const Constants = require('./constants.jsx'); +const Emoticons = require('./emoticons.jsx'); const Markdown = require('./markdown.jsx'); const UserStore = require('../stores/user_store.jsx'); const Utils = require('./utils.jsx'); const marked = require('marked'); -const markdownRenderer = new Markdown.MattermostMarkdownRenderer(); - // Performs formatting of user posts including highlighting mentions and search terms and converting urls, hashtags, and // @mentions to links by taking a user's message and returning a string of formatted html. Also takes a number of options // as part of the second parameter: // - searchTerm - If specified, this word is highlighted in the resulting html. Defaults to nothing. // - mentionHighlight - Specifies whether or not to highlight mentions of the current user. Defaults to true. // - singleline - Specifies whether or not to remove newlines. Defaults to false. +// - emoticons - Enables emoticon parsing. Defaults to true. // - markdown - Enables markdown parsing. Defaults to true. export function formatText(text, options = {}) { - if (!('markdown' in options)) { - options.markdown = true; + let output; + + if (!('markdown' in options) || options.markdown) { + // the markdown renderer will call doFormatText as necessary so just call marked + output = marked(text, { + renderer: new Markdown.MattermostMarkdownRenderer(null, options), + sanitize: true + }); + } else { + output = sanitizeHtml(text); + output = doFormatText(output, options); } - // wait until marked can sanitize the html so that we don't break markdown block quotes - let output; - if (!options.markdown) { - output = sanitizeHtml(text); - } else { - output = text; + // replace newlines with spaces if necessary + if (options.singleline) { + output = replaceNewlines(output); } + return output; +} + +// Performs most of the actual formatting work for formatText. Not intended to be called normally. +export function doFormatText(text, options) { + let output = text; + const tokens = new Map(); // replace important words and phrases with tokens - output = autolinkUrls(output, tokens, !!options.markdown); + output = autolinkUrls(output, tokens); output = autolinkAtMentions(output, tokens); output = autolinkHashtags(output, tokens); + if (!('emoticons' in options) || options.emoticon) { + output = Emoticons.handleEmoticons(output, tokens); + } + if (options.searchTerm) { output = highlightSearchTerm(output, tokens, options.searchTerm); } @@ -46,22 +63,9 @@ export function formatText(text, options = {}) { output = highlightCurrentMentions(output, tokens); } - // perform markdown parsing while we have an html-free input string - if (options.markdown) { - output = marked(output, { - renderer: markdownRenderer, - sanitize: true - }); - } - // reinsert tokens with formatted versions of the important words and phrases output = replaceTokens(output, tokens); - // replace newlines with html line breaks - if (options.singleline) { - output = replaceNewlines(output); - } - return output; } @@ -78,7 +82,7 @@ export function sanitizeHtml(text) { return output; } -function autolinkUrls(text, tokens, markdown) { +function autolinkUrls(text, tokens) { function replaceUrlWithToken(autolinker, match) { const linkText = match.getMatchedText(); let url = linkText; @@ -108,30 +112,7 @@ function autolinkUrls(text, tokens, markdown) { replaceFn: replaceUrlWithToken }); - let output = text; - - // temporarily replace markdown links if markdown is enabled so that we don't accidentally parse them twice - const markdownLinkTokens = new Map(); - if (markdown) { - function replaceMarkdownLinkWithToken(markdownLink) { - const index = markdownLinkTokens.size; - const alias = `MM_MARKDOWNLINK${index}`; - - markdownLinkTokens.set(alias, {value: markdownLink}); - - return alias; - } - - output = output.replace(/\]\([^\)]*\)/g, replaceMarkdownLinkWithToken); - } - - output = autolinker.link(output); - - if (markdown) { - output = replaceTokens(output, markdownLinkTokens); - } - - return output; + return autolinker.link(text); } function autolinkAtMentions(text, tokens) { @@ -241,7 +222,7 @@ function autolinkHashtags(text, tokens) { return prefix + alias; } - return output.replace(/(^|\W)(#[a-zA-Z0-9.\-_]+)\b/g, replaceHashtagWithToken); + return output.replace(/(^|\W)(#[a-zA-Z][a-zA-Z0-9.\-_]*)\b/g, replaceHashtagWithToken); } function highlightSearchTerm(text, tokens, searchTerm) { diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 0745914891..82bb82d6b2 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -542,7 +542,119 @@ export function toTitleCase(str) { return str.replace(/\w\S*/g, doTitleCase); } -export function changeCss(className, classValue) { +export function applyTheme(theme) { + if (theme.sidebarBg) { + changeCss('.sidebar--left', 'background:' + theme.sidebarBg, 1); + changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarBg, 1); + } + + if (theme.sidebarText) { + changeCss('.sidebar--left .nav li>a, .sidebar--right', 'color:' + theme.sidebarText, 1); + changeCss('.sidebar--left .nav li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1); + changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1); + changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 1); + changeCss('.sidebar--right .sidebar-right__body', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2); + changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1); + } + + if (theme.sidebarUnreadText) { + changeCss('.sidebar--left .nav li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 1); + } + + if (theme.sidebarTextHoverBg) { + changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'background:' + theme.sidebarTextHoverBg, 1); + } + + if (theme.sidebarTextHoverColor) { + changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'color:' + theme.sidebarTextHoverColor, 2); + } + + if (theme.sidebarTextActiveBg) { + changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + theme.sidebarTextActiveBg, 1); + } + + if (theme.sidebarTextActiveColor) { + changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'color:' + theme.sidebarTextActiveColor, 2); + } + + if (theme.sidebarHeaderBg) { + changeCss('.sidebar--left .team__header, .sidebar--menu .team__header', 'background:' + theme.sidebarHeaderBg, 1); + changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1); + changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1); + } + + if (theme.sidebarHeaderTextColor) { + changeCss('.sidebar--left .team__header .header__info, .sidebar--menu .team__header .header__info', 'color:' + theme.sidebarHeaderTextColor, 1); + changeCss('.sidebar--left .team__header .user__name, .sidebar--menu .team__header .user__name', 'color:' + changeOpacity(theme.sidebarHeaderTextColor, 0.8), 1); + changeCss('.sidebar--left .team__header:hover .user__name, .sidebar--menu .team__header:hover .user__name', 'color:' + theme.sidebarHeaderTextColor, 1); + changeCss('.modal .modal-header .modal-title, .modal .modal-header .modal-title .name, .modal .modal-header button.close', 'color:' + theme.sidebarHeaderTextColor, 1); + changeCss('#navbar .navbar-default .navbar-brand .heading, ', 'color:' + theme.sidebarHeaderTextColor, 1); + changeCss('#navbar .navbar-default .navbar-toggle .icon-bar, ', 'background:' + theme.sidebarHeaderTextColor, 1); + } + + if (theme.onlineIndicator) { + changeCss('.sidebar--left .status .online--icon', 'fill:' + theme.onlineIndicator, 1); + } + + if (theme.mentionBj) { + changeCss('.sidebar--left .nav-pills__unread-indicator', 'background:' + theme.mentionBj, 1); + changeCss('.sidebar--left .badge', 'background:' + theme.mentionBj, 1); + } + + if (theme.mentionColor) { + changeCss('.sidebar--left .nav-pills__unread-indicator', 'color:' + theme.mentionColor, 2); + changeCss('.sidebar--left .badge', 'color:' + theme.mentionColor, 2); + } + + if (theme.centerChannelBg) { + changeCss('.app__content', 'background:' + theme.centerChannelBg, 1); + changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1); + changeCss('#post-create', 'background:' + theme.centerChannelBg, 1); + changeCss('.search-bar__container .search__form .search-bar', 'background:' + theme.centerChannelBg, 1); + changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1); + changeCss('.sidebar--right', 'background:' + theme.centerChannelBg, 1); + } + + if (theme.centerChannelColor) { + changeCss('.app__content', 'color:' + theme.centerChannelColor, 2); + changeCss('#post-create', 'color:' + theme.centerChannelColor, 2); + changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1); + changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); + changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); + changeCss('.custom-textarea', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '!important; color: ' + theme.centerChannelColor, 1); + changeCss('.search-bar__container .search__form .search-bar', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: ' + theme.centerChannelColor, 2); + changeCss('.search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1); + changeCss('.date-separator .separator__text, .new-separator .separator__text', 'color:' + theme.centerChannelColor, 2); + changeCss('.date-separator .separator__hr, .new-separator .separator__hr, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.channel-intro', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); + changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); + changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); + changeCss('.post:hover, .sidebar--right .sidebar--right__header', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.date-separator.hovered--before:after, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.date-separator.hovered--after:before, .new-separator.hovered--after:before', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.post.current--user:hover .post-body ', 'background: none;', 1); + changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2); + } + + if (theme.linkColor) { + changeCss('a, a:focus, a:hover', 'color:' + theme.linkColor, 1); + changeCss('.post .comment-icon__container', 'fill:' + theme.linkColor, 1); + } + + if (theme.buttonBg) { + changeCss('.btn.btn-primary', 'background:' + theme.buttonBg, 1); + changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25), 1); + } + + if (theme.buttonColor) { + changeCss('.btn.btn-primary', 'color:' + theme.buttonColor, 2); + } +} + +export function changeCss(className, classValue, classRepeat) { // we need invisible container to store additional css definitions var cssMainContainer = $('#css-modifier-container'); if (cssMainContainer.length === 0) { @@ -552,9 +664,9 @@ export function changeCss(className, classValue) { } // and we need one div for each class - var classContainer = cssMainContainer.find('div[data-class="' + className + '"]'); + var classContainer = cssMainContainer.find('div[data-class="' + className + classRepeat + '"]'); if (classContainer.length === 0) { - classContainer = $('
    '); + classContainer = $('
    '); classContainer.appendTo(cssMainContainer); } @@ -760,57 +872,47 @@ Image.prototype.load = function imageLoad(url, progressCallback) { Image.prototype.completedPercentage = 0; export function changeColor(colourIn, amt) { - var usePound = false; - var col = colourIn; + var hex = colourIn; + var lum = amt; - if (col[0] === '#') { - col = col.slice(1); - usePound = true; + // validate hex string + hex = String(hex).replace(/[^0-9a-f]/gi, ''); + if (hex.length < 6) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + lum = lum || 0; + + // convert to decimal and change luminosity + var rgb = '#'; + var c; + var i; + for (i = 0; i < 3; i++) { + c = parseInt(hex.substr(i * 2, 2), 16); + c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16); + rgb += ('00' + c).substr(c.length); } - var num = parseInt(col, 16); - - var r = (num >> 16) + amt; - - if (r > 255) { - r = 255; - } else if (r < 0) { - r = 0; - } - - var b = ((num >> 8) & 0x00FF) + amt; - - if (b > 255) { - b = 255; - } else if (b < 0) { - b = 0; - } - - var g = (num & 0x0000FF) + amt; - - if (g > 255) { - g = 255; - } else if (g < 0) { - g = 0; - } - - var pound = '#'; - if (!usePound) { - pound = ''; - } - - return pound + String('000000' + (g | (b << 8) | (r << 16)).toString(16)).slice(-6); + return rgb; } export function changeOpacity(oldColor, opacity) { - var col = oldColor; - if (col[0] === '#') { - col = col.slice(1); + var color = oldColor; + if (color[0] === '#') { + color = color.slice(1); } - var r = parseInt(col.substring(0, 2), 16); - var g = parseInt(col.substring(2, 4), 16); - var b = parseInt(col.substring(4, 6), 16); + if (color.length === 3) { + const tempColor = color; + color = ''; + + color += tempColor[0] + tempColor[0]; + color += tempColor[1] + tempColor[1]; + color += tempColor[2] + tempColor[2]; + } + + var r = parseInt(color.substring(0, 2), 16); + var g = parseInt(color.substring(2, 4), 16); + var b = parseInt(color.substring(4, 6), 16); return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')'; } @@ -955,7 +1057,8 @@ export function getTeamURLFromAddressBar() { export function getShortenedTeamURL() { const teamURL = getTeamURLFromAddressBar(); - if (teamURL.length > 24) { + if (teamURL.length > 35) { return teamURL.substring(0, 10) + '...' + teamURL.substring(teamURL.length - 12, teamURL.length) + '/'; } + return teamURL + '/'; } diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index cdc2152e96..592d5e62e2 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -40,14 +40,11 @@ b, strong { a { word-break: break-word; -} - -a.theme { color: $primary-color; } -div.theme { - background-color: $primary-color; +a:focus, a:hover { + color: $primary-color; } .tooltip { diff --git a/web/sass-files/sass/partials/_colorpicker.scss b/web/sass-files/sass/partials/_colorpicker.scss new file mode 100644 index 0000000000..431f9d8d04 --- /dev/null +++ b/web/sass-files/sass/partials/_colorpicker.scss @@ -0,0 +1,251 @@ +/*! + * Bootstrap Colorpicker + * http://mjolnic.github.io/bootstrap-colorpicker/ + * + * Originally written by (c) 2012 Stefan Petre + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + * + */ + +.colorpicker-saturation { + float: left; + width: 100px; + height: 100px; + cursor: crosshair; + background-image: url("../images/bootstrap-colorpicker/saturation.png"); +} + +.colorpicker-saturation i { + position: absolute; + top: 0; + left: 0; + display: block; + width: 5px; + height: 5px; + margin: -4px 0 0 -4px; + border: 1px solid #000; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.colorpicker-saturation i b { + display: block; + width: 5px; + height: 5px; + border: 1px solid #fff; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.colorpicker-hue, +.colorpicker-alpha { + float: left; + width: 15px; + height: 100px; + margin-bottom: 4px; + margin-left: 4px; + cursor: row-resize; +} + +.colorpicker-hue i, +.colorpicker-alpha i { + position: absolute; + top: 0; + left: 0; + display: block; + width: 100%; + height: 1px; + margin-top: -1px; + background: #000; + border-top: 1px solid #fff; +} + +.colorpicker-hue { + background-image: url("../images/bootstrap-colorpicker/hue.png"); +} + +.colorpicker-alpha { + display: none; + background-image: url("../images/bootstrap-colorpicker/alpha.png"); +} + +.colorpicker-saturation, +.colorpicker-hue, +.colorpicker-alpha { + background-size: contain; +} + +.colorpicker { + top: 0; + left: 0; + z-index: 2500; + min-width: 130px; + padding: 4px; + margin-top: 1px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + *zoom: 1; +} + +.colorpicker:before, +.colorpicker:after { + display: table; + line-height: 0; + content: ""; +} + +.colorpicker:after { + clear: both; +} + +.colorpicker:before { + position: absolute; + top: -7px; + left: 6px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.colorpicker:after { + position: absolute; + top: -6px; + left: 7px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + border-left: 6px solid transparent; + content: ''; +} + +.colorpicker div { + position: relative; +} + +.colorpicker.colorpicker-with-alpha { + min-width: 140px; +} + +.colorpicker.colorpicker-with-alpha .colorpicker-alpha { + display: block; +} + +.colorpicker-color { + height: 10px; + margin-top: 5px; + clear: both; + background-image: url("../images/bootstrap-colorpicker/alpha.png"); + background-position: 0 100%; +} + +.colorpicker-color div { + height: 10px; +} + +.colorpicker-selectors { + display: none; + height: 10px; + margin-top: 5px; + clear: both; +} + +.colorpicker-selectors i { + float: left; + width: 10px; + height: 10px; + cursor: pointer; +} + +.colorpicker-selectors i + i { + margin-left: 3px; +} + +.colorpicker-element .input-group-addon i, +.colorpicker-element .add-on i { + display: inline-block; + width: 16px; + height: 16px; + vertical-align: text-top; + cursor: pointer; +} + +.colorpicker.colorpicker-inline { + position: relative; + z-index: auto; + display: inline-block; + float: none; +} + +.colorpicker.colorpicker-horizontal { + width: 110px; + height: auto; + min-width: 110px; +} + +.colorpicker.colorpicker-horizontal .colorpicker-saturation { + margin-bottom: 4px; +} + +.colorpicker.colorpicker-horizontal .colorpicker-color { + width: 100px; +} + +.colorpicker.colorpicker-horizontal .colorpicker-hue, +.colorpicker.colorpicker-horizontal .colorpicker-alpha { + float: left; + width: 100px; + height: 15px; + margin-bottom: 4px; + margin-left: 0; + cursor: col-resize; +} + +.colorpicker.colorpicker-horizontal .colorpicker-hue i, +.colorpicker.colorpicker-horizontal .colorpicker-alpha i { + position: absolute; + top: 0; + left: 0; + display: block; + width: 1px; + height: 15px; + margin-top: 0; + background: #ffffff; + border: none; +} + +.colorpicker.colorpicker-horizontal .colorpicker-hue { + background-image: url("../images/bootstrap-colorpicker/hue-horizontal.png"); +} + +.colorpicker.colorpicker-horizontal .colorpicker-alpha { + background-image: url("../images/bootstrap-colorpicker/alpha-horizontal.png"); +} + +.colorpicker.colorpicker-hidden { + display: none; +} + +.colorpicker.colorpicker-visible { + display: block; +} + +.colorpicker-inline.colorpicker-visible { + display: inline-block; +} + +.colorpicker-right:before { + right: 6px; + left: auto; +} + +.colorpicker-right:after { + right: 7px; + left: auto; +} \ No newline at end of file diff --git a/web/sass-files/sass/partials/_forms.scss b/web/sass-files/sass/partials/_forms.scss index c8b08f44d2..65ea161d45 100644 --- a/web/sass-files/sass/partials/_forms.scss +++ b/web/sass-files/sass/partials/_forms.scss @@ -18,6 +18,7 @@ .input__help { color: #777; margin: 10px 0 0 10px; + word-break: break-word; &.dark { color: #222; } diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index 702f0fd602..b5fcb61453 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -39,6 +39,7 @@ text-overflow: ellipsis; color: #888; margin-top: 2px; + max-height: 45px; } &.popover { white-space: normal; @@ -231,6 +232,10 @@ width: 45px; color: #999; cursor: pointer; + .fa { + margin-left: 3px; + font-size: 16px; + } } &.alt { margin: 0; diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss new file mode 100644 index 0000000000..c09e9d7b4f --- /dev/null +++ b/web/sass-files/sass/partials/_markdown.scss @@ -0,0 +1,28 @@ +.markdown__heading { + font-weight: bold; +} +.markdown__table { + background: #fff; + margin: 5px 0 10px; + th, td { + padding: 6px 13px; + border: 1px solid #ddd; + } + tbody tr { + background: #fff; + &:nth-child(2n) { + background-color: #f8f8f8; + } + } +} +pre { + border: none; + background-color: #f7f7f7; + margin: 5px 0; + .current--user & { + background: #fff; + } + code { + color: #c7254e; + } +} \ No newline at end of file diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index a046cd9041..38e9b41741 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -8,6 +8,16 @@ @include opacity(0.7); } } + a, a:focus, a:hover { + color: #2389D7; + } + .btn.btn-primary { + background: #4285f4; + &:hover, &:focus, &:active { + background: $primary-color--hover; + color: #fff; + } + } .info__label { font-weight: 600; text-align: right; diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 6940cf2fba..d4f02cf4be 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -275,7 +275,7 @@ body.ios { &.current--user { .post-body { @include border-radius(4px); - background: #f5f5f5; + background: rgba(#000, 0.05); } } &.post--comment { diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss index 9ae41ebb04..9abdd40da9 100644 --- a/web/sass-files/sass/partials/_search.scss +++ b/web/sass-files/sass/partials/_search.scss @@ -107,4 +107,5 @@ .search-highlight.theme, .search-highlight { background-color: #FFF2BB; + color: #333; } diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index 2b59a943ba..8dcd8f35c4 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -79,6 +79,36 @@ } } + .appearance-section { + .premade-themes { + .theme-label { + font-weight: 400; + margin-top: 5px; + } + img { + border: 3px solid transparent; + } + .active { + img { + border-color: $primary-color; + } + } + } + .custom-label { + font-weight: normal; + font-size: 13px; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 0; + } + .radio { + label { + font-weight: 600; + } + } + } + .section-title { margin-bottom: 5px; font-weight: 600; diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index 94583b153a..6a418e270c 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -16,6 +16,12 @@ overflow-y: auto; max-width: 200px; width: 200px; + a { + color: #262626 !important; + &:hover, &:focus { + background: #f5f5f5 !important; + } + } } .search__form { margin: 0; diff --git a/web/sass-files/sass/partials/_sidebar--right.scss b/web/sass-files/sass/partials/_sidebar--right.scss index d02a924489..b37dbf4216 100644 --- a/web/sass-files/sass/partials/_sidebar--right.scss +++ b/web/sass-files/sass/partials/_sidebar--right.scss @@ -51,9 +51,10 @@ margin: 11px 0 0 0; width: 22px; height: 22px; - background: url("../images/closeSidebar.png"); - @include background-size(100% 100%); opacity: 0.5; + font-size: 22px; + line-height: 0; + background: none; float: right; outline: none; border: none; @@ -61,11 +62,15 @@ &:hover, &:active { opacity: 0.8; } + i { + position: relative; + top: -2px; + } } .sidebar--right__header { font-size: 1em; text-transform: uppercase; - color: #444; + color: inherit; height: 44px; padding: 0 1em; line-height: 44px; diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss index de1db57e80..e704536a82 100644 --- a/web/sass-files/sass/styles.scss +++ b/web/sass-files/sass/styles.scss @@ -10,6 +10,7 @@ @import "partials/perfect-scrollbar"; @import "partials/font-awesome"; @import "partials/base"; +@import "partials/colorpicker"; // Channel Css @import "partials/headers"; @@ -36,6 +37,7 @@ @import "partials/error-bar"; @import "partials/loading"; @import "partials/get-link"; +@import "partials/markdown"; // Responsive Css @import "partials/responsive"; diff --git a/web/static/css/bootstrap-colorpicker.min.css b/web/static/css/bootstrap-colorpicker.min.css new file mode 100755 index 0000000000..5656e768fd --- /dev/null +++ b/web/static/css/bootstrap-colorpicker.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap Colorpicker + * http://mjolnic.github.io/bootstrap-colorpicker/ + * + * Originally written by (c) 2012 Stefan Petre + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + * + */.colorpicker-saturation{float:left;width:100px;height:100px;cursor:crosshair;background-image:url("../images/bootstrap-colorpicker/saturation.png")}.colorpicker-saturation i{position:absolute;top:0;left:0;display:block;width:5px;height:5px;margin:-4px 0 0 -4px;border:1px solid #000;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.colorpicker-saturation i b{display:block;width:5px;height:5px;border:1px solid #fff;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.colorpicker-hue,.colorpicker-alpha{float:left;width:15px;height:100px;margin-bottom:4px;margin-left:4px;cursor:row-resize}.colorpicker-hue i,.colorpicker-alpha i{position:absolute;top:0;left:0;display:block;width:100%;height:1px;margin-top:-1px;background:#000;border-top:1px solid #fff}.colorpicker-hue{background-image:url("../images/bootstrap-colorpicker/hue.png")}.colorpicker-alpha{display:none;background-image:url("../images/bootstrap-colorpicker/alpha.png")}.colorpicker-saturation,.colorpicker-hue,.colorpicker-alpha{background-size:contain}.colorpicker{top:0;left:0;z-index:2500;min-width:130px;padding:4px;margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1}.colorpicker:before,.colorpicker:after{display:table;line-height:0;content:""}.colorpicker:after{clear:both}.colorpicker:before{position:absolute;top:-7px;left:6px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.colorpicker:after{position:absolute;top:-6px;left:7px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.colorpicker div{position:relative}.colorpicker.colorpicker-with-alpha{min-width:140px}.colorpicker.colorpicker-with-alpha .colorpicker-alpha{display:block}.colorpicker-color{height:10px;margin-top:5px;clear:both;background-image:url("../images/bootstrap-colorpicker/alpha.png");background-position:0 100%}.colorpicker-color div{height:10px}.colorpicker-selectors{display:none;height:10px;margin-top:5px;clear:both}.colorpicker-selectors i{float:left;width:10px;height:10px;cursor:pointer}.colorpicker-selectors i+i{margin-left:3px}.colorpicker-element .input-group-addon i,.colorpicker-element .add-on i{display:inline-block;width:16px;height:16px;vertical-align:text-top;cursor:pointer}.colorpicker.colorpicker-inline{position:relative;z-index:auto;display:inline-block;float:none}.colorpicker.colorpicker-horizontal{width:110px;height:auto;min-width:110px}.colorpicker.colorpicker-horizontal .colorpicker-saturation{margin-bottom:4px}.colorpicker.colorpicker-horizontal .colorpicker-color{width:100px}.colorpicker.colorpicker-horizontal .colorpicker-hue,.colorpicker.colorpicker-horizontal .colorpicker-alpha{float:left;width:100px;height:15px;margin-bottom:4px;margin-left:0;cursor:col-resize}.colorpicker.colorpicker-horizontal .colorpicker-hue i,.colorpicker.colorpicker-horizontal .colorpicker-alpha i{position:absolute;top:0;left:0;display:block;width:1px;height:15px;margin-top:0;background:#fff;border:0}.colorpicker.colorpicker-horizontal .colorpicker-hue{background-image:url("../images/bootstrap-colorpicker/hue-horizontal.png")}.colorpicker.colorpicker-horizontal .colorpicker-alpha{background-image:url("../images/bootstrap-colorpicker/alpha-horizontal.png")}.colorpicker.colorpicker-hidden{display:none}.colorpicker.colorpicker-visible{display:block}.colorpicker-inline.colorpicker-visible{display:inline-block}.colorpicker-right:before{right:6px;left:auto}.colorpicker-right:after{right:7px;left:auto} diff --git a/web/static/images/bootstrap-colorpicker/alpha-horizontal.png b/web/static/images/bootstrap-colorpicker/alpha-horizontal.png new file mode 100755 index 0000000000..d0a65c08b0 Binary files /dev/null and b/web/static/images/bootstrap-colorpicker/alpha-horizontal.png differ diff --git a/web/static/images/bootstrap-colorpicker/alpha.png b/web/static/images/bootstrap-colorpicker/alpha.png new file mode 100755 index 0000000000..38043f1c85 Binary files /dev/null and b/web/static/images/bootstrap-colorpicker/alpha.png differ diff --git a/web/static/images/bootstrap-colorpicker/hue-horizontal.png b/web/static/images/bootstrap-colorpicker/hue-horizontal.png new file mode 100755 index 0000000000..a0d9add8e7 Binary files /dev/null and b/web/static/images/bootstrap-colorpicker/hue-horizontal.png differ diff --git a/web/static/images/bootstrap-colorpicker/hue.png b/web/static/images/bootstrap-colorpicker/hue.png new file mode 100755 index 0000000000..d89560e999 Binary files /dev/null and b/web/static/images/bootstrap-colorpicker/hue.png differ diff --git a/web/static/images/bootstrap-colorpicker/saturation.png b/web/static/images/bootstrap-colorpicker/saturation.png new file mode 100755 index 0000000000..594ae50ed7 Binary files /dev/null and b/web/static/images/bootstrap-colorpicker/saturation.png differ diff --git a/web/static/images/themes/dark.png b/web/static/images/themes/dark.png new file mode 100644 index 0000000000..832f64d2e9 Binary files /dev/null and b/web/static/images/themes/dark.png differ diff --git a/web/static/images/themes/mattermost.png b/web/static/images/themes/mattermost.png new file mode 100644 index 0000000000..4a321adcbf Binary files /dev/null and b/web/static/images/themes/mattermost.png differ diff --git a/web/static/images/themes/slack.png b/web/static/images/themes/slack.png new file mode 100644 index 0000000000..dc70c7dc21 Binary files /dev/null and b/web/static/images/themes/slack.png differ diff --git a/web/static/js/bootstrap-colorpicker.min.js b/web/static/js/bootstrap-colorpicker.min.js new file mode 100755 index 0000000000..05c0e07449 --- /dev/null +++ b/web/static/js/bootstrap-colorpicker.min.js @@ -0,0 +1 @@ +!function(a){"use strict";"object"==typeof exports?module.exports=a(window.jQuery):"function"==typeof define&&define.amd?define(["jquery"],a):window.jQuery&&!window.jQuery.fn.colorpicker&&a(window.jQuery)}(function(a){"use strict";var b=function(b,c){this.value={h:0,s:0,b:0,a:1},this.origFormat=null,c&&a.extend(this.colors,c),b&&(void 0!==b.toLowerCase?(b+="",this.setColor(b)):void 0!==b.h&&(this.value=b))};b.prototype={constructor:b,colors:{aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgrey:"#d3d3d3",lightgreen:"#90ee90",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370d8",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#d87093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32",transparent:"transparent"},_sanitizeNumber:function(a){return"number"==typeof a?a:isNaN(a)||null===a||""===a||void 0===a?1:void 0!==a.toLowerCase?parseFloat(a):1},isTransparent:function(a){return a?(a=a.toLowerCase().trim(),"transparent"===a||a.match(/#?00000000/)||a.match(/(rgba|hsla)\(0,0,0,0?\.?0\)/)):!1},rgbaIsTransparent:function(a){return 0===a.r&&0===a.g&&0===a.b&&0===a.a},setColor:function(a){a=a.toLowerCase().trim(),a&&(this.isTransparent(a)?this.value={h:0,s:0,b:0,a:0}:this.value=this.stringToHSB(a)||{h:0,s:0,b:0,a:1})},stringToHSB:function(b){b=b.toLowerCase();var c;"undefined"!=typeof this.colors[b]&&(b=this.colors[b],c="alias");var d=this,e=!1;return a.each(this.stringParsers,function(a,f){var g=f.re.exec(b),h=g&&f.parse.apply(d,[g]),i=c||f.format||"rgba";return h?(e=i.match(/hsla?/)?d.RGBtoHSB.apply(d,d.HSLtoRGB.apply(d,h)):d.RGBtoHSB.apply(d,h),d.origFormat=i,!1):!0}),e},setHue:function(a){this.value.h=1-a},setSaturation:function(a){this.value.s=a},setBrightness:function(a){this.value.b=1-a},setAlpha:function(a){this.value.a=parseInt(100*(1-a),10)/100},toRGB:function(a,b,c,d){a||(a=this.value.h,b=this.value.s,c=this.value.b),a*=360;var e,f,g,h,i;return a=a%360/60,i=c*b,h=i*(1-Math.abs(a%2-1)),e=f=g=c-i,a=~~a,e+=[i,h,0,0,h,i][a],f+=[h,i,i,h,0,0][a],g+=[0,0,h,i,i,h][a],{r:Math.round(255*e),g:Math.round(255*f),b:Math.round(255*g),a:d||this.value.a}},toHex:function(a,b,c,d){var e=this.toRGB(a,b,c,d);return this.rgbaIsTransparent(e)?"transparent":"#"+(1<<24|parseInt(e.r)<<16|parseInt(e.g)<<8|parseInt(e.b)).toString(16).substr(1)},toHSL:function(a,b,c,d){a=a||this.value.h,b=b||this.value.s,c=c||this.value.b,d=d||this.value.a;var e=a,f=(2-b)*c,g=b*c;return g/=f>0&&1>=f?f:2-f,f/=2,g>1&&(g=1),{h:isNaN(e)?0:e,s:isNaN(g)?0:g,l:isNaN(f)?0:f,a:isNaN(d)?0:d}},toAlias:function(a,b,c,d){var e=this.toHex(a,b,c,d);for(var f in this.colors)if(this.colors[f]===e)return f;return!1},RGBtoHSB:function(a,b,c,d){a/=255,b/=255,c/=255;var e,f,g,h;return g=Math.max(a,b,c),h=g-Math.min(a,b,c),e=0===h?null:g===a?(b-c)/h:g===b?(c-a)/h+2:(a-b)/h+4,e=(e+360)%6*60/360,f=0===h?0:h/g,{h:this._sanitizeNumber(e),s:f,b:g,a:this._sanitizeNumber(d)}},HueToRGB:function(a,b,c){return 0>c?c+=1:c>1&&(c-=1),1>6*c?a+(b-a)*c*6:1>2*c?b:2>3*c?a+(b-a)*(2/3-c)*6:a},HSLtoRGB:function(a,b,c,d){0>b&&(b=0);var e;e=.5>=c?c*(1+b):c+b-c*b;var f=2*c-e,g=a+1/3,h=a,i=a-1/3,j=Math.round(255*this.HueToRGB(f,e,g)),k=Math.round(255*this.HueToRGB(f,e,h)),l=Math.round(255*this.HueToRGB(f,e,i));return[j,k,l,this._sanitizeNumber(d)]},toString:function(a){a=a||"rgba";var b=!1;switch(a){case"rgb":return b=this.toRGB(),this.rgbaIsTransparent(b)?"transparent":"rgb("+b.r+","+b.g+","+b.b+")";case"rgba":return b=this.toRGB(),"rgba("+b.r+","+b.g+","+b.b+","+b.a+")";case"hsl":return b=this.toHSL(),"hsl("+Math.round(360*b.h)+","+Math.round(100*b.s)+"%,"+Math.round(100*b.l)+"%)";case"hsla":return b=this.toHSL(),"hsla("+Math.round(360*b.h)+","+Math.round(100*b.s)+"%,"+Math.round(100*b.l)+"%,"+b.a+")";case"hex":return this.toHex();case"alias":return this.toAlias()||this.toHex();default:return b}},stringParsers:[{re:/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*?\)/,format:"rgb",parse:function(a){return[a[1],a[2],a[3],1]}},{re:/rgb\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*?\)/,format:"rgb",parse:function(a){return[2.55*a[1],2.55*a[2],2.55*a[3],1]}},{re:/rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,format:"rgba",parse:function(a){return[a[1],a[2],a[3],a[4]]}},{re:/rgba\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,format:"rgba",parse:function(a){return[2.55*a[1],2.55*a[2],2.55*a[3],a[4]]}},{re:/hsl\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*?\)/,format:"hsl",parse:function(a){return[a[1]/360,a[2]/100,a[3]/100,a[4]]}},{re:/hsla\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,format:"hsla",parse:function(a){return[a[1]/360,a[2]/100,a[3]/100,a[4]]}},{re:/#?([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,format:"hex",parse:function(a){return[parseInt(a[1],16),parseInt(a[2],16),parseInt(a[3],16),1]}},{re:/#?([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/,format:"hex",parse:function(a){return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16),1]}}],colorNameToHex:function(a){return"undefined"!=typeof this.colors[a.toLowerCase()]?this.colors[a.toLowerCase()]:!1}};var c={horizontal:!1,inline:!1,color:!1,format:!1,input:"input",container:!1,component:".add-on, .input-group-addon",sliders:{saturation:{maxLeft:100,maxTop:100,callLeft:"setSaturation",callTop:"setBrightness"},hue:{maxLeft:0,maxTop:100,callLeft:!1,callTop:"setHue"},alpha:{maxLeft:0,maxTop:100,callLeft:!1,callTop:"setAlpha"}},slidersHorz:{saturation:{maxLeft:100,maxTop:100,callLeft:"setSaturation",callTop:"setBrightness"},hue:{maxLeft:100,maxTop:0,callLeft:"setHue",callTop:!1},alpha:{maxLeft:100,maxTop:0,callLeft:"setAlpha",callTop:!1}},template:'