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

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

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

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

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

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

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

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

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

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

После

Ширина:  |  Высота:  |  Размер: 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.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 {