Fixing merge
Этот коммит содержится в:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,10 +1,14 @@
|
|||||||
# Mattermost Changelog
|
# Mattermost Changelog
|
||||||
|
|
||||||
## UNDER DEVELOPMENT - Release v1.1.0
|
## UNDER DEVELOPMENT Release v1.2.0
|
||||||
|
|
||||||
The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the product's `master` branch to note key changes committed to master and are on their way to the next stable release. When a stable release is pushed the "UNDER DEVELOPMENT" heading is removed from the final changelog of the release.
|
The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the product's `master` branch to note key changes committed to master and are on their way to the next stable release. When a stable release is pushed the "UNDER DEVELOPMENT" heading is removed from the final changelog of the release.
|
||||||
|
|
||||||
- **Final release anticipated:** 2015-10-16
|
- **Final release anticipated:** 2015-11-16
|
||||||
|
|
||||||
|
## Release v1.1.0
|
||||||
|
|
||||||
|
Released: 2015-10-16
|
||||||
|
|
||||||
### Release Highlights
|
### Release Highlights
|
||||||
|
|
||||||
@@ -44,6 +48,11 @@ Messaging and Notifications
|
|||||||
- Fixed bug where System Administrator did not have Team Administrator permissions
|
- Fixed bug where System Administrator did not have Team Administrator permissions
|
||||||
- Fixed bug causing scrolling to jump when the right hand sidebar opened and closed
|
- Fixed bug causing scrolling to jump when the right hand sidebar opened and closed
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
|
||||||
|
- Slack import is unstable due to change in Slack export format
|
||||||
|
- Uploading a .flac file breaks the file previewer on iOS
|
||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
|
|
||||||
Many thanks to our external contributors. In no particular order:
|
Many thanks to our external contributors. In no particular order:
|
||||||
|
|||||||
@@ -14,11 +14,22 @@ func InitPreference(r *mux.Router) {
|
|||||||
l4g.Debug("Initializing preference api routes")
|
l4g.Debug("Initializing preference api routes")
|
||||||
|
|
||||||
sr := r.PathPrefix("/preferences").Subrouter()
|
sr := r.PathPrefix("/preferences").Subrouter()
|
||||||
|
sr.Handle("/", ApiUserRequired(getAllPreferences)).Methods("GET")
|
||||||
sr.Handle("/save", ApiUserRequired(savePreferences)).Methods("POST")
|
sr.Handle("/save", ApiUserRequired(savePreferences)).Methods("POST")
|
||||||
sr.Handle("/{category:[A-Za-z0-9_]+}", ApiUserRequired(getPreferenceCategory)).Methods("GET")
|
sr.Handle("/{category:[A-Za-z0-9_]+}", ApiUserRequired(getPreferenceCategory)).Methods("GET")
|
||||||
sr.Handle("/{category:[A-Za-z0-9_]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getPreference)).Methods("GET")
|
sr.Handle("/{category:[A-Za-z0-9_]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getPreference)).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAllPreferences(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if result := <-Srv.Store.Preference().GetAll(c.Session.UserId); result.Err != nil {
|
||||||
|
c.Err = result.Err
|
||||||
|
} else {
|
||||||
|
data := result.Data.(model.Preferences)
|
||||||
|
|
||||||
|
w.Write([]byte(data.ToJson()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func savePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
|
func savePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
preferences, err := model.PreferencesFromJson(r.Body)
|
preferences, err := model.PreferencesFromJson(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -52,61 +63,10 @@ func getPreferenceCategory(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
data := result.Data.(model.Preferences)
|
data := result.Data.(model.Preferences)
|
||||||
|
|
||||||
data = transformPreferences(c, data, category)
|
|
||||||
|
|
||||||
w.Write([]byte(data.ToJson()))
|
w.Write([]byte(data.ToJson()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformPreferences(c *Context, preferences model.Preferences, category string) model.Preferences {
|
|
||||||
if len(preferences) == 0 && category == model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW {
|
|
||||||
// add direct channels for a user that existed before preferences were added
|
|
||||||
preferences = addDirectChannels(c.Session.UserId, c.Session.TeamId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return preferences
|
|
||||||
}
|
|
||||||
|
|
||||||
func addDirectChannels(userId, teamId string) model.Preferences {
|
|
||||||
var profiles map[string]*model.User
|
|
||||||
if result := <-Srv.Store.User().GetProfiles(teamId); result.Err != nil {
|
|
||||||
l4g.Error("Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v", userId, teamId, result.Err.Error())
|
|
||||||
return model.Preferences{}
|
|
||||||
} else {
|
|
||||||
profiles = result.Data.(map[string]*model.User)
|
|
||||||
}
|
|
||||||
|
|
||||||
var preferences model.Preferences
|
|
||||||
|
|
||||||
for id := range profiles {
|
|
||||||
if id == userId {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
profile := profiles[id]
|
|
||||||
|
|
||||||
preference := model.Preference{
|
|
||||||
UserId: userId,
|
|
||||||
Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
|
|
||||||
Name: profile.Id,
|
|
||||||
Value: "true",
|
|
||||||
}
|
|
||||||
|
|
||||||
preferences = append(preferences, preference)
|
|
||||||
|
|
||||||
if len(preferences) >= 10 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil {
|
|
||||||
l4g.Error("Failed to add direct channel preferences for user user_id=%s, eam_id=%s, err=%v", userId, teamId, result.Err.Error())
|
|
||||||
return model.Preferences{}
|
|
||||||
} else {
|
|
||||||
return preferences
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPreference(c *Context, w http.ResponseWriter, r *http.Request) {
|
func getPreference(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
params := mux.Vars(r)
|
params := mux.Vars(r)
|
||||||
category := params["category"]
|
category := params["category"]
|
||||||
|
|||||||
@@ -9,6 +9,65 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestGetAllPreferences(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||||
|
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||||
|
|
||||||
|
user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||||
|
user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
|
||||||
|
store.Must(Srv.Store.User().VerifyEmail(user1.Id))
|
||||||
|
|
||||||
|
user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||||
|
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
|
||||||
|
store.Must(Srv.Store.User().VerifyEmail(user2.Id))
|
||||||
|
|
||||||
|
category := model.NewId()
|
||||||
|
|
||||||
|
preferences1 := model.Preferences{
|
||||||
|
{
|
||||||
|
UserId: user1.Id,
|
||||||
|
Category: category,
|
||||||
|
Name: model.NewId(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserId: user1.Id,
|
||||||
|
Category: category,
|
||||||
|
Name: model.NewId(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserId: user1.Id,
|
||||||
|
Category: model.NewId(),
|
||||||
|
Name: model.NewId(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.LoginByEmail(team.Name, user1.Email, "pwd")
|
||||||
|
Client.Must(Client.SetPreferences(&preferences1))
|
||||||
|
|
||||||
|
if result, err := Client.GetAllPreferences(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if data := result.Data.(model.Preferences); len(data) != 3 {
|
||||||
|
t.Fatal("received the wrong number of preferences")
|
||||||
|
} else if !((data[0] == preferences1[0] && data[1] == preferences1[1]) || (data[0] == preferences1[1] && data[1] == preferences1[0])) {
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
if data[0] != preferences1[i] && data[1] != preferences1[i] && data[2] != preferences1[i] {
|
||||||
|
t.Fatal("got incorrect preferences")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.LoginByEmail(team.Name, user2.Email, "pwd")
|
||||||
|
|
||||||
|
// note that user2 will automatically have a preference set for them to show user1 for direct messages
|
||||||
|
if result, err := Client.GetAllPreferences(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if data := result.Data.(model.Preferences); len(data) != 1 {
|
||||||
|
t.Fatal("received the wrong number of preferences")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSetPreferences(t *testing.T) {
|
func TestSetPreferences(t *testing.T) {
|
||||||
Setup()
|
Setup()
|
||||||
|
|
||||||
@@ -113,54 +172,6 @@ func TestGetPreferenceCategory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransformPreferences(t *testing.T) {
|
|
||||||
Setup()
|
|
||||||
|
|
||||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
|
||||||
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
|
||||||
|
|
||||||
user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
|
||||||
user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
|
|
||||||
store.Must(Srv.Store.User().VerifyEmail(user1.Id))
|
|
||||||
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
|
||||||
Client.Must(Client.CreateUser(user, ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
Client.Must(Client.LoginByEmail(team.Name, user1.Email, "pwd"))
|
|
||||||
|
|
||||||
if result, err := Client.GetPreferenceCategory(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if data := result.Data.(model.Preferences); len(data) != 5 {
|
|
||||||
t.Fatal("received the wrong number of direct channels")
|
|
||||||
}
|
|
||||||
|
|
||||||
user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
|
||||||
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
|
|
||||||
store.Must(Srv.Store.User().VerifyEmail(user2.Id))
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
|
||||||
Client.Must(Client.CreateUser(user, ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure user1's preferences don't change
|
|
||||||
if result, err := Client.GetPreferenceCategory(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if data := result.Data.(model.Preferences); len(data) != 5 {
|
|
||||||
t.Fatal("received the wrong number of direct channels")
|
|
||||||
}
|
|
||||||
|
|
||||||
Client.Must(Client.LoginByEmail(team.Name, user2.Email, "pwd"))
|
|
||||||
|
|
||||||
if result, err := Client.GetPreferenceCategory(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if data := result.Data.(model.Preferences); len(data) != 10 {
|
|
||||||
t.Fatal("received the wrong number of direct channels")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetPreference(t *testing.T) {
|
func TestGetPreference(t *testing.T) {
|
||||||
Setup()
|
Setup()
|
||||||
|
|
||||||
|
|||||||
23
api/team.go
23
api/team.go
@@ -52,7 +52,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isTreamCreationAllowed(c, email) {
|
if !isTeamCreationAllowed(c, email) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isTreamCreationAllowed(c, team.Email) {
|
if !isTeamCreationAllowed(c, team.Email) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isTreamCreationAllowed(c, teamSignup.Team.Email) {
|
if !isTeamCreationAllowed(c, teamSignup.Team.Email) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ func CreateTeam(c *Context, team *model.Team) *model.Team {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isTreamCreationAllowed(c, team.Email) {
|
if !isTeamCreationAllowed(c, team.Email) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,12 +276,12 @@ func CreateTeam(c *Context, team *model.Team) *model.Team {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isTreamCreationAllowed(c *Context, email string) bool {
|
func isTeamCreationAllowed(c *Context, email string) bool {
|
||||||
|
|
||||||
email = strings.ToLower(email)
|
email = strings.ToLower(email)
|
||||||
|
|
||||||
if !utils.Cfg.TeamSettings.EnableTeamCreation {
|
if !utils.Cfg.TeamSettings.EnableTeamCreation {
|
||||||
c.Err = model.NewAppError("isTreamCreationAllowed", "Team creation has been disabled. Please ask your systems administrator for details.", "")
|
c.Err = model.NewAppError("isTeamCreationAllowed", "Team creation has been disabled. Please ask your systems administrator for details.", "")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +298,7 @@ func isTreamCreationAllowed(c *Context, email string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(utils.Cfg.TeamSettings.RestrictCreationToDomains) > 0 && !matched {
|
if len(utils.Cfg.TeamSettings.RestrictCreationToDomains) > 0 && !matched {
|
||||||
c.Err = model.NewAppError("isTreamCreationAllowed", "Email must be from a specific domain (e.g. @example.com). Please ask your systems administrator for details.", "")
|
c.Err = model.NewAppError("isTeamCreationAllowed", "Email must be from a specific domain (e.g. @example.com). Please ask your systems administrator for details.", "")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,14 +409,13 @@ func findTeams(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
teams := result.Data.([]*model.Team)
|
teams := result.Data.([]*model.Team)
|
||||||
|
m := make(map[string]*model.Team)
|
||||||
s := make([]string, 0, len(teams))
|
|
||||||
|
|
||||||
for _, v := range teams {
|
for _, v := range teams {
|
||||||
s = append(s, v.Name)
|
v.Sanitize()
|
||||||
|
m[v.Id] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Write([]byte(model.ArrayToJson(s)))
|
w.Write([]byte(model.TeamMapToJson(m)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,9 +121,12 @@ func TestFindTeamByEmail(t *testing.T) {
|
|||||||
if r1, err := Client.FindTeams(user.Email); err != nil {
|
if r1, err := Client.FindTeams(user.Email); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
domains := r1.Data.([]string)
|
teams := r1.Data.(map[string]*model.Team)
|
||||||
if domains[0] != team.Name {
|
if teams[team.Id].Name != team.Name {
|
||||||
t.Fatal(domains)
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if teams[team.Id].DisplayName != team.DisplayName {
|
||||||
|
t.Fatal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
api/user.go
43
api/user.go
@@ -198,7 +198,9 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
|
|||||||
l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err)
|
l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fireAndForgetWelcomeEmail(result.Data.(*model.User).Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified)
|
fireAndForgetWelcomeEmail(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified)
|
||||||
|
|
||||||
|
addDirectChannelsAndForget(ruser)
|
||||||
|
|
||||||
if user.EmailVerified {
|
if user.EmailVerified {
|
||||||
if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil {
|
if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil {
|
||||||
@@ -237,6 +239,45 @@ func fireAndForgetWelcomeEmail(userId, email, teamName, teamDisplayName, siteURL
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addDirectChannelsAndForget(user *model.User) {
|
||||||
|
go func() {
|
||||||
|
var profiles map[string]*model.User
|
||||||
|
if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil {
|
||||||
|
l4g.Error("Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v", user.Id, user.TeamId, result.Err.Error())
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
profiles = result.Data.(map[string]*model.User)
|
||||||
|
}
|
||||||
|
|
||||||
|
var preferences model.Preferences
|
||||||
|
|
||||||
|
for id := range profiles {
|
||||||
|
if id == user.Id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
profile := profiles[id]
|
||||||
|
|
||||||
|
preference := model.Preference{
|
||||||
|
UserId: user.Id,
|
||||||
|
Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
|
||||||
|
Name: profile.Id,
|
||||||
|
Value: "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences = append(preferences, preference)
|
||||||
|
|
||||||
|
if len(preferences) >= 10 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil {
|
||||||
|
l4g.Error("Failed to add direct channel preferences for new user user_id=%s, eam_id=%s, err=%v", user.Id, user.TeamId, result.Err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func FireAndForgetVerifyEmail(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) {
|
func FireAndForgetVerifyEmail(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) {
|
||||||
go func() {
|
go func() {
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org
|
|||||||
3. Reload your bashrc
|
3. Reload your bashrc
|
||||||
`source ~/.bashrc`
|
`source ~/.bashrc`
|
||||||
6. Install Node.js
|
6. Install Node.js
|
||||||
1. Download the newest version of the Node.js sources from https://nodejs.org/download/
|
1. Download the newest version of the Node.js sources from https://nodejs.org/en/download/
|
||||||
2. Extract the contents of the package and cd into the extracted files
|
2. Extract the contents of the package and cd into the extracted files
|
||||||
3. Compile and install Node.js
|
3. Compile and install Node.js
|
||||||
`./configure`
|
`./configure`
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
The following install instructions are for single-container installs of Mattermost using Docker for exploring product functionality and upgrading to newer versions.
|
The following install instructions are for single-container installs of Mattermost using Docker for exploring product functionality and upgrading to newer versions.
|
||||||
|
|
||||||
|
### One-line Docker Install ###
|
||||||
|
|
||||||
|
If you have Docker set up, Mattermost installs in one-line:
|
||||||
|
`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`
|
||||||
|
|
||||||
|
Otherwise, see step-by-step available:
|
||||||
|
|
||||||
### Mac OSX ###
|
### Mac OSX ###
|
||||||
|
|
||||||
1. Install Docker Toolbox using instructions at: http://docs.docker.com/installation/mac/
|
1. Install Docker Toolbox using instructions at: http://docs.docker.com/installation/mac/
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
#### Important notes
|
#### Important notes
|
||||||
|
|
||||||
1. **DO NOT manipulate the Mattermost database**
|
1. **DO NOT manipulate the Mattermost database**
|
||||||
- In particular, DO NOT delete data from the database, as this will most likely crash Mattermost in strange ways. Mattermost is designed to archive content continously and generally assumes data is never deleted.
|
- In particular, DO NOT delete data from the database, as Mattermost is designed to stop working if data integrity has been compromised. The system is designed to archive content continously and generally assumes data is never deleted.
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ func (c *Client) FindTeams(email string) (*Result, *AppError) {
|
|||||||
} else {
|
} else {
|
||||||
|
|
||||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||||
r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil
|
r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,6 +844,15 @@ func (c *Client) ListIncomingWebhooks() (*Result, *AppError) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetAllPreferences() (*Result, *AppError) {
|
||||||
|
if r, err := c.DoApiGet("/preferences/", "", ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
preferences, _ := PreferencesFromJson(r.Body)
|
||||||
|
return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), preferences}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) SetPreferences(preferences *Preferences) (*Result, *AppError) {
|
func (c *Client) SetPreferences(preferences *Preferences) (*Result, *AppError) {
|
||||||
if r, err := c.DoApiPost("/preferences/save", preferences.ToJson()); err != nil {
|
if r, err := c.DoApiPost("/preferences/save", preferences.ToJson()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -219,3 +219,9 @@ func CleanTeamName(s string) string {
|
|||||||
|
|
||||||
func (o *Team) PreExport() {
|
func (o *Team) PreExport() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Team) Sanitize() {
|
||||||
|
o.Email = ""
|
||||||
|
o.Type = ""
|
||||||
|
o.AllowedDomains = ""
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEFAULT_WEBHOOK_USERNAME = "webhook"
|
||||||
|
DEFAULT_WEBHOOK_ICON = "/static/images/webhook_icon.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
type IncomingWebhook struct {
|
type IncomingWebhook struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
CreateAt int64 `json:"create_at"`
|
CreateAt int64 `json:"create_at"`
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func (s SqlPreferenceStore) Save(preferences *model.Preferences) StoreChannel {
|
|||||||
result := StoreResult{}
|
result := StoreResult{}
|
||||||
|
|
||||||
// wrap in a transaction so that if one fails, everything fails
|
// wrap in a transaction so that if one fails, everything fails
|
||||||
transaction, err := s.GetReplica().Begin()
|
transaction, err := s.GetMaster().Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Err = model.NewAppError("SqlPreferenceStore.Save", "Unable to open transaction to save preferences", err.Error())
|
result.Err = model.NewAppError("SqlPreferenceStore.Save", "Unable to open transaction to save preferences", err.Error())
|
||||||
} else {
|
} else {
|
||||||
@@ -212,3 +212,30 @@ func (s SqlPreferenceStore) GetCategory(userId string, category string) StoreCha
|
|||||||
|
|
||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlPreferenceStore) GetAll(userId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
var preferences model.Preferences
|
||||||
|
|
||||||
|
if _, err := s.GetReplica().Select(&preferences,
|
||||||
|
`SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
Preferences
|
||||||
|
WHERE
|
||||||
|
UserId = :UserId`, map[string]interface{}{"UserId": userId}); err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlPreferenceStore.GetAll", "We encounted an error while finding preferences", err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = preferences
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|||||||
@@ -144,3 +144,51 @@ func TestPreferenceGetCategory(t *testing.T) {
|
|||||||
t.Fatal("shouldn't have got any preferences")
|
t.Fatal("shouldn't have got any preferences")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreferenceGetAll(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
userId := model.NewId()
|
||||||
|
category := model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW
|
||||||
|
name := model.NewId()
|
||||||
|
|
||||||
|
preferences := model.Preferences{
|
||||||
|
{
|
||||||
|
UserId: userId,
|
||||||
|
Category: category,
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
// same user/category, different name
|
||||||
|
{
|
||||||
|
UserId: userId,
|
||||||
|
Category: category,
|
||||||
|
Name: model.NewId(),
|
||||||
|
},
|
||||||
|
// same user/name, different category
|
||||||
|
{
|
||||||
|
UserId: userId,
|
||||||
|
Category: model.NewId(),
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
// same name/category, different user
|
||||||
|
{
|
||||||
|
UserId: model.NewId(),
|
||||||
|
Category: category,
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Must(store.Preference().Save(&preferences))
|
||||||
|
|
||||||
|
if result := <-store.Preference().GetAll(userId); result.Err != nil {
|
||||||
|
t.Fatal(result.Err)
|
||||||
|
} else if data := result.Data.(model.Preferences); len(data) != 3 {
|
||||||
|
t.Fatal("got the wrong number of preferences")
|
||||||
|
} else {
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
if data[0] != preferences[i] && data[1] != preferences[i] && data[2] != preferences[i] {
|
||||||
|
t.Fatal("got incorrect preferences")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -156,4 +156,5 @@ type PreferenceStore interface {
|
|||||||
Save(preferences *model.Preferences) StoreChannel
|
Save(preferences *model.Preferences) StoreChannel
|
||||||
Get(userId string, category string, name string) StoreChannel
|
Get(userId string, category string, name string) StoreChannel
|
||||||
GetCategory(userId string, category string) StoreChannel
|
GetCategory(userId string, category string) StoreChannel
|
||||||
|
GetAll(userId string) StoreChannel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const Utils = require('../utils/utils.jsx');
|
|||||||
|
|
||||||
const Constants = require('../utils/constants.jsx');
|
const Constants = require('../utils/constants.jsx');
|
||||||
const ActionTypes = Constants.ActionTypes;
|
const ActionTypes = Constants.ActionTypes;
|
||||||
|
const KeyCodes = Constants.KeyCodes;
|
||||||
|
|
||||||
export default class CreatePost extends React.Component {
|
export default class CreatePost extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -35,6 +36,7 @@ export default class CreatePost extends React.Component {
|
|||||||
this.removePreview = this.removePreview.bind(this);
|
this.removePreview = this.removePreview.bind(this);
|
||||||
this.onChange = this.onChange.bind(this);
|
this.onChange = this.onChange.bind(this);
|
||||||
this.getFileCount = this.getFileCount.bind(this);
|
this.getFileCount = this.getFileCount.bind(this);
|
||||||
|
this.handleArrowUp = this.handleArrowUp.bind(this);
|
||||||
|
|
||||||
PostStore.clearDraftUploads();
|
PostStore.clearDraftUploads();
|
||||||
|
|
||||||
@@ -172,7 +174,7 @@ export default class CreatePost extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
postMsgKeyPress(e) {
|
postMsgKeyPress(e) {
|
||||||
if (e.which === 13 && !e.shiftKey && !e.altKey) {
|
if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
ReactDOM.findDOMNode(this.refs.textbox).blur();
|
ReactDOM.findDOMNode(this.refs.textbox).blur();
|
||||||
this.handleSubmit(e);
|
this.handleSubmit(e);
|
||||||
@@ -292,6 +294,27 @@ export default class CreatePost extends React.Component {
|
|||||||
const draft = PostStore.getDraft(channelId);
|
const draft = PostStore.getDraft(channelId);
|
||||||
return draft.previews.length + draft.uploadsInProgress.length;
|
return draft.previews.length + draft.uploadsInProgress.length;
|
||||||
}
|
}
|
||||||
|
handleArrowUp(e) {
|
||||||
|
if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const channelId = ChannelStore.getCurrentId();
|
||||||
|
const lastPost = PostStore.getCurrentUsersLatestPost(channelId);
|
||||||
|
if (!lastPost) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var type = (lastPost.root_id && lastPost.root_id.length > 0) ? 'Comment' : 'Post';
|
||||||
|
|
||||||
|
AppDispatcher.handleViewAction({
|
||||||
|
type: ActionTypes.RECIEVED_EDIT_POST,
|
||||||
|
refocusId: '#post_textbox',
|
||||||
|
title: type,
|
||||||
|
message: lastPost.message,
|
||||||
|
postId: lastPost.id,
|
||||||
|
channelId: lastPost.channel_id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
render() {
|
render() {
|
||||||
let serverError = null;
|
let serverError = null;
|
||||||
if (this.state.serverError) {
|
if (this.state.serverError) {
|
||||||
@@ -336,6 +359,7 @@ export default class CreatePost extends React.Component {
|
|||||||
<Textbox
|
<Textbox
|
||||||
onUserInput={this.handleUserInput}
|
onUserInput={this.handleUserInput}
|
||||||
onKeyPress={this.postMsgKeyPress}
|
onKeyPress={this.postMsgKeyPress}
|
||||||
|
onKeyDown={this.handleArrowUp}
|
||||||
onHeightChange={this.resizePostHolder}
|
onHeightChange={this.resizePostHolder}
|
||||||
messageText={this.state.messageText}
|
messageText={this.state.messageText}
|
||||||
createMessage='Write a message...'
|
createMessage='Write a message...'
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ var Client = require('../utils/client.jsx');
|
|||||||
var AsyncClient = require('../utils/async_client.jsx');
|
var AsyncClient = require('../utils/async_client.jsx');
|
||||||
var Textbox = require('./textbox.jsx');
|
var Textbox = require('./textbox.jsx');
|
||||||
var BrowserStore = require('../stores/browser_store.jsx');
|
var BrowserStore = require('../stores/browser_store.jsx');
|
||||||
|
var PostStore = require('../stores/post_store.jsx');
|
||||||
|
|
||||||
export default class EditPostModal extends React.Component {
|
export default class EditPostModal extends React.Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -14,6 +15,7 @@ export default class EditPostModal extends React.Component {
|
|||||||
this.handleEditInput = this.handleEditInput.bind(this);
|
this.handleEditInput = this.handleEditInput.bind(this);
|
||||||
this.handleEditKeyPress = this.handleEditKeyPress.bind(this);
|
this.handleEditKeyPress = this.handleEditKeyPress.bind(this);
|
||||||
this.handleUserInput = this.handleUserInput.bind(this);
|
this.handleUserInput = this.handleUserInput.bind(this);
|
||||||
|
this.handleEditPostEvent = this.handleEditPostEvent.bind(this);
|
||||||
|
|
||||||
this.state = {editText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''};
|
this.state = {editText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''};
|
||||||
}
|
}
|
||||||
@@ -35,16 +37,15 @@ export default class EditPostModal extends React.Component {
|
|||||||
|
|
||||||
Client.updatePost(updatedPost,
|
Client.updatePost(updatedPost,
|
||||||
function success() {
|
function success() {
|
||||||
AsyncClient.getPosts(this.state.channel_id);
|
AsyncClient.getPosts(updatedPost.channel_id);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}.bind(this),
|
},
|
||||||
function error(err) {
|
function error(err) {
|
||||||
AsyncClient.dispatchError(err, 'updatePost');
|
AsyncClient.dispatchError(err, 'updatePost');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
$('#edit_post').modal('hide');
|
$('#edit_post').modal('hide');
|
||||||
$(this.state.refocusId).focus();
|
|
||||||
}
|
}
|
||||||
handleEditInput(editMessage) {
|
handleEditInput(editMessage) {
|
||||||
this.setState({editText: editMessage});
|
this.setState({editText: editMessage});
|
||||||
@@ -59,6 +60,18 @@ export default class EditPostModal extends React.Component {
|
|||||||
handleUserInput(e) {
|
handleUserInput(e) {
|
||||||
this.setState({editText: e.target.value});
|
this.setState({editText: e.target.value});
|
||||||
}
|
}
|
||||||
|
handleEditPostEvent(options) {
|
||||||
|
this.setState({
|
||||||
|
editText: options.message || '',
|
||||||
|
title: options.title || '',
|
||||||
|
post_id: options.postId || '',
|
||||||
|
channel_id: options.channelId || '',
|
||||||
|
comments: options.comments || 0,
|
||||||
|
refocusId: options.refocusId || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
$(React.findDOMNode(this.refs.modal)).modal('show');
|
||||||
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
@@ -68,12 +81,29 @@ export default class EditPostModal extends React.Component {
|
|||||||
|
|
||||||
$(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function onShow(e) {
|
$(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function onShow(e) {
|
||||||
var button = e.relatedTarget;
|
var button = e.relatedTarget;
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.setState({editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refoucsid')});
|
self.setState({editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refoucsid')});
|
||||||
});
|
});
|
||||||
|
|
||||||
$(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', function onShown() {
|
$(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', function onShown() {
|
||||||
self.refs.editbox.resize();
|
self.refs.editbox.resize();
|
||||||
|
$('#edit_textbox').get(0).focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(React.findDOMNode(this.refs.modal)).on('hide.bs.modal', function onShown() {
|
||||||
|
if (self.state.refocusId !== '') {
|
||||||
|
setTimeout(() => {
|
||||||
|
$(self.state.refocusId).get(0).focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
PostStore.addEditPostListener(this.handleEditPostEvent);
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
PostStore.removeEditPostListener(this.handleEditPostEvent);
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
var error = (<div className='form-group'><br /></div>);
|
var error = (<div className='form-group'><br /></div>);
|
||||||
|
|||||||
@@ -11,7 +11,24 @@ var AboutBuildModal = require('./about_build_modal.jsx');
|
|||||||
var Constants = require('../utils/constants.jsx');
|
var Constants = require('../utils/constants.jsx');
|
||||||
|
|
||||||
function getStateFromStores() {
|
function getStateFromStores() {
|
||||||
return {teams: UserStore.getTeams()};
|
let teams = [];
|
||||||
|
let teamsObject = UserStore.getTeams();
|
||||||
|
for (let teamId in teamsObject) {
|
||||||
|
if (teamsObject.hasOwnProperty(teamId)) {
|
||||||
|
teams.push(teamsObject[teamId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
teams.sort(function sortByDisplayName(teamA, teamB) {
|
||||||
|
let teamADisplayName = teamA.display_name.toLowerCase();
|
||||||
|
let teamBDisplayName = teamB.display_name.toLowerCase();
|
||||||
|
if (teamADisplayName < teamBDisplayName) {
|
||||||
|
return -1;
|
||||||
|
} else if (teamADisplayName > teamBDisplayName) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return {teams};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class NavbarDropdown extends React.Component {
|
export default class NavbarDropdown extends React.Component {
|
||||||
@@ -154,9 +171,9 @@ export default class NavbarDropdown extends React.Component {
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
||||||
this.state.teams.forEach((teamName) => {
|
this.state.teams.forEach((team) => {
|
||||||
if (teamName !== this.props.teamName) {
|
if (team.name !== this.props.teamName) {
|
||||||
teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>{'Switch to ' + teamName}</a></li>);
|
teams.push(<li key={team.name}><a href={Utils.getWindowLocationOrigin() + '/' + team.name}>{'Switch to ' + team.display_name}</a></li>);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
var PostStore = require('../stores/post_store.jsx');
|
var PostStore = require('../stores/post_store.jsx');
|
||||||
var ChannelStore = require('../stores/channel_store.jsx');
|
var ChannelStore = require('../stores/channel_store.jsx');
|
||||||
var UserStore = require('../stores/user_store.jsx');
|
var UserStore = require('../stores/user_store.jsx');
|
||||||
|
var PreferenceStore = require('../stores/preference_store.jsx');
|
||||||
var UserProfile = require('./user_profile.jsx');
|
var UserProfile = require('./user_profile.jsx');
|
||||||
var AsyncClient = require('../utils/async_client.jsx');
|
var AsyncClient = require('../utils/async_client.jsx');
|
||||||
var Post = require('./post.jsx');
|
var Post = require('./post.jsx');
|
||||||
@@ -105,6 +106,7 @@ export default class PostList extends React.Component {
|
|||||||
PostStore.clearUnseenDeletedPosts(this.props.channelId);
|
PostStore.clearUnseenDeletedPosts(this.props.channelId);
|
||||||
PostStore.addChangeListener(this.onChange);
|
PostStore.addChangeListener(this.onChange);
|
||||||
UserStore.addStatusesChangeListener(this.onTimeChange);
|
UserStore.addStatusesChangeListener(this.onTimeChange);
|
||||||
|
PreferenceStore.addChangeListener(this.onTimeChange);
|
||||||
SocketStore.addChangeListener(this.onSocketChange);
|
SocketStore.addChangeListener(this.onSocketChange);
|
||||||
|
|
||||||
const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
|
const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
|
||||||
@@ -156,6 +158,7 @@ export default class PostList extends React.Component {
|
|||||||
PostStore.removeChangeListener(this.onChange);
|
PostStore.removeChangeListener(this.onChange);
|
||||||
UserStore.removeStatusesChangeListener(this.onTimeChange);
|
UserStore.removeStatusesChangeListener(this.onTimeChange);
|
||||||
SocketStore.removeChangeListener(this.onSocketChange);
|
SocketStore.removeChangeListener(this.onSocketChange);
|
||||||
|
PreferenceStore.removeChangeListener(this.onTimeChange);
|
||||||
$('body').off('click.userpopover');
|
$('body').off('click.userpopover');
|
||||||
$(window).off('resize');
|
$(window).off('resize');
|
||||||
var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
|
var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
var PostStore = require('../stores/post_store.jsx');
|
var PostStore = require('../stores/post_store.jsx');
|
||||||
var UserStore = require('../stores/user_store.jsx');
|
var UserStore = require('../stores/user_store.jsx');
|
||||||
|
var PreferenceStore = require('../stores/preference_store.jsx');
|
||||||
var utils = require('../utils/utils.jsx');
|
var utils = require('../utils/utils.jsx');
|
||||||
var SearchBox = require('./search_bar.jsx');
|
var SearchBox = require('./search_bar.jsx');
|
||||||
var CreateComment = require('./create_comment.jsx');
|
var CreateComment = require('./create_comment.jsx');
|
||||||
@@ -18,6 +19,7 @@ export default class RhsThread extends React.Component {
|
|||||||
|
|
||||||
this.onChange = this.onChange.bind(this);
|
this.onChange = this.onChange.bind(this);
|
||||||
this.onChangeAll = this.onChangeAll.bind(this);
|
this.onChangeAll = this.onChangeAll.bind(this);
|
||||||
|
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
|
||||||
|
|
||||||
this.state = this.getStateFromStores();
|
this.state = this.getStateFromStores();
|
||||||
}
|
}
|
||||||
@@ -43,6 +45,7 @@ export default class RhsThread extends React.Component {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
PostStore.addSelectedPostChangeListener(this.onChange);
|
PostStore.addSelectedPostChangeListener(this.onChange);
|
||||||
PostStore.addChangeListener(this.onChangeAll);
|
PostStore.addChangeListener(this.onChangeAll);
|
||||||
|
PreferenceStore.addChangeListener(this.forceUpdateInfo);
|
||||||
this.resize();
|
this.resize();
|
||||||
$(window).resize(function resize() {
|
$(window).resize(function resize() {
|
||||||
this.resize();
|
this.resize();
|
||||||
@@ -57,6 +60,16 @@ export default class RhsThread extends React.Component {
|
|||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
PostStore.removeSelectedPostChangeListener(this.onChange);
|
PostStore.removeSelectedPostChangeListener(this.onChange);
|
||||||
PostStore.removeChangeListener(this.onChangeAll);
|
PostStore.removeChangeListener(this.onChangeAll);
|
||||||
|
PreferenceStore.removeChangeListener(this.forceUpdateInfo);
|
||||||
|
}
|
||||||
|
forceUpdateInfo() {
|
||||||
|
if (this.state.postList) {
|
||||||
|
for (var postId in this.state.postList.posts) {
|
||||||
|
if (this.refs[postId]) {
|
||||||
|
this.refs[postId].forceUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onChange() {
|
onChange() {
|
||||||
var newState = this.getStateFromStores();
|
var newState = this.getStateFromStores();
|
||||||
@@ -174,6 +187,7 @@ export default class RhsThread extends React.Component {
|
|||||||
/>
|
/>
|
||||||
<div className='post-right__scroll'>
|
<div className='post-right__scroll'>
|
||||||
<RootPost
|
<RootPost
|
||||||
|
ref={rootPost.id}
|
||||||
post={rootPost}
|
post={rootPost}
|
||||||
commentCount={postsArray.length}
|
commentCount={postsArray.length}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -132,11 +132,7 @@ export default class Sidebar extends React.Component {
|
|||||||
SocketStore.addChangeListener(this.onSocketChange);
|
SocketStore.addChangeListener(this.onSocketChange);
|
||||||
PreferenceStore.addChangeListener(this.onChange);
|
PreferenceStore.addChangeListener(this.onChange);
|
||||||
|
|
||||||
AsyncClient.getDirectChannelPreferences();
|
$('.nav-pills__container').perfectScrollbar();
|
||||||
|
|
||||||
if ($(window).width() > 768) {
|
|
||||||
$('.nav-pills__container').perfectScrollbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateTitle();
|
this.updateTitle();
|
||||||
this.updateUnreadIndicators();
|
this.updateUnreadIndicators();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const ErrorStore = require('../stores/error_store.jsx');
|
|||||||
const Utils = require('../utils/utils.jsx');
|
const Utils = require('../utils/utils.jsx');
|
||||||
const Constants = require('../utils/constants.jsx');
|
const Constants = require('../utils/constants.jsx');
|
||||||
const ActionTypes = Constants.ActionTypes;
|
const ActionTypes = Constants.ActionTypes;
|
||||||
|
const KeyCodes = Constants.KeyCodes;
|
||||||
|
|
||||||
export default class Textbox extends React.Component {
|
export default class Textbox extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -148,8 +149,10 @@ export default class Textbox extends React.Component {
|
|||||||
this.doProcessMentions = true;
|
this.doProcessMentions = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.keyCode === 8) {
|
if (e.keyCode === KeyCodes.BACKSPACE) {
|
||||||
this.handleBackspace(e);
|
this.handleBackspace(e);
|
||||||
|
} else if (this.props.onKeyDown) {
|
||||||
|
this.props.onKeyDown(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,5 +321,6 @@ Textbox.propTypes = {
|
|||||||
onUserInput: React.PropTypes.func.isRequired,
|
onUserInput: React.PropTypes.func.isRequired,
|
||||||
onKeyPress: React.PropTypes.func.isRequired,
|
onKeyPress: React.PropTypes.func.isRequired,
|
||||||
onHeightChange: React.PropTypes.func,
|
onHeightChange: React.PropTypes.func,
|
||||||
createMessage: React.PropTypes.string.isRequired
|
createMessage: React.PropTypes.string.isRequired,
|
||||||
|
onKeyDown: React.PropTypes.func
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ var GeneralTab = require('./user_settings_general.jsx');
|
|||||||
var AppearanceTab = require('./user_settings_appearance.jsx');
|
var AppearanceTab = require('./user_settings_appearance.jsx');
|
||||||
var DeveloperTab = require('./user_settings_developer.jsx');
|
var DeveloperTab = require('./user_settings_developer.jsx');
|
||||||
var IntegrationsTab = require('./user_settings_integrations.jsx');
|
var IntegrationsTab = require('./user_settings_integrations.jsx');
|
||||||
|
var DisplayTab = require('./user_settings_display.jsx');
|
||||||
|
|
||||||
export default class UserSettings extends React.Component {
|
export default class UserSettings extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -98,6 +99,17 @@ export default class UserSettings extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if (this.props.activeTab === 'display') {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<DisplayTab
|
||||||
|
user={this.state.user}
|
||||||
|
activeSection={this.props.activeSection}
|
||||||
|
updateSection={this.props.updateSection}
|
||||||
|
updateTab={this.props.updateTab}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div/>;
|
return <div/>;
|
||||||
|
|||||||
168
web/react/components/user_settings/user_settings_display.jsx
Обычный файл
168
web/react/components/user_settings/user_settings_display.jsx
Обычный файл
@@ -0,0 +1,168 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import { savePreferences } from '../../utils/client.jsx';
|
||||||
|
import SettingItemMin from '../setting_item_min.jsx';
|
||||||
|
import SettingItemMax from '../setting_item_max.jsx';
|
||||||
|
import Constants from '../../utils/constants.jsx';
|
||||||
|
import PreferenceStore from '../../stores/preference_store.jsx';
|
||||||
|
|
||||||
|
function getDisplayStateFromStores() {
|
||||||
|
const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'});
|
||||||
|
|
||||||
|
return {militaryTime: militaryTime.value};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class UserSettingsDisplay extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
this.handleClockRadio = this.handleClockRadio.bind(this);
|
||||||
|
this.updateSection = this.updateSection.bind(this);
|
||||||
|
this.handleClose = this.handleClose.bind(this);
|
||||||
|
|
||||||
|
this.state = getDisplayStateFromStores();
|
||||||
|
}
|
||||||
|
handleSubmit() {
|
||||||
|
const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime);
|
||||||
|
|
||||||
|
savePreferences([preference],
|
||||||
|
() => {
|
||||||
|
PreferenceStore.emitChange();
|
||||||
|
this.updateSection('');
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.setState({serverError: err.message});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
handleClockRadio(militaryTime) {
|
||||||
|
this.setState({militaryTime: militaryTime});
|
||||||
|
}
|
||||||
|
updateSection(section) {
|
||||||
|
this.setState(getDisplayStateFromStores());
|
||||||
|
this.props.updateSection(section);
|
||||||
|
}
|
||||||
|
handleClose() {
|
||||||
|
this.updateSection('');
|
||||||
|
}
|
||||||
|
componentDidMount() {
|
||||||
|
$('#user_settings').on('hidden.bs.modal', this.handleClose);
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
$('#user_settings').off('hidden.bs.modal', this.handleClose);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const serverError = this.state.serverError || null;
|
||||||
|
let clockSection;
|
||||||
|
if (this.props.activeSection === 'clock') {
|
||||||
|
let clockFormat = [false, false];
|
||||||
|
if (this.state.militaryTime === 'true') {
|
||||||
|
clockFormat[1] = true;
|
||||||
|
} else {
|
||||||
|
clockFormat[0] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateClockSection = (e) => {
|
||||||
|
this.updateSection('');
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputs = [
|
||||||
|
<div key='userDisplayClockOptions'>
|
||||||
|
<div className='radio'>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
checked={clockFormat[0]}
|
||||||
|
onChange={this.handleClockRadio.bind(this, 'false')}
|
||||||
|
>
|
||||||
|
12-hour clock (example: 4:00 PM)
|
||||||
|
</input>
|
||||||
|
</label>
|
||||||
|
<br/>
|
||||||
|
</div>
|
||||||
|
<div className='radio'>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
checked={clockFormat[1]}
|
||||||
|
onChange={this.handleClockRadio.bind(this, 'true')}
|
||||||
|
>
|
||||||
|
24-hour clock (example: 16:00)
|
||||||
|
</input>
|
||||||
|
</label>
|
||||||
|
<br/>
|
||||||
|
</div>
|
||||||
|
<div><br/>{'Select how you prefer time displayed.'}</div>
|
||||||
|
</div>
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
clockSection = (
|
||||||
|
<SettingItemMax
|
||||||
|
title='Clock Display'
|
||||||
|
inputs={inputs}
|
||||||
|
submit={this.handleSubmit}
|
||||||
|
server_error={serverError}
|
||||||
|
updateSection={handleUpdateClockSection}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let describe = '';
|
||||||
|
if (this.state.militaryTime === 'true') {
|
||||||
|
describe = '24-hour clock (example: 16:00)';
|
||||||
|
} else {
|
||||||
|
describe = '12-hour clock (example: 4:00 PM)';
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateClockSection = () => {
|
||||||
|
this.props.updateSection('clock');
|
||||||
|
};
|
||||||
|
|
||||||
|
clockSection = (
|
||||||
|
<SettingItemMin
|
||||||
|
title='Clock Display'
|
||||||
|
describe={describe}
|
||||||
|
updateSection={handleUpdateClockSection}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='modal-header'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='close'
|
||||||
|
data-dismiss='modal'
|
||||||
|
aria-label='Close'
|
||||||
|
>
|
||||||
|
<span aria-hidden='true'>{'×'}</span>
|
||||||
|
</button>
|
||||||
|
<h4
|
||||||
|
className='modal-title'
|
||||||
|
ref='title'
|
||||||
|
>
|
||||||
|
<i className='modal-back'></i>
|
||||||
|
{'Display Settings'}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className='user-settings'>
|
||||||
|
<h3 className='tab-header'>{'Display Settings'}</h3>
|
||||||
|
<div className='divider-dark first'/>
|
||||||
|
{clockSection}
|
||||||
|
<div className='divider-dark'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UserSettingsDisplay.propTypes = {
|
||||||
|
user: React.PropTypes.object,
|
||||||
|
updateSection: React.PropTypes.func,
|
||||||
|
updateTab: React.PropTypes.func,
|
||||||
|
activeSection: React.PropTypes.string
|
||||||
|
};
|
||||||
@@ -41,6 +41,7 @@ export default class UserSettingsModal extends React.Component {
|
|||||||
if (global.window.mm_config.EnableIncomingWebhooks === 'true') {
|
if (global.window.mm_config.EnableIncomingWebhooks === 'true') {
|
||||||
tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
|
tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
|
||||||
}
|
}
|
||||||
|
tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
|
|||||||
var RegisterAppModal = require('../components/register_app_modal.jsx');
|
var RegisterAppModal = require('../components/register_app_modal.jsx');
|
||||||
var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
|
var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
|
||||||
|
|
||||||
|
var AsyncClient = require('../utils/async_client.jsx');
|
||||||
var Constants = require('../utils/constants.jsx');
|
var Constants = require('../utils/constants.jsx');
|
||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
@@ -46,6 +47,8 @@ function setupChannelPage(props) {
|
|||||||
id: props.ChannelId
|
id: props.ChannelId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AsyncClient.getAllPreferences();
|
||||||
|
|
||||||
// ChannelLoader must be rendered first
|
// ChannelLoader must be rendered first
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<ChannelLoader/>,
|
<ChannelLoader/>,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ var EventEmitter = require('events').EventEmitter;
|
|||||||
|
|
||||||
var ChannelStore = require('../stores/channel_store.jsx');
|
var ChannelStore = require('../stores/channel_store.jsx');
|
||||||
var BrowserStore = require('../stores/browser_store.jsx');
|
var BrowserStore = require('../stores/browser_store.jsx');
|
||||||
|
var UserStore = require('../stores/user_store.jsx');
|
||||||
|
|
||||||
var Constants = require('../utils/constants.jsx');
|
var Constants = require('../utils/constants.jsx');
|
||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
@@ -16,6 +17,7 @@ var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
|
|||||||
var SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
|
var SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
|
||||||
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
|
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
|
||||||
var ADD_MENTION_EVENT = 'add_mention';
|
var ADD_MENTION_EVENT = 'add_mention';
|
||||||
|
var EDIT_POST_EVENT = 'edit_post';
|
||||||
|
|
||||||
class PostStoreClass extends EventEmitter {
|
class PostStoreClass extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -75,6 +77,10 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this);
|
this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this);
|
||||||
this.storeLatestUpdate = this.storeLatestUpdate.bind(this);
|
this.storeLatestUpdate = this.storeLatestUpdate.bind(this);
|
||||||
this.getLatestUpdate = this.getLatestUpdate.bind(this);
|
this.getLatestUpdate = this.getLatestUpdate.bind(this);
|
||||||
|
this.emitEditPost = this.emitEditPost.bind(this);
|
||||||
|
this.addEditPostListener = this.addEditPostListener.bind(this);
|
||||||
|
this.removeEditPostListener = this.removeEditPostListener.bind(this);
|
||||||
|
this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this);
|
||||||
}
|
}
|
||||||
emitChange() {
|
emitChange() {
|
||||||
this.emit(CHANGE_EVENT);
|
this.emit(CHANGE_EVENT);
|
||||||
@@ -148,6 +154,18 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.removeListener(ADD_MENTION_EVENT, callback);
|
this.removeListener(ADD_MENTION_EVENT, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitEditPost(post) {
|
||||||
|
this.emit(EDIT_POST_EVENT, post);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEditPostListener(callback) {
|
||||||
|
this.on(EDIT_POST_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEditPostListener(callback) {
|
||||||
|
this.removeListener(EDIT_POST_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentPosts() {
|
getCurrentPosts() {
|
||||||
var currentId = ChannelStore.getCurrentId();
|
var currentId = ChannelStore.getCurrentId();
|
||||||
|
|
||||||
@@ -212,6 +230,22 @@ class PostStoreClass extends EventEmitter {
|
|||||||
getPosts(channelId) {
|
getPosts(channelId) {
|
||||||
return BrowserStore.getItem('posts_' + channelId);
|
return BrowserStore.getItem('posts_' + channelId);
|
||||||
}
|
}
|
||||||
|
getCurrentUsersLatestPost(channelId) {
|
||||||
|
const userId = UserStore.getCurrentId();
|
||||||
|
var postList = makePostListNonNull(this.getPosts(channelId));
|
||||||
|
var i = 0;
|
||||||
|
var len = postList.order.length;
|
||||||
|
var lastPost = null;
|
||||||
|
|
||||||
|
for (i; i < len; i++) {
|
||||||
|
if (postList.posts[postList.order[i]].user_id === userId) {
|
||||||
|
lastPost = postList.posts[postList.order[i]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastPost;
|
||||||
|
}
|
||||||
storePost(post) {
|
storePost(post) {
|
||||||
this.pStorePost(post);
|
this.pStorePost(post);
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
@@ -446,6 +480,9 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
|
|||||||
case ActionTypes.RECIEVED_ADD_MENTION:
|
case ActionTypes.RECIEVED_ADD_MENTION:
|
||||||
PostStore.emitAddMention(action.id, action.username);
|
PostStore.emitAddMention(action.id, action.username);
|
||||||
break;
|
break;
|
||||||
|
case ActionTypes.RECIEVED_EDIT_POST:
|
||||||
|
PostStore.emitEditPost(action);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -120,3 +120,4 @@ class PreferenceStoreClass extends EventEmitter {
|
|||||||
|
|
||||||
const PreferenceStore = new PreferenceStoreClass();
|
const PreferenceStore = new PreferenceStoreClass();
|
||||||
export default PreferenceStore;
|
export default PreferenceStore;
|
||||||
|
window.PreferenceStore = PreferenceStore;
|
||||||
|
|||||||
@@ -639,16 +639,15 @@ export function getMyTeam() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDirectChannelPreferences() {
|
export function getAllPreferences() {
|
||||||
if (isCallInProgress('getDirectChannelPreferences')) {
|
if (isCallInProgress('getAllPreferences')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
callTracker.getDirectChannelPreferences = utils.getTimestamp();
|
callTracker.getAllPreferences = utils.getTimestamp();
|
||||||
client.getPreferenceCategory(
|
client.getAllPreferences(
|
||||||
Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
|
||||||
(data, textStatus, xhr) => {
|
(data, textStatus, xhr) => {
|
||||||
callTracker.getDirectChannelPreferences = 0;
|
callTracker.getAllPreferences = 0;
|
||||||
|
|
||||||
if (xhr.status === 304 || !data) {
|
if (xhr.status === 304 || !data) {
|
||||||
return;
|
return;
|
||||||
@@ -660,8 +659,8 @@ export function getDirectChannelPreferences() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
callTracker.getDirectChannelPreferences = 0;
|
callTracker.getAllPreferences = 0;
|
||||||
dispatchError(err, 'getDirectChannelPreferences');
|
dispatchError(err, 'getAllPreferences');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1142,6 +1142,19 @@ export function listIncomingHooks(success, error) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAllPreferences(success, error) {
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/preferences/`,
|
||||||
|
dataType: 'json',
|
||||||
|
type: 'GET',
|
||||||
|
success,
|
||||||
|
error: (xhr, status, err) => {
|
||||||
|
var e = handleError('getAllPreferences', xhr, status, err);
|
||||||
|
error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getPreferenceCategory(category, success, error) {
|
export function getPreferenceCategory(category, success, error) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: `/api/v1/preferences/${category}`,
|
url: `/api/v1/preferences/${category}`,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ module.exports = {
|
|||||||
|
|
||||||
RECIEVED_POSTS: null,
|
RECIEVED_POSTS: null,
|
||||||
RECIEVED_POST: null,
|
RECIEVED_POST: null,
|
||||||
|
RECIEVED_EDIT_POST: null,
|
||||||
RECIEVED_SEARCH: null,
|
RECIEVED_SEARCH: null,
|
||||||
RECIEVED_POST_SELECTED: null,
|
RECIEVED_POST_SELECTED: null,
|
||||||
RECIEVED_MENTION_DATA: null,
|
RECIEVED_MENTION_DATA: null,
|
||||||
@@ -287,6 +288,16 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
Preferences: {
|
Preferences: {
|
||||||
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show'
|
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
|
||||||
|
CATEGORY_DISPLAY_SETTINGS: 'display_settings'
|
||||||
|
},
|
||||||
|
KeyCodes: {
|
||||||
|
UP: 38,
|
||||||
|
DOWN: 40,
|
||||||
|
LEFT: 37,
|
||||||
|
RIGHT: 39,
|
||||||
|
BACKSPACE: 8,
|
||||||
|
ENTER: 13,
|
||||||
|
ESCAPE: 27
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||||
var ChannelStore = require('../stores/channel_store.jsx');
|
var ChannelStore = require('../stores/channel_store.jsx');
|
||||||
var UserStore = require('../stores/user_store.jsx');
|
var UserStore = require('../stores/user_store.jsx');
|
||||||
|
var PreferenceStore = require('../stores/preference_store.jsx');
|
||||||
var TeamStore = require('../stores/team_store.jsx');
|
var TeamStore = require('../stores/team_store.jsx');
|
||||||
var Constants = require('../utils/constants.jsx');
|
var Constants = require('../utils/constants.jsx');
|
||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
@@ -164,23 +165,29 @@ export function displayDate(ticks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function displayTime(ticks) {
|
export function displayTime(ticks) {
|
||||||
var d = new Date(ticks);
|
const d = new Date(ticks);
|
||||||
var hours = d.getHours();
|
let hours = d.getHours();
|
||||||
var minutes = d.getMinutes();
|
let minutes = d.getMinutes();
|
||||||
|
let ampm = '';
|
||||||
|
|
||||||
var ampm = 'AM';
|
|
||||||
if (hours >= 12) {
|
|
||||||
ampm = 'PM';
|
|
||||||
}
|
|
||||||
|
|
||||||
hours = hours % 12;
|
|
||||||
if (!hours) {
|
|
||||||
hours = '12';
|
|
||||||
}
|
|
||||||
if (minutes <= 9) {
|
if (minutes <= 9) {
|
||||||
minutes = '0' + minutes;
|
minutes = '0' + minutes;
|
||||||
}
|
}
|
||||||
return hours + ':' + minutes + ' ' + ampm;
|
|
||||||
|
const useMilitaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'}).value;
|
||||||
|
if (useMilitaryTime === 'false') {
|
||||||
|
ampm = ' AM';
|
||||||
|
if (hours >= 12) {
|
||||||
|
ampm = ' PM';
|
||||||
|
}
|
||||||
|
|
||||||
|
hours = hours % 12;
|
||||||
|
if (!hours) {
|
||||||
|
hours = '12';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hours + ':' + minutes + ampm;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function displayDateTime(ticks) {
|
export function displayDateTime(ticks) {
|
||||||
|
|||||||
@@ -93,7 +93,8 @@
|
|||||||
|
|
||||||
.post-right__scroll {
|
.post-right__scroll {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: auto;
|
overflow: scroll;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-right-comment-time {
|
.post-right-comment-time {
|
||||||
|
|||||||
@@ -67,8 +67,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-items-container {
|
.search-items-container {
|
||||||
overflow: auto;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: scroll;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-results-header {
|
.search-results-header {
|
||||||
|
|||||||
@@ -47,7 +47,8 @@
|
|||||||
.nav-pills__container {
|
.nav-pills__container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: auto;
|
overflow: scroll;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-pills__unread-indicator {
|
.nav-pills__unread-indicator {
|
||||||
|
|||||||
Двоичные данные
web/static/images/webhook_icon.jpg
Обычный файл
Двоичные данные
web/static/images/webhook_icon.jpg
Обычный файл
Двоичный файл не отображается.
|
После Ширина: | Высота: | Размер: 67 KiB |
16
web/web.go
16
web/web.go
@@ -1031,12 +1031,20 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
post := &model.Post{UserId: hook.UserId, ChannelId: channel.Id, Message: text}
|
post := &model.Post{UserId: hook.UserId, ChannelId: channel.Id, Message: text}
|
||||||
post.AddProp("from_webhook", "true")
|
post.AddProp("from_webhook", "true")
|
||||||
|
|
||||||
if len(overrideUsername) != 0 && utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
|
if utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
|
||||||
post.AddProp("override_username", overrideUsername)
|
if len(overrideUsername) != 0 {
|
||||||
|
post.AddProp("override_username", overrideUsername)
|
||||||
|
} else {
|
||||||
|
post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(overrideIconUrl) != 0 && utils.Cfg.ServiceSettings.EnablePostIconOverride {
|
if utils.Cfg.ServiceSettings.EnablePostIconOverride {
|
||||||
post.AddProp("override_icon_url", overrideIconUrl)
|
if len(overrideIconUrl) != 0 {
|
||||||
|
post.AddProp("override_icon_url", overrideIconUrl)
|
||||||
|
} else {
|
||||||
|
post.AddProp("override_icon_url", model.DEFAULT_WEBHOOK_ICON)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
|
if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user