From 1ab7034574de2229d3cfc49391e6579db37a3064 Mon Sep 17 00:00:00 2001 From: Antti Ahti Date: Wed, 14 Oct 2015 18:27:03 +0300 Subject: [PATCH 01/24] Use team display name in team switcher menu - /teams/find_teams returns team objects instead of just team names - use display_name in UI instead of name in the team switch menu --- api/team.go | 8 +++----- api/team_test.go | 9 ++++++--- model/client.go | 2 +- web/react/components/navbar_dropdown.jsx | 13 ++++++++----- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/api/team.go b/api/team.go index 6aa5ec1bb2..7148caac3e 100644 --- a/api/team.go +++ b/api/team.go @@ -409,14 +409,12 @@ 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) + m[v.Id] = v } - w.Write([]byte(model.ArrayToJson(s))) + w.Write([]byte(model.TeamMapToJson(m))) } } diff --git a/api/team_test.go b/api/team_test.go index 9b701911b9..507f4252a3 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -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() } } diff --git a/model/client.go b/model/client.go index 77b0aaad28..19c99df729 100644 --- a/model/client.go +++ b/model/client.go @@ -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 } } diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 49d517419e..b3f8e44188 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -145,7 +145,7 @@ export default class NavbarDropdown extends React.Component { var teams = []; - if (this.state.teams.length > 1) { + if (Object.keys(this.state.teams).length > 1) { teams.push(
  • ); - this.state.teams.forEach((teamName) => { - if (teamName !== this.props.teamName) { - teams.push(
  • {'Switch to ' + teamName}
  • ); + for (let teamId in this.state.teams) { + if (this.state.teams.hasOwnProperty(teamId)) { + let team = this.state.teams[teamId]; + if (team.name !== this.props.teamName) { + teams.push(
  • {'Switch to ' + team.display_name}
  • ); + } } - }); + } } if (global.window.config.EnableTeamCreation === 'true') { From 731595c1a93e51bcd4e6034b088b53766883a9eb Mon Sep 17 00:00:00 2001 From: Antti Ahti Date: Wed, 14 Oct 2015 18:31:58 +0300 Subject: [PATCH 02/24] Fix typo (isTreamCreationAllowed -> isTeamCreationAllowed) --- api/team.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/api/team.go b/api/team.go index 7148caac3e..18acf33ce6 100644 --- a/api/team.go +++ b/api/team.go @@ -52,7 +52,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !isTreamCreationAllowed(c, email) { + if !isTeamCreationAllowed(c, email) { return } @@ -100,7 +100,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !isTreamCreationAllowed(c, team.Email) { + if !isTeamCreationAllowed(c, team.Email) { return } @@ -169,7 +169,7 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !isTreamCreationAllowed(c, teamSignup.Team.Email) { + if !isTeamCreationAllowed(c, teamSignup.Team.Email) { return } @@ -257,7 +257,7 @@ func CreateTeam(c *Context, team *model.Team) *model.Team { return nil } - if !isTreamCreationAllowed(c, team.Email) { + if !isTeamCreationAllowed(c, team.Email) { return nil } @@ -276,12 +276,12 @@ func CreateTeam(c *Context, team *model.Team) *model.Team { } } -func isTreamCreationAllowed(c *Context, email string) bool { +func isTeamCreationAllowed(c *Context, email string) bool { email = strings.ToLower(email) if !utils.Cfg.TeamSettings.EnableTeamCreation { - c.Err = model.NewAppError("isTreamCreationAllowed", "Team creation has been disabled. Please ask your systems administrator for details.", "") + c.Err = model.NewAppError("isTeamCreationAllowed", "Team creation has been disabled. Please ask your systems administrator for details.", "") return false } @@ -298,7 +298,7 @@ func isTreamCreationAllowed(c *Context, email string) bool { } if len(utils.Cfg.TeamSettings.RestrictCreationToDomains) > 0 && !matched { - c.Err = model.NewAppError("isTreamCreationAllowed", "Email must be from a specific domain (e.g. @example.com). Please ask your systems administrator for details.", "") + c.Err = model.NewAppError("isTeamCreationAllowed", "Email must be from a specific domain (e.g. @example.com). Please ask your systems administrator for details.", "") return false } From 8f96db4d0b52d0701398703364948e659f3ce34f Mon Sep 17 00:00:00 2001 From: Antti Ahti Date: Wed, 14 Oct 2015 19:27:19 +0300 Subject: [PATCH 03/24] Sanitize team data We don't want to expose sensitive data that might pose a security risk. --- api/team.go | 1 + model/team.go | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/api/team.go b/api/team.go index 18acf33ce6..f6038566a8 100644 --- a/api/team.go +++ b/api/team.go @@ -411,6 +411,7 @@ func findTeams(c *Context, w http.ResponseWriter, r *http.Request) { teams := result.Data.([]*model.Team) m := make(map[string]*model.Team) for _, v := range teams { + v.Sanitize() m[v.Id] = v } diff --git a/model/team.go b/model/team.go index c0f6524cd5..584c78f8d8 100644 --- a/model/team.go +++ b/model/team.go @@ -219,3 +219,9 @@ func CleanTeamName(s string) string { func (o *Team) PreExport() { } + +func (o *Team) Sanitize() { + o.Email = "" + o.Type = "" + o.AllowedDomains = "" +} From 76e278a13de936c09068ad1c2c2a16be0c419249 Mon Sep 17 00:00:00 2001 From: Stas Vovk Date: Wed, 14 Oct 2015 20:36:12 +0300 Subject: [PATCH 04/24] added display tab under account settings. added 24h time option --- web/react/components/post_list.jsx | 3 ++ .../user_settings/user_settings.jsx | 12 +++++++ .../user_settings/user_settings_modal.jsx | 1 + web/react/utils/constants.jsx | 3 +- web/react/utils/utils.jsx | 33 +++++++++++-------- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 643b38af5b..ebbe319b59 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -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 = $(React.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 = $(React.findDOMNode(this.refs.postlist)); diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx index 5ce9b6330a..15bf961d64 100644 --- a/web/react/components/user_settings/user_settings.jsx +++ b/web/react/components/user_settings/user_settings.jsx @@ -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 { /> ); + } else if (this.props.activeTab === 'display') { + return ( +
    + +
    + ); } return
    ; diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 19b97fc854..692fb26ee4 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -41,6 +41,7 @@ export default class UserSettingsModal extends React.Component { if (global.window.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 (
    = 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) { From 29a0c182ef376de8368552e7b472afc960c4ba8a Mon Sep 17 00:00:00 2001 From: Stas Vovk Date: Wed, 14 Oct 2015 21:39:33 +0300 Subject: [PATCH 05/24] added file with display settings --- .../user_settings/user_settings_display.jsx | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 web/react/components/user_settings/user_settings_display.jsx diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx new file mode 100644 index 0000000000..ec209c2187 --- /dev/null +++ b/web/react/components/user_settings/user_settings_display.jsx @@ -0,0 +1,168 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import { savePreferences } from '../../utils/client.jsx'; +import SettingItemMin from '../setting_item_min.jsx'; +import SettingItemMax from '../setting_item_max.jsx'; +import Constants from '../../utils/constants.jsx'; +import PreferenceStore from '../../stores/preference_store.jsx'; + +function getDisplayStateFromStores() { + const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'}); + + return {militaryTime: militaryTime.value}; +} + +export default class UserSettingsDisplay extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + this.handleClockRadio = this.handleClockRadio.bind(this); + this.updateSection = this.updateSection.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = getDisplayStateFromStores(); + } + handleSubmit() { + const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime); + + savePreferences([preference], + () => { + PreferenceStore.emitChange(); + this.updateSection(''); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + handleClockRadio(militaryTime) { + this.setState({militaryTime: militaryTime}); + } + updateSection(section) { + this.setState(getDisplayStateFromStores()); + this.props.updateSection(section); + } + handleClose() { + this.updateSection(''); + } + componentDidMount() { + $('#user_settings').on('hidden.bs.modal', this.handleClose); + } + componentWillUnmount() { + $('#user_settings').off('hidden.bs.modal', this.handleClose); + } + render() { + const serverError = this.state.serverError || null; + let clockSection; + if (this.props.activeSection === 'clock') { + let clockFormat = [false, false]; + if (this.state.militaryTime === 'true') { + clockFormat[1] = true; + } else { + clockFormat[0] = true; + } + + const handleUpdateClockSection = (e) => { + this.updateSection(''); + e.preventDefault(); + }; + + const inputs = [ +
    +
    + +
    +
    +
    + +
    +
    +

    {'Select how you prefer time displayed.'}
    +
    + ]; + + + clockSection = ( + + ); + } 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 = ( + + ); + } + + return ( +
    +
    + +

    + + {'Display Settings'} +

    +
    +
    +

    {'Display Settings'}

    +
    + {clockSection} +
    +
    +
    + ); + } +} + +UserSettingsDisplay.propTypes = { + user: React.PropTypes.object, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func, + activeSection: React.PropTypes.string +}; From 440faabc604fa6763515efd9009a7c276e140f09 Mon Sep 17 00:00:00 2001 From: Stas Vovk Date: Thu, 15 Oct 2015 00:05:39 +0300 Subject: [PATCH 06/24] fixed style guide error --- web/react/utils/utils.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index a5057b5ca8..8f01b3e33b 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -175,7 +175,7 @@ export function displayTime(ticks) { } const useMilitaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'}).value; - if(useMilitaryTime === 'false') { + if (useMilitaryTime === 'false') { ampm = ' AM'; if (hours >= 12) { ampm = ' PM'; From 059df3de0126f2a506b525f92911035eba02bbd3 Mon Sep 17 00:00:00 2001 From: Antti Ahti Date: Thu, 15 Oct 2015 08:41:52 +0300 Subject: [PATCH 07/24] Sort teams by display name First we need to convert the object to array, because objects cannot be sorted. --- web/react/components/navbar_dropdown.jsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index b3f8e44188..d4308ad727 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -11,7 +11,25 @@ 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 (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; + } else { + return 0; + } + }); + return {teams}; } export default class NavbarDropdown extends React.Component { From c0809bbc3fc7a9630659872850c313eb6df701d9 Mon Sep 17 00:00:00 2001 From: Antti Ahti Date: Thu, 15 Oct 2015 08:54:20 +0300 Subject: [PATCH 08/24] Treat teams as an array instead of object --- web/react/components/navbar_dropdown.jsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index d4308ad727..564a64f48c 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -163,7 +163,7 @@ export default class NavbarDropdown extends React.Component { var teams = []; - if (Object.keys(this.state.teams).length > 1) { + if (this.state.teams.length > 1) { teams.push(
  • ); - for (let teamId in this.state.teams) { - if (this.state.teams.hasOwnProperty(teamId)) { - let team = this.state.teams[teamId]; - if (team.name !== this.props.teamName) { - teams.push(
  • {'Switch to ' + team.display_name}
  • ); - } + this.state.teams.forEach((team) => { + if (team.name !== this.props.teamName) { + teams.push(
  • {'Switch to ' + team.display_name}
  • ); } - } + }); } if (global.window.config.EnableTeamCreation === 'true') { From 7abde676a1828714c71b25184fdd91a81c2d77a1 Mon Sep 17 00:00:00 2001 From: Antti Ahti Date: Thu, 15 Oct 2015 19:35:45 +0300 Subject: [PATCH 09/24] Fix lint --- web/react/components/navbar_dropdown.jsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 564a64f48c..dc41775c7b 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -15,19 +15,18 @@ function getStateFromStores() { let teamsObject = UserStore.getTeams(); for (let teamId in teamsObject) { if (teamsObject.hasOwnProperty(teamId)) { - teams.push(teamsObject[teamId]) + teams.push(teamsObject[teamId]); } } - teams.sort(function (teamA, teamB) { + teams.sort(function sortByDisplayName(teamA, teamB) { let teamADisplayName = teamA.display_name.toLowerCase(); let teamBDisplayName = teamB.display_name.toLowerCase(); if (teamADisplayName < teamBDisplayName) { - return -1 + return -1; } else if (teamADisplayName > teamBDisplayName) { return 1; - } else { - return 0; } + return 0; }); return {teams}; } From 2bd81ff379acb347534e4d6a1a27624e0262deb0 Mon Sep 17 00:00:00 2001 From: Stas Vovk Date: Thu, 15 Oct 2015 19:49:34 +0300 Subject: [PATCH 10/24] update posts time in the right sidebar when user changes time format --- web/react/components/rhs_thread.jsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx index 41fd74adbc..748cb16a83 100644 --- a/web/react/components/rhs_thread.jsx +++ b/web/react/components/rhs_thread.jsx @@ -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(); @@ -59,6 +62,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 { />
    From b438eefad1a4303aeb5ab60214204c074e260224 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 15 Oct 2015 14:52:59 -0400 Subject: [PATCH 11/24] Removed preference migration code --- api/preference.go | 51 ------------------------------------------ api/preference_test.go | 48 --------------------------------------- api/user.go | 43 ++++++++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 100 deletions(-) diff --git a/api/preference.go b/api/preference.go index 88cb132f88..3a05354732 100644 --- a/api/preference.go +++ b/api/preference.go @@ -52,61 +52,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"] diff --git a/api/preference_test.go b/api/preference_test.go index 318ce95826..dfdf8b5815 100644 --- a/api/preference_test.go +++ b/api/preference_test.go @@ -113,54 +113,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() diff --git a/api/user.go b/api/user.go index 146ede0154..ac33e81a18 100644 --- a/api/user.go +++ b/api/user.go @@ -198,7 +198,9 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err) } - fireAndForgetWelcomeEmail(result.Data.(*model.User).Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified) + fireAndForgetWelcomeEmail(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified) + + addDirectChannelsAndForget(ruser) if user.EmailVerified { if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { @@ -237,6 +239,45 @@ func fireAndForgetWelcomeEmail(userId, email, teamName, teamDisplayName, siteURL }() } +func addDirectChannelsAndForget(user *model.User) { + go func() { + var profiles map[string]*model.User + if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { + l4g.Error("Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v", user.Id, user.TeamId, result.Err.Error()) + return + } else { + profiles = result.Data.(map[string]*model.User) + } + + var preferences model.Preferences + + for id := range profiles { + if id == user.Id { + continue + } + + profile := profiles[id] + + preference := model.Preference{ + UserId: user.Id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, + Name: profile.Id, + Value: "true", + } + + preferences = append(preferences, preference) + + if len(preferences) >= 10 { + break + } + } + + if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil { + l4g.Error("Failed to add direct channel preferences for new user user_id=%s, eam_id=%s, err=%v", user.Id, user.TeamId, result.Err.Error()) + } + }() +} + func FireAndForgetVerifyEmail(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { From 4b99e1714029689f27ebf4cb078c60367b0594a4 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 15 Oct 2015 14:52:59 -0400 Subject: [PATCH 12/24] Removed preference migration code --- api/preference.go | 51 ------------------------------------------ api/preference_test.go | 48 --------------------------------------- api/user.go | 39 ++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 99 deletions(-) diff --git a/api/preference.go b/api/preference.go index 88cb132f88..3a05354732 100644 --- a/api/preference.go +++ b/api/preference.go @@ -52,61 +52,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"] diff --git a/api/preference_test.go b/api/preference_test.go index 318ce95826..dfdf8b5815 100644 --- a/api/preference_test.go +++ b/api/preference_test.go @@ -113,54 +113,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() diff --git a/api/user.go b/api/user.go index 146ede0154..1d628d3014 100644 --- a/api/user.go +++ b/api/user.go @@ -237,6 +237,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() { From 327b0b5a2119ae888c812f682b3934061b8f59bf Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 15 Oct 2015 15:09:40 -0400 Subject: [PATCH 13/24] Added an initial call to get all user preferences on page load --- api/preference.go | 11 +++++ api/preference_test.go | 58 +++++++++++++++++++++++++++ model/client.go | 9 +++++ store/sql_preference_store.go | 27 +++++++++++++ store/sql_preference_store_test.go | 48 ++++++++++++++++++++++ store/store.go | 1 + web/react/components/sidebar.jsx | 6 +-- web/react/pages/channel.jsx | 3 ++ web/react/stores/preference_store.jsx | 1 + web/react/utils/async_client.jsx | 15 ++++--- web/react/utils/client.jsx | 13 ++++++ 11 files changed, 179 insertions(+), 13 deletions(-) diff --git a/api/preference.go b/api/preference.go index 3a05354732..6d6ac1a7ff 100644 --- a/api/preference.go +++ b/api/preference.go @@ -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 { diff --git a/api/preference_test.go b/api/preference_test.go index dfdf8b5815..9d3db9e2fc 100644 --- a/api/preference_test.go +++ b/api/preference_test.go @@ -9,6 +9,64 @@ 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") + + if result, err := Client.GetAllPreferences(); err != nil { + t.Fatal(err) + } else if data := result.Data.(model.Preferences); len(data) != 0 { + t.Fatal("received the wrong number of preferences") + } +} + func TestSetPreferences(t *testing.T) { Setup() diff --git a/model/client.go b/model/client.go index 77b0aaad28..ca1e37479a 100644 --- a/model/client.go +++ b/model/client.go @@ -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 diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go index 46cef38b15..7a0fe2f2e7 100644 --- a/store/sql_preference_store.go +++ b/store/sql_preference_store.go @@ -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 +} diff --git a/store/sql_preference_store_test.go b/store/sql_preference_store_test.go index 76b1bcb17e..e68203cc33 100644 --- a/store/sql_preference_store_test.go +++ b/store/sql_preference_store_test.go @@ -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") + } + } + } +} diff --git a/store/store.go b/store/store.go index b436a5c403..de335cc2b5 100644 --- a/store/store.go +++ b/store/store.go @@ -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 } diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index e7d90bb881..5ab0c4f9e3 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -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(); diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 698213eef5..19e158aa6d 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -37,6 +37,7 @@ var RegisterAppModal = require('../components/register_app_modal.jsx'); var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx'); var TeamStore = require('../stores/team_store.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; @@ -54,6 +55,8 @@ function setupChannelPage(props) { id: props.TeamId }); + AsyncClient.getAllPreferences(); + // ChannelLoader must be rendered first React.render( , diff --git a/web/react/stores/preference_store.jsx b/web/react/stores/preference_store.jsx index d71efa10fc..f630d150de 100644 --- a/web/react/stores/preference_store.jsx +++ b/web/react/stores/preference_store.jsx @@ -120,3 +120,4 @@ class PreferenceStoreClass extends EventEmitter { const PreferenceStore = new PreferenceStoreClass(); export default PreferenceStore; +window.PreferenceStore = PreferenceStore; diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 1bf8a6fee9..b22d7237e2 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -638,16 +638,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; @@ -659,8 +658,8 @@ export function getDirectChannelPreferences() { }); }, (err) => { - callTracker.getDirectChannelPreferences = 0; - dispatchError(err, 'getDirectChannelPreferences'); + callTracker.getAllPreferences = 0; + dispatchError(err, 'getAllPreferences'); } ); } diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 76a4028552..f6aee362c2 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -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}`, From 551b07960ed8ec36c24341f96f2b06fee25ceab3 Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Thu, 15 Oct 2015 02:13:48 +0200 Subject: [PATCH 14/24] PLT-74: Enable Up Arrow keyboard shortcut to edit your last message --- web/react/components/create_post.jsx | 23 ++++++++++++++- web/react/components/edit_post_modal.jsx | 22 ++++++++++++++ web/react/components/textbox.jsx | 8 +++-- web/react/stores/post_store.jsx | 37 ++++++++++++++++++++++++ web/react/utils/constants.jsx | 10 +++++++ 5 files changed, 97 insertions(+), 3 deletions(-) diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 6203e567a8..ed6818dc81 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -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,24 @@ 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); + var type = (lastPost.root_id && lastPost.root_id.length > 0) ? 'Comment' : 'Post'; + + AppDispatcher.handleViewAction({ + type: ActionTypes.RECIEVED_EDIT_POST, + refoucsId: '#post_textbox', + title: type, + message: lastPost.message, + lastPostId: lastPost.id, + channelId: lastPost.channel_id + }); + } + } render() { let serverError = null; if (this.state.serverError) { @@ -336,6 +356,7 @@ export default class CreatePost extends React.Component {
    ); diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx index d51fb95239..86bb42f62c 100644 --- a/web/react/components/textbox.jsx +++ b/web/react/components/textbox.jsx @@ -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 }; diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index d8da480000..8609d8bbfb 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -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: } }); diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index cee2ec1146..b8200db54d 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -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, @@ -289,5 +290,14 @@ module.exports = { ], Preferences: { CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show' + }, + KeyCodes: { + UP: 38, + DOWN: 40, + LEFT: 37, + RIGHT: 39, + BACKSPACE: 8, + ENTER: 13, + ESCAPE: 27 } }; From fcce3168b2776af6baf87096704465fc71fe28ac Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Thu, 15 Oct 2015 21:22:08 +0200 Subject: [PATCH 15/24] PLT-74: Enable Up Arrow keyboard shortcut to edit your last message - fix bug where channel_id was not set if update_post ajax was "too fast" - fix bug js error if there is no last post, i.e. empty channel - renamed lastPostId to postId so it has a nicer api to be triggered from everywhere - fix a typo - automatically focus textarea on modal open --- web/react/components/create_post.jsx | 7 +++++-- web/react/components/edit_post_modal.jsx | 14 +++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index ed6818dc81..2581bdccae 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -300,14 +300,17 @@ export default class CreatePost extends React.Component { 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, - refoucsId: '#post_textbox', + refocusId: '#post_textbox', title: type, message: lastPost.message, - lastPostId: lastPost.id, + postId: lastPost.id, channelId: lastPost.channel_id }); } diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index 38d31d66f7..90d9696e7d 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -37,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}); @@ -90,6 +89,15 @@ export default class EditPostModal extends React.Component { $(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); From 218af22936f55358f3b31b618e41004c0e2142a3 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 15 Oct 2015 15:39:50 -0400 Subject: [PATCH 16/24] Changed preference saving to use the master database --- store/sql_preference_store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go index 46cef38b15..dbbe201194 100644 --- a/store/sql_preference_store.go +++ b/store/sql_preference_store.go @@ -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 { From d549d240a8f00ccb19d620a4456c41b48cbe88e2 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 15 Oct 2015 16:16:32 -0400 Subject: [PATCH 17/24] Updated node.js source path in Ubuntu dev setup --- doc/developer/Setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/developer/Setup.md b/doc/developer/Setup.md index afaef4ee46..d806b7a9ba 100644 --- a/doc/developer/Setup.md +++ b/doc/developer/Setup.md @@ -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` From c3115c5b2dec9b79bc1b5e3607b942ce7a1d9fbc Mon Sep 17 00:00:00 2001 From: lfbrock Date: Thu, 15 Oct 2015 17:23:44 -0400 Subject: [PATCH 18/24] Added known issue to v1.1 and added new section for v1.2 --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eac1e61e53..916dc94cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ # Mattermost Changelog -## UNDER DEVELOPMENT - Release v1.1.0 +## UNDER DEVELOPMENT Release v1.2.0 The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the product's `master` branch to note key changes committed to master and are on their way to the next stable release. When a stable release is pushed the "UNDER DEVELOPMENT" heading is removed from the final changelog of the release. -- **Final release anticipated:** 2015-10-16 +- **Final release anticipated:** 2015-11-16 + +## Release v1.1.0 + +Released: 2015-10-16 ### Release Highlights @@ -44,6 +48,10 @@ 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 + +- Uploading a .flac file breaks the file previewer on iOS + ### Contributors Many thanks to our external contributors. In no particular order: From 282f9ebfb756cabfed5a389c62383ef1bdcf541e Mon Sep 17 00:00:00 2001 From: it33 Date: Thu, 15 Oct 2015 14:30:43 -0700 Subject: [PATCH 19/24] Update CHANGELOG.md Adding known issue around Slack import --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 916dc94cb1..425902cf01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Messaging and Notifications ### Known Issues +- Slack import is unstable due to change in Slack export format - Uploading a .flac file breaks the file previewer on iOS ### Contributors From 50a8e2bd4fa53c6bb2f12a03237be4fbaf259617 Mon Sep 17 00:00:00 2001 From: it33 Date: Thu, 15 Oct 2015 23:18:08 -0700 Subject: [PATCH 20/24] Update Troubleshooting.md --- doc/install/Troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/Troubleshooting.md b/doc/install/Troubleshooting.md index 8d82100d8c..b87663ab35 100644 --- a/doc/install/Troubleshooting.md +++ b/doc/install/Troubleshooting.md @@ -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. From 12f47b10606b88ab0fbc16f1800ffeffe6eb19bf Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Fri, 16 Oct 2015 12:37:51 +0500 Subject: [PATCH 21/24] Adding momentum scrolling on IOS --- web/sass-files/sass/partials/_post_right.scss | 3 ++- web/sass-files/sass/partials/_search.scss | 3 ++- web/sass-files/sass/partials/_sidebar--left.scss | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss index 9557d75704..91f9355de9 100644 --- a/web/sass-files/sass/partials/_post_right.scss +++ b/web/sass-files/sass/partials/_post_right.scss @@ -93,7 +93,8 @@ .post-right__scroll { position: relative; - overflow: auto; + overflow: scroll; + -webkit-overflow-scrolling: touch; } .post-right-comment-time { diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss index ca90ec46d1..2f15a445f1 100644 --- a/web/sass-files/sass/partials/_search.scss +++ b/web/sass-files/sass/partials/_search.scss @@ -67,8 +67,9 @@ } .search-items-container { - overflow: auto; position: relative; + overflow: scroll; + -webkit-overflow-scrolling: touch; } .search-results-header { diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index 831c19cffc..585a51f080 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -47,7 +47,8 @@ .nav-pills__container { height: 100%; position: relative; - overflow: auto; + overflow: scroll; + -webkit-overflow-scrolling: touch; } .nav-pills__unread-indicator { From 58c33f01d563d729e5fa5f8f037369210f8a1962 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Fri, 16 Oct 2015 08:16:34 -0400 Subject: [PATCH 22/24] Add default username and icon to incoming webhooks. --- model/webhook.go | 5 +++++ web/static/images/webhook_icon.jpg | Bin 0 -> 68190 bytes web/web.go | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 web/static/images/webhook_icon.jpg diff --git a/model/webhook.go b/model/webhook.go index 3bf034908a..9b9969b968 100644 --- a/model/webhook.go +++ b/model/webhook.go @@ -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"` diff --git a/web/static/images/webhook_icon.jpg b/web/static/images/webhook_icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..af5303421d2d44d92bd1cb8ad5166fd19b7fbebb GIT binary patch literal 68190 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

    1tj-HAdT~xcUQ5 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 Date: Fri, 16 Oct 2015 09:55:50 -0400 Subject: [PATCH 23/24] Fixed preference test that was failing --- api/preference_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/preference_test.go b/api/preference_test.go index 9d3db9e2fc..eaa92fe471 100644 --- a/api/preference_test.go +++ b/api/preference_test.go @@ -60,9 +60,10 @@ func TestGetAllPreferences(t *testing.T) { 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) != 0 { + } else if data := result.Data.(model.Preferences); len(data) != 1 { t.Fatal("received the wrong number of preferences") } } From 3c593af598b2bb986244099ed1b9a46ed3837911 Mon Sep 17 00:00:00 2001 From: it33 Date: Fri, 16 Oct 2015 07:19:18 -0700 Subject: [PATCH 24/24] Adding single-line Docker install --- doc/install/Docker-Single-Container.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/install/Docker-Single-Container.md b/doc/install/Docker-Single-Container.md index 4b952cd710..7c0784ad0c 100644 --- a/doc/install/Docker-Single-Container.md +++ b/doc/install/Docker-Single-Container.md @@ -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/