Fixing merge
@@ -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).
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
64
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
15
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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -33,11 +33,11 @@
|
||||
"FileFormat": "",
|
||||
"FileLocation": ""
|
||||
},
|
||||
"ImageSettings": {
|
||||
"FileSettings": {
|
||||
"DriverName": "local",
|
||||
"Directory": "./data/",
|
||||
"EnablePublicLink": true,
|
||||
"PublicLinkSalt": "LhaAWC6lYEKHTkBKsvyXNIOfUIT37AXe",
|
||||
"PublicLinkSalt": "A705AklYF8MFDOfcwh3I488G8vtLlVip",
|
||||
"ThumbnailWidth": 120,
|
||||
"ThumbnailHeight": 100,
|
||||
"PreviewWidth": 1024,
|
||||
|
||||
@@ -33,25 +33,6 @@ git checkout -b <branch name>
|
||||
|
||||
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.
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"FileFormat": "",
|
||||
"FileLocation": ""
|
||||
},
|
||||
"ImageSettings": {
|
||||
"FileSettings": {
|
||||
"DriverName": "local",
|
||||
"Directory": "/mattermost/data/",
|
||||
"EnablePublicLink": true,
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"FileFormat": "",
|
||||
"FileLocation": ""
|
||||
},
|
||||
"ImageSettings": {
|
||||
"FileSettings": {
|
||||
"DriverName": "local",
|
||||
"Directory": "/mattermost/data/",
|
||||
"EnablePublicLink": true,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = <LogsTab />;
|
||||
} else if (this.state.selected === 'image_settings') {
|
||||
tab = <ImageSettingsTab config={this.state.config} />;
|
||||
tab = <FileSettingsTab config={this.state.config} />;
|
||||
} else if (this.state.selected === 'privacy_settings') {
|
||||
tab = <PrivacySettingsTab config={this.state.config} />;
|
||||
} else if (this.state.selected === 'rate_settings') {
|
||||
|
||||
@@ -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'}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -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 (
|
||||
<div className='wrapper--fixed'>
|
||||
<h3>{'Image Settings'}</h3>
|
||||
<h3>{'File Settings'}</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
@@ -161,7 +161,7 @@ export default class ImageSettings extends React.Component {
|
||||
className='form-control'
|
||||
id='DriverName'
|
||||
ref='DriverName'
|
||||
defaultValue={this.props.config.ImageSettings.DriverName}
|
||||
defaultValue={this.props.config.FileSettings.DriverName}
|
||||
onChange={this.handleChange.bind(this, 'DriverName')}
|
||||
>
|
||||
<option value=''>{'Disable File Storage'}</option>
|
||||
@@ -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}
|
||||
/>
|
||||
<p className='help-text'>{'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.'}</p>
|
||||
@@ -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}
|
||||
/>
|
||||
<p className='help-text'>{'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.'}</p>
|
||||
@@ -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}
|
||||
/>
|
||||
<p className='help-text'>{'Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'}</p>
|
||||
@@ -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}
|
||||
/>
|
||||
<p className='help-text'>{'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.'}</p>
|
||||
@@ -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}
|
||||
/>
|
||||
<p className='help-text'>{'Width of profile picture.'}</p>
|
||||
@@ -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}
|
||||
/>
|
||||
<p className='help-text'>{'Height of profile picture.'}</p>
|
||||
@@ -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}
|
||||
/>
|
||||
<p className='help-text'>{'32-character salt added to signing of public image links.'}</p>
|
||||
@@ -491,6 +491,6 @@ export default class ImageSettings extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
ImageSettings.propTypes = {
|
||||
FileSettings.propTypes = {
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = <p>Your email has been verified! Click <a href={this.props.teamURL + '?email=' + this.props.userEmail}>here</a> to log in.</p>;
|
||||
@@ -30,6 +32,9 @@ export default class EmailVerify extends React.Component {
|
||||
Resend Email
|
||||
</button>
|
||||
);
|
||||
if (this.props.resendSuccess) {
|
||||
resendConfirm = <div><br /><p className='alert alert-success'><i className='fa fa-check'></i>{' Verification email sent.'}</p></div>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -41,6 +46,7 @@ export default class EmailVerify extends React.Component {
|
||||
<div className='panel-body'>
|
||||
{body}
|
||||
{resend}
|
||||
{resendConfirm}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -114,7 +114,7 @@ export default class MoreDirectChannels extends React.Component {
|
||||
<span aria-hidden='true'>×</span>
|
||||
<span className='sr-only'>Close</span>
|
||||
</button>
|
||||
<h4 className='modal-title'>More Private Messages</h4>
|
||||
<h4 className='modal-title'>More Direct Messages</h4>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<ul className='nav nav-pills nav-stacked'>
|
||||
|
||||
@@ -93,6 +93,7 @@ export default class NewChannelModal extends React.Component {
|
||||
<span>
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
bsSize='large'
|
||||
onHide={this.props.onModalDismissed}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
@@ -122,7 +123,7 @@ export default class NewChannelModal extends React.Component {
|
||||
/>
|
||||
{displayNameError}
|
||||
<p className='input__help dark'>
|
||||
{'Channel URL: ' + prettyTeamURL + this.props.channelData.name + ' ('}
|
||||
{'URL: ' + prettyTeamURL + this.props.channelData.name + ' ('}
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.props.onChangeURLPressed}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
const Utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
|
||||
export default class PasswordResetSendLink extends React.Component {
|
||||
@@ -15,8 +16,8 @@ export default class PasswordResetSendLink extends React.Component {
|
||||
e.preventDefault();
|
||||
var state = {};
|
||||
|
||||
var email = React.findDOMNode(this.refs.email).value.trim();
|
||||
if (!email) {
|
||||
var email = React.findDOMNode(this.refs.email).value.trim().toLowerCase();
|
||||
if (!email || !Utils.isEmail(email)) {
|
||||
state.error = 'Please enter a valid email address.';
|
||||
this.setState(state);
|
||||
return;
|
||||
@@ -67,7 +68,7 @@ export default class PasswordResetSendLink extends React.Component {
|
||||
<p>{'To reset your password, enter the email address you used to sign up for ' + this.props.teamDisplayName + '.'}</p>
|
||||
<div className={formClass}>
|
||||
<input
|
||||
type='text'
|
||||
type='email'
|
||||
className='form-control'
|
||||
name='email'
|
||||
ref='email'
|
||||
|
||||
@@ -65,7 +65,7 @@ export default class PopoverListMembers extends React.Component {
|
||||
>
|
||||
{count}
|
||||
<span
|
||||
className='glyphicon glyphicon-user'
|
||||
className='fa fa-user'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ export default class Post extends React.Component {
|
||||
|
||||
var post = this.props.post;
|
||||
client.createPost(post, post.channel_id,
|
||||
function success(data) {
|
||||
(data) => {
|
||||
AsyncClient.getPosts();
|
||||
|
||||
var channel = ChannelStore.get(post.channel_id);
|
||||
@@ -65,11 +65,11 @@ export default class Post extends React.Component {
|
||||
post: data
|
||||
});
|
||||
},
|
||||
function error() {
|
||||
() => {
|
||||
post.state = Constants.POST_FAILED;
|
||||
PostStore.updatePendingPost(post);
|
||||
this.forceUpdate();
|
||||
}.bind(this)
|
||||
}
|
||||
);
|
||||
|
||||
post.state = Constants.POST_LOADING;
|
||||
@@ -81,8 +81,40 @@ export default class Post extends React.Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.sameRoot !== this.props.sameRoot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.sameUser !== this.props.sameUser) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.getCommentCount(nextProps) !== this.getCommentCount(this.props)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
getCommentCount(props) {
|
||||
const post = props.post;
|
||||
const parentPost = props.parentPost;
|
||||
const posts = props.posts;
|
||||
|
||||
let commentCount = 0;
|
||||
let commentRootId;
|
||||
if (parentPost) {
|
||||
commentRootId = post.root_id;
|
||||
} else {
|
||||
commentRootId = post.id;
|
||||
}
|
||||
for (let postId in posts) {
|
||||
if (posts[postId].root_id === commentRootId) {
|
||||
commentCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return commentCount;
|
||||
}
|
||||
render() {
|
||||
var post = this.props.post;
|
||||
var parentPost = this.props.parentPost;
|
||||
@@ -93,18 +125,7 @@ export default class Post extends React.Component {
|
||||
type = 'Comment';
|
||||
}
|
||||
|
||||
var commentCount = 0;
|
||||
var commentRootId;
|
||||
if (parentPost) {
|
||||
commentRootId = post.root_id;
|
||||
} else {
|
||||
commentRootId = post.id;
|
||||
}
|
||||
for (var postId in posts) {
|
||||
if (posts[postId].root_id === commentRootId) {
|
||||
commentCount += 1;
|
||||
}
|
||||
}
|
||||
const commentCount = this.getCommentCount(this.props);
|
||||
|
||||
var rootUser;
|
||||
if (this.props.sameRoot) {
|
||||
|
||||
@@ -35,7 +35,6 @@ export default class PostBody extends React.Component {
|
||||
|
||||
parseEmojis() {
|
||||
twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE});
|
||||
global.window.emojify.run(React.findDOMNode(this.refs.message_span));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
@@ -326,8 +326,8 @@ export default class PostList extends React.Component {
|
||||
<strong><UserProfile userId={teammate.id} /></strong>
|
||||
</div>
|
||||
<p className='channel-intro-text'>
|
||||
{'This is the start of your private message history with ' + teammateName + '.'}<br/>
|
||||
{'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 + '.'}<br/>
|
||||
{'Direct messages and files shared here are not shown to people outside this area.'}
|
||||
</p>
|
||||
<a
|
||||
className='intro-links'
|
||||
@@ -346,7 +346,7 @@ export default class PostList extends React.Component {
|
||||
|
||||
return (
|
||||
<div className='channel-intro'>
|
||||
<p className='channel-intro-text'>{'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.'}</p>
|
||||
<p className='channel-intro-text'>{'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.'}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export default class PostListContainer extends React.Component {
|
||||
for (let i = 0; i <= this.state.postLists.length - 1; i++) {
|
||||
postListCtls.push(
|
||||
<PostList
|
||||
key={'postlistkey' + i}
|
||||
channelId={postLists[i]}
|
||||
isActive={postLists[i] === channelId}
|
||||
/>
|
||||
|
||||
@@ -228,7 +228,7 @@ export default class RegisterAppModal extends React.Component {
|
||||
data-dismiss='modal'
|
||||
aria-label='Close'
|
||||
>
|
||||
<span aria-hidden='true'>{'x'}</span>
|
||||
<span aria-hidden='true'>{'×'}</span>
|
||||
</button>
|
||||
<h4
|
||||
className='modal-title'
|
||||
|
||||
@@ -56,7 +56,6 @@ export default class RhsComment 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();
|
||||
@@ -114,14 +113,7 @@ export default class RhsComment extends React.Component {
|
||||
var ownerOptions;
|
||||
if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) {
|
||||
ownerOptions = (
|
||||
<div
|
||||
className='dropdown'
|
||||
onClick={
|
||||
function scroll() {
|
||||
$('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time').scrollTop() + 50);
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className='dropdown'>
|
||||
<a
|
||||
href='#'
|
||||
className='dropdown-toggle theme'
|
||||
|
||||
@@ -65,6 +65,7 @@ export default class RhsHeaderPost extends React.Component {
|
||||
aria-label='Close'
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
<i className='fa fa-sign-out'/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export default class SearchResultsHeader extends React.Component {
|
||||
title='Close'
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
<i className='fa fa-sign-out'/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -566,7 +566,7 @@ export default class Sidebar extends React.Component {
|
||||
{privateChannelItems}
|
||||
</ul>
|
||||
<ul className='nav nav-pills nav-stacked'>
|
||||
<li><h4>Private Messages</h4></li>
|
||||
<li><h4>Direct Messages</h4></li>
|
||||
{directMessageItems}
|
||||
{directMessageMore}
|
||||
</ul>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: ''});
|
||||
|
||||
108
web/react/components/user_settings/custom_theme_chooser.jsx
Normal file
@@ -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(
|
||||
<div className='col-sm-4 form-group'>
|
||||
<label className='custom-label'>{element.uiName}</label>
|
||||
<div
|
||||
className='input-group color-picker'
|
||||
id={element.id}
|
||||
>
|
||||
<input
|
||||
className='form-control'
|
||||
type='text'
|
||||
defaultValue={theme[element.id]}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
<span className='input-group-addon'><i></i></span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
colors += theme[element.id] + ',';
|
||||
});
|
||||
|
||||
const pasteBox = (
|
||||
<div className='col-sm-12'>
|
||||
<label className='custom-label'>
|
||||
{'Copy and paste to share theme colors:'}
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
value={colors}
|
||||
onChange={this.pasteBoxChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='row form-group'>
|
||||
{elements}
|
||||
</div>
|
||||
<div className='row'>
|
||||
{pasteBox}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CustomThemeChooser.propTypes = {
|
||||
theme: React.PropTypes.object.isRequired,
|
||||
updateTheme: React.PropTypes.func.isRequired
|
||||
};
|
||||
179
web/react/components/user_settings/import_theme_modal.jsx
Normal file
@@ -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 (
|
||||
<span>
|
||||
<Modal
|
||||
show={this.state.show}
|
||||
onHide={() => this.setState({show: false})}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>{'Import Slack Theme'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<form
|
||||
role='form'
|
||||
className='form-horizontal'
|
||||
>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
{'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:'}
|
||||
</p>
|
||||
<div className='form-group less'>
|
||||
<div className='col-sm-9'>
|
||||
<input
|
||||
ref='input'
|
||||
type='text'
|
||||
className='form-control'
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
{this.state.inputError}
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={() => this.setState({show: false})}
|
||||
>
|
||||
{'Cancel'}
|
||||
</button>
|
||||
<button
|
||||
onClick={this.handleSubmit}
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
tabIndex='3'
|
||||
>
|
||||
{'Submit'}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
55
web/react/components/user_settings/premade_theme_chooser.jsx
Normal file
@@ -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(
|
||||
<div className='col-sm-3 premade-themes'>
|
||||
<div
|
||||
className={activeClass}
|
||||
onClick={() => this.props.updateTheme(premadeTheme)}
|
||||
>
|
||||
<label>
|
||||
<img
|
||||
className='img-responsive'
|
||||
src={'/static/images/themes/' + premadeTheme.type.toLowerCase() + '.png'}
|
||||
/>
|
||||
<div className='theme-label'>{Utils.toTitleCase(premadeTheme.type)}</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='row'>
|
||||
{premadeThemes}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PremadeThemeChooser.propTypes = {
|
||||
theme: React.PropTypes.object.isRequired,
|
||||
updateTheme: React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -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(
|
||||
<button
|
||||
key={ThemeColors[i] + 'key' + i}
|
||||
ref={ThemeColors[i]}
|
||||
type='button'
|
||||
className='btn btn-lg color-btn'
|
||||
style={{backgroundColor: ThemeColors[i]}}
|
||||
onClick={this.updateTheme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<li
|
||||
key='themeColorSetting'
|
||||
className='setting-list-item'
|
||||
>
|
||||
<div
|
||||
className='btn-group'
|
||||
data-toggle='buttons-radio'
|
||||
>
|
||||
{themeButtons}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
themeSection = (
|
||||
<SettingItemMax
|
||||
title='Theme Color'
|
||||
inputs={inputs}
|
||||
submit={this.submitTheme}
|
||||
serverError={serverError}
|
||||
updateSection={function updateSection(e) {
|
||||
self.props.updateSection('');
|
||||
e.preventDefault();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
themeSection = (
|
||||
<SettingItemMin
|
||||
title='Theme Color'
|
||||
describe={this.state.theme}
|
||||
updateSection={function updateSection() {
|
||||
self.props.updateSection('theme');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
let custom;
|
||||
let premade;
|
||||
if (displayCustom) {
|
||||
custom = (
|
||||
<CustomThemeChooser
|
||||
theme={this.state.theme}
|
||||
updateTheme={this.updateTheme}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
premade = (
|
||||
<PremadeThemeChooser
|
||||
theme={this.state.theme}
|
||||
updateTheme={this.updateTheme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const themeUI = (
|
||||
<div className='section-max appearance-section'>
|
||||
<div className='col-sm-12'>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input type='radio'
|
||||
checked={!displayCustom}
|
||||
onChange={this.updateType.bind(this, 'premade')}
|
||||
>
|
||||
{'Theme Colors'}
|
||||
</input>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
{premade}
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input type='radio'
|
||||
checked={displayCustom}
|
||||
onChange={this.updateType.bind(this, 'custom')}
|
||||
>
|
||||
{'Custom Theme'}
|
||||
</input>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
{custom}
|
||||
<hr />
|
||||
{serverError}
|
||||
<a
|
||||
className='btn btn-sm btn-primary'
|
||||
href='#'
|
||||
onClick={this.submitTheme}
|
||||
>
|
||||
{'Submit'}
|
||||
</a>
|
||||
<a
|
||||
className='btn btn-sm theme'
|
||||
href='#'
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
{'Cancel'}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='modal-header'>
|
||||
@@ -151,21 +198,28 @@ export default class UserSettingsAppearance extends React.Component {
|
||||
data-dismiss='modal'
|
||||
aria-label='Close'
|
||||
>
|
||||
<span aria-hidden='true'>×</span>
|
||||
<span aria-hidden='true'>{'×'}</span>
|
||||
</button>
|
||||
<h4
|
||||
className='modal-title'
|
||||
ref='title'
|
||||
>
|
||||
<i className='modal-back'></i>Appearance Settings
|
||||
<i className='modal-back'></i>{'Appearance Settings'}
|
||||
</h4>
|
||||
</div>
|
||||
<div className='user-settings'>
|
||||
<h3 className='tab-header'>Appearance Settings</h3>
|
||||
<h3 className='tab-header'>{'Appearance Settings'}</h3>
|
||||
<div className='divider-dark first'/>
|
||||
{themeSection}
|
||||
{themeUI}
|
||||
<div className='divider-dark'/>
|
||||
</div>
|
||||
<br/>
|
||||
<a
|
||||
className='theme'
|
||||
onClick={this.handleImportModal}
|
||||
>
|
||||
{'Import from Slack'}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -176,6 +230,5 @@ UserSettingsAppearance.defaultProps = {
|
||||
};
|
||||
UserSettingsAppearance.propTypes = {
|
||||
activeSection: React.PropTypes.string,
|
||||
updateSection: React.PropTypes.func,
|
||||
updateTab: React.PropTypes.func
|
||||
};
|
||||
|
||||
@@ -64,7 +64,7 @@ export default class DeveloperTab extends React.Component {
|
||||
data-dismiss='modal'
|
||||
aria-label='Close'
|
||||
>
|
||||
<span aria-hidden='true'>{'x'}</span>
|
||||
<span aria-hidden='true'>{'×'}</span>
|
||||
</button>
|
||||
<h4
|
||||
className='modal-title'
|
||||
|
||||
@@ -60,7 +60,7 @@ export default class UserSettingsModal extends React.Component {
|
||||
data-dismiss='modal'
|
||||
aria-label='Close'
|
||||
>
|
||||
<span aria-hidden='true'>{'x'}</span>
|
||||
<span aria-hidden='true'>{'×'}</span>
|
||||
</button>
|
||||
<h4
|
||||
className='modal-title'
|
||||
|
||||
@@ -241,7 +241,7 @@ export default class NotificationsTab extends React.Component {
|
||||
checked={notifyActive[1]}
|
||||
onChange={this.handleNotifyRadio.bind(this, 'mention')}
|
||||
>
|
||||
Only for mentions and private messages
|
||||
Only for mentions and direct messages
|
||||
</input>
|
||||
</label>
|
||||
<br/>
|
||||
@@ -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 {
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
<div><br/>{'Email notifications are sent for mentions and private messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}</div>
|
||||
<div><br/>{'Email notifications are sent for mentions and direct messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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(
|
||||
<ImportThemeModal />,
|
||||
document.getElementById('import_theme_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<TeamSettingsModal teamDisplayName={props.TeamDisplayName} />,
|
||||
document.getElementById('team_settings_modal')
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
|
||||
@@ -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: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>",
|
||||
OFFLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path fill='#cccccc' d='M6.002,7.143C5.645,7.363,5.167,7.52,4.502,7.52c-2.493,0-2.5-2.02-2.5-2.02S1.029,5.607,0.775,6.004C0.41,6.577,0.15,7.716,0.049,8.545c-0.025,0.145-0.057,0.537-0.05,0.598c0.162,1.295,2.237,2.321,4.375,2.357c0.043,0.001,0.085,0.001,0.127,0.001c0.043,0,0.084,0,0.127-0.001c1.879-0.023,3.793-0.879,4.263-2h-2.89L6.002,7.143L6.002,7.143z M4.501,5.488c1.372,0,2.483-1.117,2.483-2.494c0-1.378-1.111-2.495-2.483-2.495c-1.371,0-2.481,1.117-2.481,2.495C2.02,4.371,3.13,5.488,4.501,5.488z M7.002,6.5v2h5v-2H7.002z'/></g></g></svg>",
|
||||
MENU_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>",
|
||||
COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>"
|
||||
COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>",
|
||||
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'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
159
web/react/utils/emoticons.jsx
Normal file
@@ -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, // </3
|
||||
thumbsup: /:\+1:/g, // :+1:
|
||||
thumbsdown: /:\-1:/g // :-1:
|
||||
};
|
||||
|
||||
function initializeEmoticonMap() {
|
||||
const emoticonNames =
|
||||
('+1,-1,100,1234,8ball,a,ab,abc,abcd,accept,aerial_tramway,airplane,alarm_clock,alien,ambulance,anchor,angel,' +
|
||||
'anger,angry,anguished,ant,apple,aquarius,aries,arrow_backward,arrow_double_down,arrow_double_up,arrow_down,' +
|
||||
'arrow_down_small,arrow_forward,arrow_heading_down,arrow_heading_up,arrow_left,arrow_lower_left,' +
|
||||
'arrow_lower_right,arrow_right,arrow_right_hook,arrow_up,arrow_up_down,arrow_up_small,arrow_upper_left,' +
|
||||
'arrow_upper_right,arrows_clockwise,arrows_counterclockwise,art,articulated_lorry,astonished,atm,b,baby,' +
|
||||
'baby_bottle,baby_chick,baby_symbol,back,baggage_claim,balloon,ballot_box_with_check,bamboo,banana,bangbang,' +
|
||||
'bank,bar_chart,barber,baseball,basketball,bath,bathtub,battery,bear,bee,beer,beers,beetle,beginner,bell,bento,' +
|
||||
'bicyclist,bike,bikini,bird,birthday,black_circle,black_joker,black_medium_small_square,black_medium_square,' +
|
||||
'black_nib,black_small_square,black_square,black_square_button,blossom,blowfish,blue_book,blue_car,blue_heart,' +
|
||||
'blush,boar,boat,bomb,book,bookmark,bookmark_tabs,books,boom,boot,bouquet,bow,bowling,bowtie,boy,bread,' +
|
||||
'bride_with_veil,bridge_at_night,briefcase,broken_heart,bug,bulb,bullettrain_front,bullettrain_side,bus,busstop,' +
|
||||
'bust_in_silhouette,busts_in_silhouette,cactus,cake,calendar,calling,camel,camera,cancer,candy,capital_abcd,' +
|
||||
'capricorn,car,card_index,carousel_horse,cat,cat2,cd,chart,chart_with_downwards_trend,chart_with_upwards_trend,' +
|
||||
'checkered_flag,cherries,cherry_blossom,chestnut,chicken,children_crossing,chocolate_bar,christmas_tree,church,' +
|
||||
'cinema,circus_tent,city_sunrise,city_sunset,cl,clap,clapper,clipboard,clock1,clock10,clock1030,clock11,' +
|
||||
'clock1130,clock12,clock1230,clock130,clock2,clock230,clock3,clock330,clock4,clock430,clock5,clock530,clock6,' +
|
||||
'clock630,clock7,clock730,clock8,clock830,clock9,clock930,closed_book,closed_lock_with_key,closed_umbrella,cloud,' +
|
||||
'clubs,cn,cocktail,coffee,cold_sweat,collision,computer,confetti_ball,confounded,confused,congratulations,' +
|
||||
'construction,construction_worker,convenience_store,cookie,cool,cop,copyright,corn,couple,couple_with_heart,' +
|
||||
'couplekiss,cow,cow2,credit_card,crescent_moon,crocodile,crossed_flags,crown,cry,crying_cat_face,crystal_ball,' +
|
||||
'cupid,curly_loop,currency_exchange,curry,custard,customs,cyclone,dancer,dancers,dango,dart,dash,date,de,' +
|
||||
'deciduous_tree,department_store,diamond_shape_with_a_dot_inside,diamonds,disappointed,disappointed_relieved,' +
|
||||
'dizzy,dizzy_face,do_not_litter,dog,dog2,dollar,dolls,dolphin,donut,door,doughnut,dragon,dragon_face,dress,' +
|
||||
'dromedary_camel,droplet,dvd,e-mail,ear,ear_of_rice,earth_africa,earth_americas,earth_asia,egg,eggplant,eight,' +
|
||||
'eight_pointed_black_star,eight_spoked_asterisk,electric_plug,elephant,email,end,envelope,es,euro,' +
|
||||
'european_castle,european_post_office,evergreen_tree,exclamation,expressionless,eyeglasses,eyes,facepunch,' +
|
||||
'factory,fallen_leaf,family,fast_forward,fax,fearful,feelsgood,feet,ferris_wheel,file_folder,finnadie,fire,' +
|
||||
'fire_engine,fireworks,first_quarter_moon,first_quarter_moon_with_face,fish,fish_cake,fishing_pole_and_fish,fist,' +
|
||||
'five,flags,flashlight,floppy_disk,flower_playing_cards,flushed,foggy,football,fork_and_knife,fountain,four,' +
|
||||
'four_leaf_clover,fr,free,fried_shrimp,fries,frog,frowning,fu,fuelpump,full_moon,full_moon_with_face,game_die,gb,' +
|
||||
'gem,gemini,ghost,gift,gift_heart,girl,globe_with_meridians,goat,goberserk,godmode,golf,grapes,green_apple,' +
|
||||
'green_book,green_heart,grey_exclamation,grey_question,grimacing,grin,grinning,guardsman,guitar,gun,haircut,' +
|
||||
'hamburger,hammer,hamster,hand,handbag,hankey,hash,hatched_chick,hatching_chick,headphones,hear_no_evil,heart,' +
|
||||
'heart_decoration,heart_eyes,heart_eyes_cat,heartbeat,heartpulse,hearts,heavy_check_mark,heavy_division_sign,' +
|
||||
'heavy_dollar_sign,heavy_exclamation_mark,heavy_minus_sign,heavy_multiplication_x,heavy_plus_sign,helicopter,' +
|
||||
'herb,hibiscus,high_brightness,high_heel,hocho,honey_pot,honeybee,horse,horse_racing,hospital,hotel,hotsprings,' +
|
||||
'hourglass,hourglass_flowing_sand,house,house_with_garden,hurtrealbad,hushed,ice_cream,icecream,id,' +
|
||||
'ideograph_advantage,imp,inbox_tray,incoming_envelope,information_desk_person,information_source,innocent,' +
|
||||
'interrobang,iphone,it,izakaya_lantern,jack_o_lantern,japan,japanese_castle,japanese_goblin,japanese_ogre,jeans,' +
|
||||
'joy,joy_cat,jp,key,keycap_ten,kimono,kiss,kissing,kissing_cat,kissing_closed_eyes,kissing_face,kissing_heart,' +
|
||||
'kissing_smiling_eyes,koala,koko,kr,large_blue_circle,large_blue_diamond,large_orange_diamond,last_quarter_moon,' +
|
||||
'last_quarter_moon_with_face,laughing,leaves,ledger,left_luggage,left_right_arrow,leftwards_arrow_with_hook,' +
|
||||
'lemon,leo,leopard,libra,light_rail,link,lips,lipstick,lock,lock_with_ink_pen,lollipop,loop,loudspeaker,' +
|
||||
'love_hotel,love_letter,low_brightness,m,mag,mag_right,mahjong,mailbox,mailbox_closed,mailbox_with_mail,' +
|
||||
'mailbox_with_no_mail,man,man_with_gua_pi_mao,man_with_turban,mans_shoe,maple_leaf,mask,massage,meat_on_bone,' +
|
||||
'mega,melon,memo,mens,metal,metro,microphone,microscope,milky_way,minibus,minidisc,mobile_phone_off,' +
|
||||
'money_with_wings,moneybag,monkey,monkey_face,monorail,mortar_board,mount_fuji,mountain_bicyclist,' +
|
||||
'mountain_cableway,mountain_railway,mouse,mouse2,movie_camera,moyai,muscle,mushroom,musical_keyboard,' +
|
||||
'musical_note,musical_score,mute,nail_care,name_badge,neckbeard,necktie,negative_squared_cross_mark,' +
|
||||
'neutral_face,new,new_moon,new_moon_with_face,newspaper,ng,nine,no_bell,no_bicycles,no_entry,no_entry_sign,' +
|
||||
'no_good,no_mobile_phones,no_mouth,no_pedestrians,no_smoking,non-potable_water,nose,notebook,' +
|
||||
'notebook_with_decorative_cover,notes,nut_and_bolt,o,o2,ocean,octocat,octopus,oden,office,ok,ok_hand,' +
|
||||
'ok_woman,older_man,older_woman,on,oncoming_automobile,oncoming_bus,oncoming_police_car,oncoming_taxi,one,' +
|
||||
'open_file_folder,open_hands,open_mouth,ophiuchus,orange_book,outbox_tray,ox,package,page_facing_up,' +
|
||||
'page_with_curl,pager,palm_tree,panda_face,paperclip,parking,part_alternation_mark,partly_sunny,' +
|
||||
'passport_control,paw_prints,peach,pear,pencil,pencil2,penguin,pensive,performing_arts,persevere,' +
|
||||
'person_frowning,person_with_blond_hair,person_with_pouting_face,phone,pig,pig2,pig_nose,pill,pineapple,pisces,' +
|
||||
'pizza,plus1,point_down,point_left,point_right,point_up,point_up_2,police_car,poodle,poop,post_office,' +
|
||||
'postal_horn,postbox,potable_water,pouch,poultry_leg,pound,pouting_cat,pray,princess,punch,purple_heart,purse,' +
|
||||
'pushpin,put_litter_in_its_place,question,rabbit,rabbit2,racehorse,radio,radio_button,rage,rage1,rage2,rage3,' +
|
||||
'rage4,railway_car,rainbow,raised_hand,raised_hands,raising_hand,ram,ramen,rat,recycle,red_car,red_circle,' +
|
||||
'registered,relaxed,relieved,repeat,repeat_one,restroom,revolving_hearts,rewind,ribbon,rice,rice_ball,' +
|
||||
'rice_cracker,rice_scene,ring,rocket,roller_coaster,rooster,rose,rotating_light,round_pushpin,rowboat,ru,' +
|
||||
'rugby_football,runner,running,running_shirt_with_sash,sa,sagittarius,sailboat,sake,sandal,santa,satellite,' +
|
||||
'satisfied,saxophone,school,school_satchel,scissors,scorpius,scream,scream_cat,scroll,seat,secret,see_no_evil,' +
|
||||
'seedling,seven,shaved_ice,sheep,shell,ship,shipit,shirt,shit,shoe,shower,signal_strength,six,six_pointed_star,' +
|
||||
'ski,skull,sleeping,sleepy,slot_machine,small_blue_diamond,small_orange_diamond,small_red_triangle,' +
|
||||
'small_red_triangle_down,smile,smile_cat,smiley,smiley_cat,smiling_imp,smirk,smirk_cat,smoking,snail,snake,' +
|
||||
'snowboarder,snowflake,snowman,sob,soccer,soon,sos,sound,space_invader,spades,spaghetti,sparkle,sparkler,' +
|
||||
'sparkles,sparkling_heart,speak_no_evil,speaker,speech_balloon,speedboat,squirrel,star,star2,stars,station,' +
|
||||
'statue_of_liberty,steam_locomotive,stew,straight_ruler,strawberry,stuck_out_tongue,stuck_out_tongue_closed_eyes,' +
|
||||
'stuck_out_tongue_winking_eye,sun_with_face,sunflower,sunglasses,sunny,sunrise,sunrise_over_mountains,surfer,' +
|
||||
'sushi,suspect,suspension_railway,sweat,sweat_drops,sweat_smile,sweet_potato,swimmer,symbols,syringe,tada,' +
|
||||
'tanabata_tree,tangerine,taurus,taxi,tea,telephone,telephone_receiver,telescope,tennis,tent,thought_balloon,' +
|
||||
'three,thumbsdown,thumbsup,ticket,tiger,tiger2,tired_face,tm,toilet,tokyo_tower,tomato,tongue,top,tophat,' +
|
||||
'tractor,traffic_light,train,train2,tram,triangular_flag_on_post,triangular_ruler,trident,triumph,trolleybus,' +
|
||||
'trollface,trophy,tropical_drink,tropical_fish,truck,trumpet,tshirt,tulip,turtle,tv,twisted_rightwards_arrows,' +
|
||||
'two,two_hearts,two_men_holding_hands,two_women_holding_hands,u5272,u5408,u55b6,u6307,u6708,u6709,u6e80,u7121,' +
|
||||
'u7533,u7981,u7a7a,uk,umbrella,unamused,underage,unlock,up,us,v,vertical_traffic_light,vhs,vibration_mode,' +
|
||||
'video_camera,video_game,violin,virgo,volcano,vs,walking,waning_crescent_moon,waning_gibbous_moon,warning,watch,' +
|
||||
'water_buffalo,watermelon,wave,wavy_dash,waxing_crescent_moon,waxing_gibbous_moon,wc,weary,wedding,whale,whale2,' +
|
||||
'wheelchair,white_check_mark,white_circle,white_flower,white_large_square,white_medium_small_square,' +
|
||||
'white_medium_square,white_small_square,white_square_button,wind_chime,wine_glass,wink,wolf,woman,' +
|
||||
'womans_clothes,womans_hat,womens,worried,wrench,x,yellow_heart,yen,yum,zap,zero,zzz').split(',');
|
||||
|
||||
// use a map to help make lookups faster instead of having to use indexOf on an array
|
||||
const out = new Map();
|
||||
|
||||
for (let i = 0; i < emoticonNames.length; i++) {
|
||||
out[emoticonNames[i]] = true;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
const emoticonMap = initializeEmoticonMap();
|
||||
|
||||
export function handleEmoticons(text, tokens) {
|
||||
let output = text;
|
||||
|
||||
function replaceEmoticonWithToken(match, name) {
|
||||
if (emoticonMap[name]) {
|
||||
const index = tokens.size;
|
||||
const alias = `MM_EMOTICON${index}`;
|
||||
|
||||
tokens.set(alias, {
|
||||
value: `<img align="absmiddle" alt=${match} class="emoji" src=${getImagePathForEmoticon(name)} title=${match} />`,
|
||||
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`;
|
||||
}
|
||||
@@ -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 `<h${level} id="${id}" class="markdown__heading">${text}</h${level}>`;
|
||||
}
|
||||
|
||||
link(href, title, text) {
|
||||
let outHref = href;
|
||||
|
||||
@@ -11,7 +27,7 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
|
||||
outHref = `http://${outHref}`;
|
||||
}
|
||||
|
||||
let output = '<a class="theme" href="' + outHref + '"';
|
||||
let output = '<a class="theme markdown__link" href="' + outHref + '"';
|
||||
if (title) {
|
||||
output += ' title="' + title + '"';
|
||||
}
|
||||
@@ -19,4 +35,12 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
table(header, body) {
|
||||
return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
|
||||
}
|
||||
|
||||
text(text) {
|
||||
return TextFormatting.doFormatText(text, this.formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = $('<div data-class="' + className + '"></div>');
|
||||
classContainer = $('<div data-class="' + className + classRepeat + '"></div>');
|
||||
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 + '/';
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
251
web/sass-files/sass/partials/_colorpicker.scss
Normal file
@@ -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;
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
.input__help {
|
||||
color: #777;
|
||||
margin: 10px 0 0 10px;
|
||||
word-break: break-word;
|
||||
&.dark {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
28
web/sass-files/sass/partials/_markdown.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -275,7 +275,7 @@ body.ios {
|
||||
&.current--user {
|
||||
.post-body {
|
||||
@include border-radius(4px);
|
||||
background: #f5f5f5;
|
||||
background: rgba(#000, 0.05);
|
||||
}
|
||||
}
|
||||
&.post--comment {
|
||||
|
||||
@@ -107,4 +107,5 @@
|
||||
|
||||
.search-highlight.theme, .search-highlight {
|
||||
background-color: #FFF2BB;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
9
web/static/css/bootstrap-colorpicker.min.css
vendored
Executable file
@@ -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}
|
||||
BIN
web/static/images/bootstrap-colorpicker/alpha-horizontal.png
Executable file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
web/static/images/bootstrap-colorpicker/alpha.png
Executable file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
web/static/images/bootstrap-colorpicker/hue-horizontal.png
Executable file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
web/static/images/bootstrap-colorpicker/hue.png
Executable file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
web/static/images/bootstrap-colorpicker/saturation.png
Executable file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
web/static/images/themes/dark.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
web/static/images/themes/mattermost.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
web/static/images/themes/slack.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
1
web/static/js/bootstrap-colorpicker.min.js
vendored
Executable file
4
web/static/js/emojify.min.js
vendored
@@ -29,6 +29,7 @@
|
||||
<div id="edit_mention_tab"></div>
|
||||
<div id="get_link_modal"></div>
|
||||
<div id="user_settings_modal"></div>
|
||||
<div id="import_theme_modal"></div>
|
||||
<div id="team_settings_modal"></div>
|
||||
<div id="invite_member_modal"></div>
|
||||
<div id="edit_channel_modal"></div>
|
||||
|
||||
@@ -25,10 +25,12 @@
|
||||
|
||||
<link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
|
||||
<link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/css/bootstrap-colorpicker.min.css" rel="stylesheet">
|
||||
|
||||
<script src="/static/js/react-with-addons-0.13.3.js"></script>
|
||||
<script src="/static/js/jquery-1.11.1.js"></script>
|
||||
<script src="/static/js/bootstrap-3.3.5.js"></script>
|
||||
<script src="/static/js/bootstrap-colorpicker.min.js"></script>
|
||||
<script src="/static/js/react-bootstrap-0.25.1.js"></script>
|
||||
|
||||
<link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
|
||||
@@ -40,11 +42,6 @@
|
||||
|
||||
<script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
|
||||
|
||||
<script src="/static/js/emojify.min.js"></script>
|
||||
<script>
|
||||
emojify.setConfig({img_dir: '/static/images/emoji'});
|
||||
</script>
|
||||
|
||||
<style id="antiClickjack">body{display:none !important;}</style>
|
||||
<script src="/static/js/bundle.js"></script>
|
||||
<script type="text/javascript">
|
||||
@@ -56,7 +53,7 @@
|
||||
<script type="text/javascript">
|
||||
if (window.config.SegmentDeveloperKey != null && window.config.SegmentDeveloperKey !== "") {
|
||||
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1";
|
||||
analytics.load(window.config.SegmentWriteKey);
|
||||
analytics.load(window.config.SegmentDeveloperKey);
|
||||
var user = window.UserStore.getCurrentUser(true);
|
||||
if (user) {
|
||||
analytics.identify(user.id, {
|
||||
@@ -65,7 +62,6 @@
|
||||
createdAt: user.create_at,
|
||||
username: user.username,
|
||||
team_id: user.team_id,
|
||||
team_domain: window.getSubDomain(),
|
||||
id: user.id
|
||||
});
|
||||
}
|
||||
|
||||
@@ -355,6 +355,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
||||
resend := r.URL.Query().Get("resend")
|
||||
resendSuccess := r.URL.Query().Get("resend_success")
|
||||
name := r.URL.Query().Get("teamname")
|
||||
email := r.URL.Query().Get("email")
|
||||
hashedId := r.URL.Query().Get("hid")
|
||||
@@ -375,7 +376,9 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
user := result.Data.(*model.User)
|
||||
api.FireAndForgetVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
|
||||
newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1)
|
||||
http.Redirect(w, r, newAddress, http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -400,6 +403,7 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
||||
page.Props["IsVerified"] = isVerified
|
||||
page.Props["TeamURL"] = c.GetTeamURLFromTeam(team)
|
||||
page.Props["UserEmail"] = email
|
||||
page.Props["ResendSuccess"] = resendSuccess
|
||||
page.Render(c, w)
|
||||
}
|
||||
|
||||
|
||||