Этот коммит содержится в:
=Corey Hulen
2015-10-16 09:12:30 -07:00
родитель 6a1755d2e3 9c045f0d4b
Коммит 8b2d7f391a
37 изменённых файлов: 642 добавлений и 165 удалений

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@@ -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 Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 67 KiB

Просмотреть файл

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