Fixing merge

This commit is contained in:
=Corey Hulen
2015-09-23 14:38:34 -07:00
83 changed files with 1672 additions and 492 deletions

View File

@@ -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).

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -33,11 +33,11 @@
"FileFormat": "",
"FileLocation": ""
},
"ImageSettings": {
"FileSettings": {
"DriverName": "local",
"Directory": "./data/",
"EnablePublicLink": true,
"PublicLinkSalt": "LhaAWC6lYEKHTkBKsvyXNIOfUIT37AXe",
"PublicLinkSalt": "A705AklYF8MFDOfcwh3I488G8vtLlVip",
"ThumbnailWidth": 120,
"ThumbnailHeight": 100,
"PreviewWidth": 1024,

View File

@@ -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.

View File

@@ -33,7 +33,7 @@
"FileFormat": "",
"FileLocation": ""
},
"ImageSettings": {
"FileSettings": {
"DriverName": "local",
"Directory": "/mattermost/data/",
"EnablePublicLink": true,

View File

@@ -33,7 +33,7 @@
"FileFormat": "",
"FileLocation": ""
},
"ImageSettings": {
"FileSettings": {
"DriverName": "local",
"Directory": "/mattermost/data/",
"EnablePublicLink": true,

View File

@@ -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

View File

@@ -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
}

View File

@@ -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() {

View File

@@ -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
}

View File

@@ -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') {

View File

@@ -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>

View File

@@ -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
};

View File

@@ -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') {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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
};

View File

@@ -114,7 +114,7 @@ export default class MoreDirectChannels extends React.Component {
<span aria-hidden='true'>&times;</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'>

View File

@@ -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}

View File

@@ -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'

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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() {

View File

@@ -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>
);
}

View File

@@ -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}
/>

View File

@@ -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'

View File

@@ -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'

View File

@@ -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>
);

View File

@@ -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;
}

View File

@@ -50,6 +50,7 @@ export default class SearchResultsHeader extends React.Component {
title='Close'
onClick={this.handleClose}
>
<i className='fa fa-sign-out'/>
</button>
</div>
);

View File

@@ -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';
}
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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: ''});

View 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
};

View 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>
);
}
}

View 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
};

View File

@@ -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'>&times;</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
};

View File

@@ -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'

View File

@@ -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'

View File

@@ -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>
);

View File

@@ -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": {

View File

@@ -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')

View File

@@ -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')
);

View File

@@ -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:
}

View File

@@ -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'
}
]
};

View 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: /:[']-?\(|:&#x27;\(/g, // :`(
kissing_heart: /:-?\*/g, // :*
wink: /;-?\)/g, // ;)
pensive: /:-?\//g, // :/
confounded: /:-?s/gi, // :s
flushed: /:-?\|/g, // :|
relaxed: /:-?\$/g, // :$
mask: /:-x/gi, // :-x
heart: /<3|&lt;3/g, // <3
broken_heart: /<\/3|&lt;&#x2F;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`;
}

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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 + '/';
}

View File

@@ -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 {

View 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;
}

View File

@@ -18,6 +18,7 @@
.input__help {
color: #777;
margin: 10px 0 0 10px;
word-break: break-word;
&.dark {
color: #222;
}

View File

@@ -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;

View 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;
}
}

View File

@@ -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;

View File

@@ -275,7 +275,7 @@ body.ios {
&.current--user {
.post-body {
@include border-radius(4px);
background: #f5f5f5;
background: rgba(#000, 0.05);
}
}
&.post--comment {

View File

@@ -107,4 +107,5 @@
.search-highlight.theme, .search-highlight {
background-color: #FFF2BB;
color: #333;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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";

View 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}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

1
web/static/js/bootstrap-colorpicker.min.js vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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>

View File

@@ -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
});
}

View File

@@ -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)
}