Fixing merge
Этот коммит содержится в:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,10 +1,14 @@
|
||||
# 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.
|
||||
|
||||
- **Final release anticipated:** 2015-10-16
|
||||
- **Final release anticipated:** 2015-11-16
|
||||
|
||||
## Release v1.1.0
|
||||
|
||||
Released: 2015-10-16
|
||||
|
||||
### Release Highlights
|
||||
|
||||
@@ -44,6 +48,11 @@ Messaging and Notifications
|
||||
- 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
|
||||
|
||||
### Known Issues
|
||||
|
||||
- Slack import is unstable due to change in Slack export format
|
||||
- Uploading a .flac file breaks the file previewer on iOS
|
||||
|
||||
### Contributors
|
||||
|
||||
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")
|
||||
|
||||
sr := r.PathPrefix("/preferences").Subrouter()
|
||||
sr.Handle("/", ApiUserRequired(getAllPreferences)).Methods("GET")
|
||||
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_]+}/{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) {
|
||||
preferences, err := model.PreferencesFromJson(r.Body)
|
||||
if err != nil {
|
||||
@@ -52,61 +63,10 @@ func getPreferenceCategory(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
data := result.Data.(model.Preferences)
|
||||
|
||||
data = transformPreferences(c, data, category)
|
||||
|
||||
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) {
|
||||
params := mux.Vars(r)
|
||||
category := params["category"]
|
||||
|
||||
@@ -9,6 +9,65 @@ import (
|
||||
"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) {
|
||||
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) {
|
||||
Setup()
|
||||
|
||||
|
||||
23
api/team.go
23
api/team.go
@@ -52,7 +52,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !isTreamCreationAllowed(c, email) {
|
||||
if !isTeamCreationAllowed(c, email) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !isTreamCreationAllowed(c, team.Email) {
|
||||
if !isTeamCreationAllowed(c, team.Email) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !isTreamCreationAllowed(c, teamSignup.Team.Email) {
|
||||
if !isTeamCreationAllowed(c, teamSignup.Team.Email) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ func CreateTeam(c *Context, team *model.Team) *model.Team {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isTreamCreationAllowed(c, team.Email) {
|
||||
if !isTeamCreationAllowed(c, team.Email) {
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ func isTreamCreationAllowed(c *Context, email string) bool {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -409,14 +409,13 @@ func findTeams(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
} else {
|
||||
teams := result.Data.([]*model.Team)
|
||||
|
||||
s := make([]string, 0, len(teams))
|
||||
|
||||
m := make(map[string]*model.Team)
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
domains := r1.Data.([]string)
|
||||
if domains[0] != team.Name {
|
||||
t.Fatal(domains)
|
||||
teams := r1.Data.(map[string]*model.Team)
|
||||
if teams[team.Id].Name != team.Name {
|
||||
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)
|
||||
}
|
||||
|
||||
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 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) {
|
||||
go func() {
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org
|
||||
3. Reload your bashrc
|
||||
`source ~/.bashrc`
|
||||
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
|
||||
3. Compile and install Node.js
|
||||
`./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.
|
||||
|
||||
### 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 ###
|
||||
|
||||
1. Install Docker Toolbox using instructions at: http://docs.docker.com/installation/mac/
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
#### Important notes
|
||||
|
||||
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 {
|
||||
|
||||
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) {
|
||||
if r, err := c.DoApiPost("/preferences/save", preferences.ToJson()); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -219,3 +219,9 @@ func CleanTeamName(s string) string {
|
||||
|
||||
func (o *Team) PreExport() {
|
||||
}
|
||||
|
||||
func (o *Team) Sanitize() {
|
||||
o.Email = ""
|
||||
o.Type = ""
|
||||
o.AllowedDomains = ""
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_WEBHOOK_USERNAME = "webhook"
|
||||
DEFAULT_WEBHOOK_ICON = "/static/images/webhook_icon.jpg"
|
||||
)
|
||||
|
||||
type IncomingWebhook struct {
|
||||
Id string `json:"id"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
|
||||
@@ -43,7 +43,7 @@ func (s SqlPreferenceStore) Save(preferences *model.Preferences) StoreChannel {
|
||||
result := StoreResult{}
|
||||
|
||||
// wrap in a transaction so that if one fails, everything fails
|
||||
transaction, err := s.GetReplica().Begin()
|
||||
transaction, err := s.GetMaster().Begin()
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlPreferenceStore.Save", "Unable to open transaction to save preferences", err.Error())
|
||||
} else {
|
||||
@@ -212,3 +212,30 @@ func (s SqlPreferenceStore) GetCategory(userId string, category string) StoreCha
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Get(userId string, category string, name 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 ActionTypes = Constants.ActionTypes;
|
||||
const KeyCodes = Constants.KeyCodes;
|
||||
|
||||
export default class CreatePost extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -35,6 +36,7 @@ export default class CreatePost extends React.Component {
|
||||
this.removePreview = this.removePreview.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.getFileCount = this.getFileCount.bind(this);
|
||||
this.handleArrowUp = this.handleArrowUp.bind(this);
|
||||
|
||||
PostStore.clearDraftUploads();
|
||||
|
||||
@@ -172,7 +174,7 @@ export default class CreatePost extends React.Component {
|
||||
}
|
||||
}
|
||||
postMsgKeyPress(e) {
|
||||
if (e.which === 13 && !e.shiftKey && !e.altKey) {
|
||||
if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
ReactDOM.findDOMNode(this.refs.textbox).blur();
|
||||
this.handleSubmit(e);
|
||||
@@ -292,6 +294,27 @@ export default class CreatePost extends React.Component {
|
||||
const draft = PostStore.getDraft(channelId);
|
||||
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() {
|
||||
let serverError = null;
|
||||
if (this.state.serverError) {
|
||||
@@ -336,6 +359,7 @@ export default class CreatePost extends React.Component {
|
||||
<Textbox
|
||||
onUserInput={this.handleUserInput}
|
||||
onKeyPress={this.postMsgKeyPress}
|
||||
onKeyDown={this.handleArrowUp}
|
||||
onHeightChange={this.resizePostHolder}
|
||||
messageText={this.state.messageText}
|
||||
createMessage='Write a message...'
|
||||
|
||||
@@ -5,6 +5,7 @@ var Client = require('../utils/client.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var Textbox = require('./textbox.jsx');
|
||||
var BrowserStore = require('../stores/browser_store.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
|
||||
export default class EditPostModal extends React.Component {
|
||||
constructor() {
|
||||
@@ -14,6 +15,7 @@ export default class EditPostModal extends React.Component {
|
||||
this.handleEditInput = this.handleEditInput.bind(this);
|
||||
this.handleEditKeyPress = this.handleEditKeyPress.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: ''};
|
||||
}
|
||||
@@ -35,16 +37,15 @@ export default class EditPostModal extends React.Component {
|
||||
|
||||
Client.updatePost(updatedPost,
|
||||
function success() {
|
||||
AsyncClient.getPosts(this.state.channel_id);
|
||||
AsyncClient.getPosts(updatedPost.channel_id);
|
||||
window.scrollTo(0, 0);
|
||||
}.bind(this),
|
||||
},
|
||||
function error(err) {
|
||||
AsyncClient.dispatchError(err, 'updatePost');
|
||||
}
|
||||
);
|
||||
|
||||
$('#edit_post').modal('hide');
|
||||
$(this.state.refocusId).focus();
|
||||
}
|
||||
handleEditInput(editMessage) {
|
||||
this.setState({editText: editMessage});
|
||||
@@ -59,6 +60,18 @@ export default class EditPostModal extends React.Component {
|
||||
handleUserInput(e) {
|
||||
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() {
|
||||
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) {
|
||||
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')});
|
||||
});
|
||||
|
||||
$(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', function onShown() {
|
||||
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() {
|
||||
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');
|
||||
|
||||
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 {
|
||||
@@ -154,9 +171,9 @@ export default class NavbarDropdown extends React.Component {
|
||||
</li>
|
||||
);
|
||||
|
||||
this.state.teams.forEach((teamName) => {
|
||||
if (teamName !== this.props.teamName) {
|
||||
teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>{'Switch to ' + teamName}</a></li>);
|
||||
this.state.teams.forEach((team) => {
|
||||
if (team.name !== this.props.teamName) {
|
||||
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 ChannelStore = require('../stores/channel_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var PreferenceStore = require('../stores/preference_store.jsx');
|
||||
var UserProfile = require('./user_profile.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var Post = require('./post.jsx');
|
||||
@@ -105,6 +106,7 @@ export default class PostList extends React.Component {
|
||||
PostStore.clearUnseenDeletedPosts(this.props.channelId);
|
||||
PostStore.addChangeListener(this.onChange);
|
||||
UserStore.addStatusesChangeListener(this.onTimeChange);
|
||||
PreferenceStore.addChangeListener(this.onTimeChange);
|
||||
SocketStore.addChangeListener(this.onSocketChange);
|
||||
|
||||
const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
|
||||
@@ -156,6 +158,7 @@ export default class PostList extends React.Component {
|
||||
PostStore.removeChangeListener(this.onChange);
|
||||
UserStore.removeStatusesChangeListener(this.onTimeChange);
|
||||
SocketStore.removeChangeListener(this.onSocketChange);
|
||||
PreferenceStore.removeChangeListener(this.onTimeChange);
|
||||
$('body').off('click.userpopover');
|
||||
$(window).off('resize');
|
||||
var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var PreferenceStore = require('../stores/preference_store.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var SearchBox = require('./search_bar.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.onChangeAll = this.onChangeAll.bind(this);
|
||||
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
|
||||
|
||||
this.state = this.getStateFromStores();
|
||||
}
|
||||
@@ -43,6 +45,7 @@ export default class RhsThread extends React.Component {
|
||||
componentDidMount() {
|
||||
PostStore.addSelectedPostChangeListener(this.onChange);
|
||||
PostStore.addChangeListener(this.onChangeAll);
|
||||
PreferenceStore.addChangeListener(this.forceUpdateInfo);
|
||||
this.resize();
|
||||
$(window).resize(function resize() {
|
||||
this.resize();
|
||||
@@ -57,6 +60,16 @@ export default class RhsThread extends React.Component {
|
||||
componentWillUnmount() {
|
||||
PostStore.removeSelectedPostChangeListener(this.onChange);
|
||||
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() {
|
||||
var newState = this.getStateFromStores();
|
||||
@@ -174,6 +187,7 @@ export default class RhsThread extends React.Component {
|
||||
/>
|
||||
<div className='post-right__scroll'>
|
||||
<RootPost
|
||||
ref={rootPost.id}
|
||||
post={rootPost}
|
||||
commentCount={postsArray.length}
|
||||
/>
|
||||
|
||||
@@ -132,11 +132,7 @@ export default class Sidebar extends React.Component {
|
||||
SocketStore.addChangeListener(this.onSocketChange);
|
||||
PreferenceStore.addChangeListener(this.onChange);
|
||||
|
||||
AsyncClient.getDirectChannelPreferences();
|
||||
|
||||
if ($(window).width() > 768) {
|
||||
$('.nav-pills__container').perfectScrollbar();
|
||||
}
|
||||
$('.nav-pills__container').perfectScrollbar();
|
||||
|
||||
this.updateTitle();
|
||||
this.updateUnreadIndicators();
|
||||
|
||||
@@ -9,6 +9,7 @@ const ErrorStore = require('../stores/error_store.jsx');
|
||||
const Utils = require('../utils/utils.jsx');
|
||||
const Constants = require('../utils/constants.jsx');
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
const KeyCodes = Constants.KeyCodes;
|
||||
|
||||
export default class Textbox extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -148,8 +149,10 @@ export default class Textbox extends React.Component {
|
||||
this.doProcessMentions = true;
|
||||
}
|
||||
|
||||
if (e.keyCode === 8) {
|
||||
if (e.keyCode === KeyCodes.BACKSPACE) {
|
||||
this.handleBackspace(e);
|
||||
} else if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,5 +321,6 @@ Textbox.propTypes = {
|
||||
onUserInput: React.PropTypes.func.isRequired,
|
||||
onKeyPress: React.PropTypes.func.isRequired,
|
||||
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 DeveloperTab = require('./user_settings_developer.jsx');
|
||||
var IntegrationsTab = require('./user_settings_integrations.jsx');
|
||||
var DisplayTab = require('./user_settings_display.jsx');
|
||||
|
||||
export default class UserSettings extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -98,6 +99,17 @@ export default class UserSettings extends React.Component {
|
||||
/>
|
||||
</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/>;
|
||||
|
||||
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') {
|
||||
tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
|
||||
}
|
||||
tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'});
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -36,6 +36,7 @@ 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 AsyncClient = require('../utils/async_client.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
@@ -46,6 +47,8 @@ function setupChannelPage(props) {
|
||||
id: props.ChannelId
|
||||
});
|
||||
|
||||
AsyncClient.getAllPreferences();
|
||||
|
||||
// ChannelLoader must be rendered first
|
||||
ReactDOM.render(
|
||||
<ChannelLoader/>,
|
||||
|
||||
@@ -6,6 +6,7 @@ var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var BrowserStore = require('../stores/browser_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
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 MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
|
||||
var ADD_MENTION_EVENT = 'add_mention';
|
||||
var EDIT_POST_EVENT = 'edit_post';
|
||||
|
||||
class PostStoreClass extends EventEmitter {
|
||||
constructor() {
|
||||
@@ -75,6 +77,10 @@ class PostStoreClass extends EventEmitter {
|
||||
this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this);
|
||||
this.storeLatestUpdate = this.storeLatestUpdate.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() {
|
||||
this.emit(CHANGE_EVENT);
|
||||
@@ -148,6 +154,18 @@ class PostStoreClass extends EventEmitter {
|
||||
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() {
|
||||
var currentId = ChannelStore.getCurrentId();
|
||||
|
||||
@@ -212,6 +230,22 @@ class PostStoreClass extends EventEmitter {
|
||||
getPosts(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) {
|
||||
this.pStorePost(post);
|
||||
this.emitChange();
|
||||
@@ -446,6 +480,9 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
|
||||
case ActionTypes.RECIEVED_ADD_MENTION:
|
||||
PostStore.emitAddMention(action.id, action.username);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_EDIT_POST:
|
||||
PostStore.emitEditPost(action);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
@@ -120,3 +120,4 @@ class PreferenceStoreClass extends EventEmitter {
|
||||
|
||||
const PreferenceStore = new PreferenceStoreClass();
|
||||
export default PreferenceStore;
|
||||
window.PreferenceStore = PreferenceStore;
|
||||
|
||||
@@ -639,16 +639,15 @@ export function getMyTeam() {
|
||||
);
|
||||
}
|
||||
|
||||
export function getDirectChannelPreferences() {
|
||||
if (isCallInProgress('getDirectChannelPreferences')) {
|
||||
export function getAllPreferences() {
|
||||
if (isCallInProgress('getAllPreferences')) {
|
||||
return;
|
||||
}
|
||||
|
||||
callTracker.getDirectChannelPreferences = utils.getTimestamp();
|
||||
client.getPreferenceCategory(
|
||||
Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
||||
callTracker.getAllPreferences = utils.getTimestamp();
|
||||
client.getAllPreferences(
|
||||
(data, textStatus, xhr) => {
|
||||
callTracker.getDirectChannelPreferences = 0;
|
||||
callTracker.getAllPreferences = 0;
|
||||
|
||||
if (xhr.status === 304 || !data) {
|
||||
return;
|
||||
@@ -660,8 +659,8 @@ export function getDirectChannelPreferences() {
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
callTracker.getDirectChannelPreferences = 0;
|
||||
dispatchError(err, 'getDirectChannelPreferences');
|
||||
callTracker.getAllPreferences = 0;
|
||||
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) {
|
||||
$.ajax({
|
||||
url: `/api/v1/preferences/${category}`,
|
||||
|
||||
@@ -17,6 +17,7 @@ module.exports = {
|
||||
|
||||
RECIEVED_POSTS: null,
|
||||
RECIEVED_POST: null,
|
||||
RECIEVED_EDIT_POST: null,
|
||||
RECIEVED_SEARCH: null,
|
||||
RECIEVED_POST_SELECTED: null,
|
||||
RECIEVED_MENTION_DATA: null,
|
||||
@@ -287,6 +288,16 @@ module.exports = {
|
||||
}
|
||||
],
|
||||
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 ChannelStore = require('../stores/channel_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var PreferenceStore = require('../stores/preference_store.jsx');
|
||||
var TeamStore = require('../stores/team_store.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
@@ -164,23 +165,29 @@ export function displayDate(ticks) {
|
||||
}
|
||||
|
||||
export function displayTime(ticks) {
|
||||
var d = new Date(ticks);
|
||||
var hours = d.getHours();
|
||||
var minutes = d.getMinutes();
|
||||
const d = new Date(ticks);
|
||||
let hours = d.getHours();
|
||||
let minutes = d.getMinutes();
|
||||
let ampm = '';
|
||||
|
||||
var ampm = 'AM';
|
||||
if (hours >= 12) {
|
||||
ampm = 'PM';
|
||||
}
|
||||
|
||||
hours = hours % 12;
|
||||
if (!hours) {
|
||||
hours = '12';
|
||||
}
|
||||
if (minutes <= 9) {
|
||||
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) {
|
||||
|
||||
@@ -93,7 +93,8 @@
|
||||
|
||||
.post-right__scroll {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.post-right-comment-time {
|
||||
|
||||
@@ -67,8 +67,9 @@
|
||||
}
|
||||
|
||||
.search-items-container {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.search-results-header {
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
.nav-pills__container {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.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.AddProp("from_webhook", "true")
|
||||
|
||||
if len(overrideUsername) != 0 && utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
|
||||
post.AddProp("override_username", overrideUsername)
|
||||
if utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
|
||||
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 {
|
||||
post.AddProp("override_icon_url", overrideIconUrl)
|
||||
if utils.Cfg.ServiceSettings.EnablePostIconOverride {
|
||||
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 {
|
||||
|
||||
Ссылка в новой задаче
Block a user