diff options
28 files changed, 411 insertions, 238 deletions
diff --git a/api/command.go b/api/command.go index a8573cdcc..bebe6629c 100644 --- a/api/command.go +++ b/api/command.go @@ -4,6 +4,7 @@ package api import ( + "crypto/tls" "fmt" "io/ioutil" "net/http" @@ -52,6 +53,8 @@ func InitCommand(r *mux.Router) { sr.Handle("/test", ApiAppHandler(testCommand)).Methods("POST") sr.Handle("/test", ApiAppHandler(testCommand)).Methods("GET") + sr.Handle("/test_e", ApiAppHandler(testEphemeralCommand)).Methods("POST") + sr.Handle("/test_e", ApiAppHandler(testEphemeralCommand)).Methods("GET") } func listCommands(c *Context, w http.ResponseWriter, r *http.Request) { @@ -107,9 +110,8 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { provider := GetCommandProvider(trigger) if provider != nil { - response := provider.DoCommand(c, channelId, message) - handleResponse(c, w, response, channelId) + handleResponse(c, w, response, channelId, provider.GetCommand(c)) return } else { chanChan := Srv.Store.Channel().Get(channelId) @@ -172,7 +174,11 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { method = "GET" } - client := &http.Client{} + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + client := &http.Client{Transport: tr} + req, _ := http.NewRequest(method, cmd.URL, strings.NewReader(p.Encode())) req.Header.Set("Accept", "application/json") if cmd.Method == model.COMMAND_METHOD_POST { @@ -187,7 +193,7 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { if response == nil { c.Err = model.NewLocAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": trigger}, "") } else { - handleResponse(c, w, response, channelId) + handleResponse(c, w, response, channelId, cmd) } } else { body, _ := ioutil.ReadAll(resp.Body) @@ -205,21 +211,41 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = model.NewLocAppError("command", "api.command.execute_command.not_found.app_error", map[string]interface{}{"Trigger": trigger}, "") } -func handleResponse(c *Context, w http.ResponseWriter, response *model.CommandResponse, channelId string) { +func handleResponse(c *Context, w http.ResponseWriter, response *model.CommandResponse, channelId string, cmd *model.Command) { + + post := &model.Post{} + post.ChannelId = channelId + post.AddProp("from_webhook", "true") + + if utils.Cfg.ServiceSettings.EnablePostUsernameOverride { + if len(cmd.Username) != 0 { + post.AddProp("override_username", cmd.Username) + } else { + post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME) + } + } + + if utils.Cfg.ServiceSettings.EnablePostIconOverride { + if len(cmd.IconURL) != 0 { + post.AddProp("override_icon_url", cmd.IconURL) + } else { + post.AddProp("override_icon_url", model.DEFAULT_WEBHOOK_ICON) + } + } + if response.ResponseType == model.COMMAND_RESPONSE_TYPE_IN_CHANNEL { - post := &model.Post{} - post.ChannelId = channelId post.Message = response.Text if _, err := CreatePost(c, post, true); err != nil { c.Err = model.NewLocAppError("command", "api.command.execute_command.save.app_error", nil, "") } } else if response.ResponseType == model.COMMAND_RESPONSE_TYPE_EPHEMERAL { - // post := &model.Post{} - // post.ChannelId = channelId - // post.Message = "TODO_EPHEMERAL: " + response.Text - // if _, err := CreatePost(c, post, true); err != nil { - // c.Err = model.NewLocAppError("command", "api.command.execute_command.save.app_error", nil, "") - // } + post.Message = response.Text + post.CreateAt = model.GetMillis() + SendEphemeralPost( + c.Session.TeamId, + c.Session.UserId, + post, + ) } w.Write([]byte(response.ToJson())) @@ -399,3 +425,23 @@ func testCommand(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(rc.ToJson())) } + +func testEphemeralCommand(c *Context, w http.ResponseWriter, r *http.Request) { + r.ParseForm() + + msg := "" + if r.Method == "POST" { + msg = msg + "\ntoken=" + r.FormValue("token") + msg = msg + "\nteam_domain=" + r.FormValue("team_domain") + } else { + body, _ := ioutil.ReadAll(r.Body) + msg = string(body) + } + + rc := &model.CommandResponse{ + Text: "test command response " + msg, + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } + + w.Write([]byte(rc.ToJson())) +} diff --git a/api/post.go b/api/post.go index c17da262f..fadabd66e 100644 --- a/api/post.go +++ b/api/post.go @@ -4,6 +4,7 @@ package api import ( + "crypto/tls" "fmt" l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" @@ -401,7 +402,10 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team p.Set("text", post.Message) p.Set("trigger_word", firstWord) - client := &http.Client{} + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + client := &http.Client{Transport: tr} for _, url := range hook.CallbackURLs { go func(url string) { @@ -682,7 +686,10 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName } - httpClient := http.Client{} + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + httpClient := &http.Client{Transport: tr} request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+"/api/v1/send_push", strings.NewReader(msg.ToJson())) l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message) diff --git a/api/user.go b/api/user.go index 9926f3ff3..507c83d28 100644 --- a/api/user.go +++ b/api/user.go @@ -5,6 +5,7 @@ package api import ( "bytes" + "crypto/tls" b64 "encoding/base64" "fmt" l4g "github.com/alecthomas/log4go" @@ -1960,7 +1961,10 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) p.Set("redirect_uri", redirectUri) - client := &http.Client{} + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + client := &http.Client{Transport: tr} req, _ := http.NewRequest("POST", sso.TokenEndpoint, strings.NewReader(p.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") diff --git a/api/webhook.go b/api/webhook.go index 3906d09be..c0f8ea506 100644 --- a/api/webhook.go +++ b/api/webhook.go @@ -238,7 +238,7 @@ func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) { } func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { + if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { c.Err = model.NewLocAppError("deleteOutgoingHook", "api.webhook.delete_outgoing.disabled.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented return diff --git a/config/config.json b/config/config.json index 5ed05fecd..2795546f8 100644 --- a/config/config.json +++ b/config/config.json @@ -14,6 +14,7 @@ "EnableTesting": false, "EnableDeveloper": false, "EnableSecurityFixAlert": true, + "EnableInsecureOutgoingConnections": false, "SessionLengthWebInDays": 30, "SessionLengthMobileInDays": 30, "SessionLengthSSOInDays": 30, @@ -112,4 +113,4 @@ "TokenEndpoint": "", "UserApiEndpoint": "" } -}
\ No newline at end of file +} diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json index e831bbb3a..6a1290189 100644 --- a/docker/dev/config_docker.json +++ b/docker/dev/config_docker.json @@ -14,6 +14,7 @@ "EnableTesting": false, "EnableDeveloper": false, "EnableSecurityFixAlert": true, + "EnableInsecureOutgoingConnections": false, "SessionLengthWebInDays" : 30, "SessionLengthMobileInDays" : 30, "SessionLengthSSOInDays" : 30, diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json index e831bbb3a..6a1290189 100644 --- a/docker/local/config_docker.json +++ b/docker/local/config_docker.json @@ -14,6 +14,7 @@ "EnableTesting": false, "EnableDeveloper": false, "EnableSecurityFixAlert": true, + "EnableInsecureOutgoingConnections": false, "SessionLengthWebInDays" : 30, "SessionLengthMobileInDays" : 30, "SessionLengthSSOInDays" : 30, diff --git a/i18n/es.json b/i18n/es.json index 9512bd929..6f9c7499b 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -245,7 +245,7 @@ }, { "id": "api.command.admin_only.app_error", - "translation": "Las ingtegraciones solo pueden ser utilizadas por adminitradores." + "translation": "Las integraciones han sido limitadas a los adminitradores." }, { "id": "api.command.delete.app_error", diff --git a/model/config.go b/model/config.go index acb525abf..aa3dd3586 100644 --- a/model/config.go +++ b/model/config.go @@ -24,26 +24,27 @@ const ( ) type ServiceSettings struct { - ListenAddress string - MaximumLoginAttempts int - SegmentDeveloperKey string - GoogleDeveloperKey string - EnableOAuthServiceProvider bool - EnableIncomingWebhooks bool - EnableOutgoingWebhooks bool - EnableCommands *bool - EnableOnlyAdminIntegrations *bool - EnablePostUsernameOverride bool - EnablePostIconOverride bool - EnableTesting bool - EnableDeveloper *bool - EnableSecurityFixAlert *bool - SessionLengthWebInDays *int - SessionLengthMobileInDays *int - SessionLengthSSOInDays *int - SessionCacheInMinutes *int - WebsocketSecurePort *int - WebsocketPort *int + ListenAddress string + MaximumLoginAttempts int + SegmentDeveloperKey string + GoogleDeveloperKey string + EnableOAuthServiceProvider bool + EnableIncomingWebhooks bool + EnableOutgoingWebhooks bool + EnableCommands *bool + EnableOnlyAdminIntegrations *bool + EnablePostUsernameOverride bool + EnablePostIconOverride bool + EnableTesting bool + EnableDeveloper *bool + EnableSecurityFixAlert *bool + EnableInsecureOutgoingConnections *bool + SessionLengthWebInDays *int + SessionLengthMobileInDays *int + SessionLengthSSOInDays *int + SessionCacheInMinutes *int + WebsocketSecurePort *int + WebsocketPort *int } type SSOSettings struct { @@ -164,7 +165,7 @@ type LdapSettings struct { UsernameAttribute *string IdAttribute *string - // Advansed + // Advanced QueryTimeout *int } @@ -252,6 +253,11 @@ func (o *Config) SetDefaults() { *o.ServiceSettings.EnableSecurityFixAlert = true } + if o.ServiceSettings.EnableInsecureOutgoingConnections == nil { + o.ServiceSettings.EnableInsecureOutgoingConnections = new(bool) + *o.ServiceSettings.EnableInsecureOutgoingConnections = false + } + if o.TeamSettings.RestrictTeamNames == nil { o.TeamSettings.RestrictTeamNames = new(bool) *o.TeamSettings.RestrictTeamNames = true diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx index 2cc68d1ed..f232d4633 100644 --- a/web/react/components/admin_console/service_settings.jsx +++ b/web/react/components/admin_console/service_settings.jsx @@ -75,6 +75,7 @@ class ServiceSettings extends React.Component { config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked; config.ServiceSettings.EnableDeveloper = ReactDOM.findDOMNode(this.refs.EnableDeveloper).checked; config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked; + config.ServiceSettings.EnableInsecureOutgoingConnections = ReactDOM.findDOMNode(this.refs.EnableInsecureOutgoingConnections).checked; config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked; config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked; @@ -720,6 +721,53 @@ class ServiceSettings extends React.Component { <div className='form-group'> <label className='control-label col-sm-4' + htmlFor='EnableInsecureOutgoingConnections' + > + <FormattedMessage + id='admin.service.insecureTlsTitle' + defaultMessage='Enable Insecure Outgoing Connections: ' + /> + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='EnableInsecureOutgoingConnections' + value='true' + ref='EnableInsecureOutgoingConnections' + defaultChecked={this.props.config.ServiceSettings.EnableInsecureOutgoingConnections} + onChange={this.handleChange} + /> + <FormattedMessage + id='admin.service.true' + defaultMessage='true' + /> + </label> + <label className='radio-inline'> + <input + type='radio' + name='EnableInsecureOutgoingConnections' + value='false' + defaultChecked={!this.props.config.ServiceSettings.EnableInsecureOutgoingConnections} + onChange={this.handleChange} + /> + <FormattedMessage + id='admin.service.false' + defaultMessage='false' + /> + </label> + <p className='help-text'> + <FormattedMessage + id='admin.service.insecureTlsDesc' + defaultMessage='When true, any outgoing HTTPS requests will accept unverified, self-signed certificates. For example, outgoing webhooks to a server with a self-signed TLS certificate, using any domain, will be allowed. Note that this makes these connections susceptible to man-in-the-middle attacks.' + /> + </p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' htmlFor='SessionLengthWebInDays' > <FormattedMessage @@ -896,4 +944,4 @@ ServiceSettings.propTypes = { config: React.PropTypes.object }; -export default injectIntl(ServiceSettings);
\ No newline at end of file +export default injectIntl(ServiceSettings); diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx index 537055641..ac750614b 100644 --- a/web/react/components/setting_item_max.jsx +++ b/web/react/components/setting_item_max.jsx @@ -46,9 +46,14 @@ export default class SettingItemMax extends React.Component { widthClass = 'col-sm-9 col-sm-offset-3'; } + let title; + if (this.props.title) { + title = <li className='col-sm-12 section-title'>{this.props.title}</li>; + } + return ( <ul className='section-max form-horizontal'> - <li className='col-sm-12 section-title'>{this.props.title}</li> + {title} <li className={widthClass}> <ul className='setting-list'> <li className='setting-list-item'> diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 9116dd938..2d88a3650 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -222,7 +222,7 @@ class CustomThemeChooser extends React.Component { } else { elements.push( <div - className='col-sm-4 form-group' + className='col-sm-4 form-group element' key={'custom-theme-key' + index} > <label className='custom-label'>{formatMessage(messages[element.id])}</label> @@ -265,8 +265,8 @@ class CustomThemeChooser extends React.Component { ); return ( - <div> - <div className='row form-group'> + <div className='appearance-section'> + <div className='theme-elements row form-group'> {elements} </div> <div className='row'> @@ -283,4 +283,4 @@ CustomThemeChooser.propTypes = { updateTheme: React.PropTypes.func.isRequired }; -export default injectIntl(CustomThemeChooser);
\ No newline at end of file +export default injectIntl(CustomThemeChooser); diff --git a/web/react/components/user_settings/manage_command_hooks.jsx b/web/react/components/user_settings/manage_command_hooks.jsx index b2fc0a4e1..d23d2957e 100644 --- a/web/react/components/user_settings/manage_command_hooks.jsx +++ b/web/react/components/user_settings/manage_command_hooks.jsx @@ -530,23 +530,19 @@ export default class ManageCommandCmds extends React.Component { /> </label> <div className='padding-top'> - <label> - <input - type='checkbox' - checked={this.state.cmd.auto_complete} - onChange={this.updateAutoComplete} - /> - <FormattedMessage - id='user.settings.cmds.auto_complete_desc_desc' - defaultMessage='A short description of what this commands does' - /> - </label> - </div> - <div className='padding-top'> - <FormattedMessage - id='user.settings.cmds.auto_complete_help' - defaultMessage='Show this command in autocomplete list.' - /> + <div className='checkbox'> + <label> + <input + type='checkbox' + checked={this.state.cmd.auto_complete} + onChange={this.updateAutoComplete} + /> + <FormattedMessage + id='user.settings.cmds.auto_complete_help' + defaultMessage=' Show this command in autocomplete list' + /> + </label> + </div> </div> </div> <div className='padding-top x2'> @@ -565,12 +561,6 @@ export default class ManageCommandCmds extends React.Component { placeholder={this.props.intl.formatMessage(holders.addAutoCompleteDescPlaceholder)} /> </div> - <div className='padding-top'> - <FormattedMessage - id='user.settings.cmds.auto_complete_desc_desc' - defaultMessage='A short description of what this commands does' - /> - </div> </div> <div className='padding-top x2'> <label className='control-label'> @@ -649,7 +639,7 @@ export default class ManageCommandCmds extends React.Component { </div> {addError} </div> - <div className='padding-top padding-bottom'> + <div className='padding-top x2 padding-bottom'> <a className={'btn btn-sm btn-primary'} href='#' diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx index 9889bff5c..80ff8c4de 100644 --- a/web/react/components/user_settings/premade_theme_chooser.jsx +++ b/web/react/components/user_settings/premade_theme_chooser.jsx @@ -45,7 +45,7 @@ export default class PremadeThemeChooser extends React.Component { } return ( - <div className='row'> + <div className='row appearance-section'> {premadeThemes} </div> ); diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx index 54d98bbde..4da51fa5f 100644 --- a/web/react/components/user_settings/user_settings.jsx +++ b/web/react/components/user_settings/user_settings.jsx @@ -6,7 +6,6 @@ import * as utils from '../../utils/utils.jsx'; import NotificationsTab from './user_settings_notifications.jsx'; import SecurityTab from './user_settings_security.jsx'; import GeneralTab from './user_settings_general.jsx'; -import AppearanceTab from './user_settings_appearance.jsx'; import DeveloperTab from './user_settings_developer.jsx'; import IntegrationsTab from './user_settings_integrations.jsx'; import DisplayTab from './user_settings_display.jsx'; @@ -85,21 +84,6 @@ export default class UserSettings extends React.Component { /> </div> ); - } else if (this.props.activeTab === 'appearance') { - return ( - <div> - <AppearanceTab - ref='activeTab' - activeSection={this.props.activeSection} - updateSection={this.props.updateSection} - updateTab={this.props.updateTab} - closeModal={this.props.closeModal} - collapseModal={this.props.collapseModal} - setEnforceFocus={this.props.setEnforceFocus} - setRequireConfirm={this.props.setRequireConfirm} - /> - </div> - ); } else if (this.props.activeTab === 'developer') { return ( <div> @@ -137,6 +121,8 @@ export default class UserSettings extends React.Component { updateTab={this.props.updateTab} closeModal={this.props.closeModal} collapseModal={this.props.collapseModal} + setEnforceFocus={this.props.setEnforceFocus} + setRequireConfirm={this.props.setRequireConfirm} /> </div> ); diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx index 776bde442..4b11c06fb 100644 --- a/web/react/components/user_settings/user_settings_display.jsx +++ b/web/react/components/user_settings/user_settings_display.jsx @@ -1,15 +1,18 @@ // 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'; -const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES; -import PreferenceStore from '../../stores/preference_store.jsx'; import ManageLanguages from './manage_languages.jsx'; +import ThemeSetting from './user_settings_theme.jsx'; + +import PreferenceStore from '../../stores/preference_store.jsx'; import * as Utils from '../../utils/utils.jsx'; +import Constants from '../../utils/constants.jsx'; +const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES; + +import {savePreferences} from '../../utils/client.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; const holders = defineMessages({ @@ -452,6 +455,13 @@ class UserSettingsDisplay extends React.Component { /> </h3> <div className='divider-dark first'/> + <ThemeSetting + selected={this.props.activeSection === 'theme'} + updateSection={this.updateSection} + setRequireConfirm={this.props.setRequireConfirm} + setEnforceFocus={this.props.setEnforceFocus} + /> + <div className='divider-dark'/> {fontSection} <div className='divider-dark'/> {clockSection} @@ -472,7 +482,9 @@ UserSettingsDisplay.propTypes = { updateTab: React.PropTypes.func, activeSection: React.PropTypes.string, closeModal: React.PropTypes.func.isRequired, - collapseModal: React.PropTypes.func.isRequired + collapseModal: React.PropTypes.func.isRequired, + setRequireConfirm: React.PropTypes.func.isRequired, + setEnforceFocus: React.PropTypes.func.isRequired }; -export default injectIntl(UserSettingsDisplay);
\ No newline at end of file +export default injectIntl(UserSettingsDisplay); diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 2a0a90cf5..e0b72157b 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -2,9 +2,13 @@ // See License.txt for license information. import ConfirmModal from '../confirm_modal.jsx'; -const Modal = ReactBootstrap.Modal; -import SettingsSidebar from '../settings_sidebar.jsx'; import UserSettings from './user_settings.jsx'; +import SettingsSidebar from '../settings_sidebar.jsx'; + +import UserStore from '../../stores/user_store.jsx'; +import * as Utils from '../../utils/utils.jsx'; + +const Modal = ReactBootstrap.Modal; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; @@ -21,10 +25,6 @@ const holders = defineMessages({ id: 'user.settings.modal.notifications', defaultMessage: 'Notifications' }, - appearance: { - id: 'user.settings.modal.appearance', - defaultMessage: 'Appearance' - }, developer: { id: 'user.settings.modal.developer', defaultMessage: 'Developer' @@ -214,6 +214,12 @@ class UserSettingsModal extends React.Component { if (!skipConfirm && this.requireConfirm) { this.showConfirmModal(() => this.updateSection(section, true)); } else { + if (this.state.active_section === 'theme' && section !== 'theme') { + const user = UserStore.getCurrentUser(); + if (user.theme_props != null) { + Utils.applyTheme(user.theme_props); + } + } this.setState({active_section: section}); } } @@ -224,7 +230,6 @@ class UserSettingsModal extends React.Component { tabs.push({name: 'general', uiName: formatMessage(holders.general), icon: 'glyphicon glyphicon-cog'}); tabs.push({name: 'security', uiName: formatMessage(holders.security), icon: 'glyphicon glyphicon-lock'}); tabs.push({name: 'notifications', uiName: formatMessage(holders.notifications), icon: 'glyphicon glyphicon-exclamation-sign'}); - tabs.push({name: 'appearance', uiName: formatMessage(holders.appearance), icon: 'glyphicon glyphicon-wrench'}); if (global.window.mm_config.EnableOAuthServiceProvider === 'true') { tabs.push({name: 'developer', uiName: formatMessage(holders.developer), icon: 'glyphicon glyphicon-th'}); } @@ -294,4 +299,4 @@ UserSettingsModal.propTypes = { onModalDismissed: React.PropTypes.func.isRequired }; -export default injectIntl(UserSettingsModal);
\ No newline at end of file +export default injectIntl(UserSettingsModal); diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_theme.jsx index fb11dc81b..a0656feaa 100644 --- a/web/react/components/user_settings/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_theme.jsx @@ -1,8 +1,10 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import CustomThemeChooser from './custom_theme_chooser.jsx'; import PremadeThemeChooser from './premade_theme_chooser.jsx'; +import SettingItemMin from '../setting_item_min.jsx'; +import SettingItemMax from '../setting_item_max.jsx'; import UserStore from '../../stores/user_store.jsx'; @@ -12,11 +14,22 @@ import * as Utils from '../../utils/utils.jsx'; import Constants from '../../utils/constants.jsx'; -import {FormattedMessage} from 'mm-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; const ActionTypes = Constants.ActionTypes; -export default class UserSettingsAppearance extends React.Component { +const holders = defineMessages({ + themeTitle: { + id: 'user.settings.display.theme.title', + defaultMessage: 'Theme' + }, + themeDescribe: { + id: 'user.settings.display.theme.describe', + defaultMessage: 'Open to manage your theme' + } +}); + +export default class ThemeSetting extends React.Component { constructor(props) { super(props); @@ -34,16 +47,21 @@ export default class UserSettingsAppearance extends React.Component { componentDidMount() { UserStore.addChangeListener(this.onChange); - if (this.props.activeSection === 'theme') { + if (this.props.selected) { $(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); } } componentDidUpdate() { - if (this.props.activeSection === 'theme') { + if (this.props.selected) { $('.color-btn').removeClass('active-border'); $(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); } } + componentWillReceiveProps(nextProps) { + if (!this.props.selected && nextProps.selected) { + this.resetFields(); + } + } componentWillUnmount() { UserStore.removeChangeListener(this.onChange); } @@ -96,6 +114,7 @@ export default class UserSettingsAppearance extends React.Component { this.props.setRequireConfirm(false); this.originalTheme = Object.assign({}, this.state.theme); this.scrollToTop(); + this.props.updateSection(''); }, (err) => { var state = this.getStateFromStores(); @@ -149,6 +168,8 @@ export default class UserSettingsAppearance extends React.Component { this.props.setEnforceFocus(false); } render() { + const {formatMessage} = this.props.intl; + var serverError; if (this.state.serverError) { serverError = this.state.serverError; @@ -160,136 +181,121 @@ export default class UserSettingsAppearance extends React.Component { let premade; if (displayCustom) { custom = ( - <CustomThemeChooser - theme={this.state.theme} - updateTheme={this.updateTheme} - /> + <div key='customThemeChooser'> + <br/> + <CustomThemeChooser + theme={this.state.theme} + updateTheme={this.updateTheme} + /> + </div> ); } else { premade = ( - <PremadeThemeChooser - theme={this.state.theme} - updateTheme={this.updateTheme} - /> + <div key='premadeThemeChooser'> + <br/> + <PremadeThemeChooser + theme={this.state.theme} + updateTheme={this.updateTheme} + /> + </div> ); } - const themeUI = ( - <div className='section-max appearance-section'> - <div className='col-sm-12'> - <div className='radio'> - <label> - <input type='radio' - checked={!displayCustom} - onChange={this.updateType.bind(this, 'premade')} - /> - <FormattedMessage - id='user.settings.appearance.themeColors' - defaultMessage='Theme Colors' - /> - </label> - <br/> - </div> - {premade} - <div className='radio'> - <label> - <input type='radio' - checked={displayCustom} - onChange={this.updateType.bind(this, 'custom')} - /> - <FormattedMessage - id='user.settings.appearance.customTheme' - defaultMessage='Custom Theme' - /> - </label> - <br/> - </div> - {custom} - <hr /> - {serverError} - <a - className='btn btn-sm btn-primary' - href='#' - onClick={this.submitTheme} - > - <FormattedMessage - id='user.settings.appearance.save' - defaultMessage='Save' + let themeUI; + if (this.props.selected) { + let inputs = []; + + inputs.push( + <div + className='radio' + key='premadeThemeColorLabel' + > + <label> + <input type='radio' + checked={!displayCustom} + onChange={this.updateType.bind(this, 'premade')} /> - </a> - <a - className='btn btn-sm theme' - href='#' - onClick={this.resetFields} - > <FormattedMessage - id='user.settings.appearance.cancel' - defaultMessage='Cancel' + id='user.settings.display.theme.themeColors' + defaultMessage='Theme Colors' /> - </a> + </label> + <br/> </div> - </div> - ); + ); - return ( - <div> - <div className='modal-header'> - <button - type='button' - className='close' - aria-label='Close' - onClick={this.props.closeModal} - > - <span aria-hidden='true'>{'×'}</span> - </button> - <h4 - className='modal-title' - ref='title' - > - <i - className='modal-back' - onClick={this.props.collapseModal} + inputs.push(premade); + + inputs.push( + <div + className='radio' + key='customThemeColorLabel' + > + <label> + <input type='radio' + checked={displayCustom} + onChange={this.updateType.bind(this, 'custom')} /> <FormattedMessage - id='user.settings.appearance.title' - defaultMessage='Appearance Settings' + id='user.settings.display.theme.customTheme' + defaultMessage='Custom Theme' /> - </h4> + </label> + <br/> </div> - <div className='user-settings'> - <h3 className='tab-header'> - <FormattedMessage - id='user.settings.appearance.title' - defaultMessage='Appearance Settings' - /> - </h3> - <div className='divider-dark first'/> - {themeUI} - <div className='divider-dark'/> + ); + + inputs.push(custom); + + inputs.push( + <div key='importSlackThemeButton'> <br/> <a className='theme' onClick={this.handleImportModal} > <FormattedMessage - id='user.settings.appearance.import' + id='user.settings.display.theme.import' defaultMessage='Import theme colors from Slack' /> </a> </div> - </div> - ); + ); + + themeUI = ( + <SettingItemMax + inputs={inputs} + submit={this.submitTheme} + server_error={serverError} + width='full' + updateSection={(e) => { + this.props.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + themeUI = ( + <SettingItemMin + title={formatMessage(holders.themeTitle)} + describe={formatMessage(holders.themeDescribe)} + updateSection={() => { + this.props.updateSection('theme'); + }} + /> + ); + } + + return themeUI; } } -UserSettingsAppearance.defaultProps = { - activeSection: '' -}; -UserSettingsAppearance.propTypes = { - activeSection: React.PropTypes.string, - updateTab: React.PropTypes.func, - closeModal: React.PropTypes.func.isRequired, - collapseModal: React.PropTypes.func.isRequired, +ThemeSetting.propTypes = { + intl: intlShape.isRequired, + selected: React.PropTypes.bool.isRequired, + updateSection: React.PropTypes.func.isRequired, setRequireConfirm: React.PropTypes.func.isRequired, setEnforceFocus: React.PropTypes.func.isRequired }; + +export default injectIntl(ThemeSetting); diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 9c3270f68..bc2bdbe64 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -28,10 +28,13 @@ class SocketStoreClass extends EventEmitter { this.addChangeListener = this.addChangeListener.bind(this); this.removeChangeListener = this.removeChangeListener.bind(this); this.sendMessage = this.sendMessage.bind(this); + this.close = this.close.bind(this); + this.failCount = 0; this.initialize(); } + initialize() { if (!UserStore.getCurrentId()) { return; @@ -106,15 +109,19 @@ class SocketStoreClass extends EventEmitter { }; } } + emitChange(msg) { this.emit(CHANGE_EVENT, msg); } + addChangeListener(callback) { this.on(CHANGE_EVENT, callback); } + removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } + handleMessage(msg) { switch (msg.action) { case SocketEvents.POSTED: @@ -153,6 +160,7 @@ class SocketStoreClass extends EventEmitter { default: } } + sendMessage(msg) { if (conn && conn.readyState === WebSocket.OPEN) { conn.send(JSON.stringify(msg)); @@ -161,9 +169,16 @@ class SocketStoreClass extends EventEmitter { this.initialize(); } } + setTranslations(messages) { this.translations = messages; } + + close() { + if (conn && conn.readyState === WebSocket.OPEN) { + conn.close(); + } + } } function handleNewPostEvent(msg, translations) { @@ -305,12 +320,5 @@ function handlePreferenceChangedEvent(msg) { var SocketStore = new SocketStoreClass(); -/*SocketStore.dispatchToken = AppDispatcher.register((payload) => { - var action = payload.action; - - switch (action.type) { - default: - } - });*/ - export default SocketStore; +window.SocketStore = SocketStore; diff --git a/web/react/utils/channel_intro_messages.jsx b/web/react/utils/channel_intro_messages.jsx index 69e08f143..1aca0467e 100644 --- a/web/react/utils/channel_intro_messages.jsx +++ b/web/react/utils/channel_intro_messages.jsx @@ -128,7 +128,7 @@ export function createDefaultIntroMessage(channel) { <div className='channel-intro'> <FormattedHTMLMessage id='intro_messages.default' - defaultMessage="<h4 class='channel-intro__title'>Beginning of {display_name}</h4><p class='channel-intro__content'><strong>Welcome to {display_name}!'</strong><br/><br/>This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.</p>" + defaultMessage="<h4 class='channel-intro__title'>Beginning of {display_name}</h4><p class='channel-intro__content'><strong>Welcome to {display_name}!</strong><br/><br/>This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.</p>" values={{ display_name: channel.display_name }} diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index a13689382..ee6a6b955 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -25,6 +25,10 @@ body { } } +b, strong { + font-weight: 600; +} + .inner__wrap { height: 100%; > .row.main { diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index a018315e3..cc22cc913 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -485,8 +485,9 @@ body.ios { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: absolute; - top: -3px; - left: -1.0em; + top: -2px; + left: -7px; + font-size: 11px; line-height: 37px; @include opacity(0); } @@ -570,11 +571,20 @@ body.ios { li { display: inline-block; + vertical-align: top; } .col__name { margin-right: 7px; font-weight: 600; + + .user-popover { + max-width: 200px; + @include clearfix; + text-overflow: ellipsis; + white-space: nowrap; + } + } .col__reply { diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 09d498a69..5d6cbee60 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -160,6 +160,11 @@ .col__name { pointer-events: none; + + .user-popover { + max-width: 130px; + } + } } diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index bd47ca960..bf296e913 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -178,9 +178,17 @@ } } } + + .theme-elements { + padding-left:15px; + .element { + margin-right:10px; + } + } + .custom-label { font-weight: normal; - font-size: 13px; + font-size: 12px; width: 100%; overflow: hidden; text-overflow: ellipsis; diff --git a/web/sass-files/sass/partials/_sidebar--right.scss b/web/sass-files/sass/partials/_sidebar--right.scss index f40a50b03..aaa6f4c92 100644 --- a/web/sass-files/sass/partials/_sidebar--right.scss +++ b/web/sass-files/sass/partials/_sidebar--right.scss @@ -16,6 +16,22 @@ } + .post { + + .post__header { + + .col__name { + + .user-popover { + max-width: 130px; + } + + } + + } + + } + .sidebar--right__content { height: 100%; @include display-flex; diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index caa4afae3..64d06f46d 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -365,6 +365,8 @@ "admin.service.developerDesc": "(Developer Option) When true, extra information around errors will be displayed in the UI.", "admin.service.securityTitle": "Enable Security Alerts: ", "admin.service.securityDesc": "When true, System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.", + "admin.service.insecureTlsTitle": "Enable Insecure Outgoing Connections: ", + "admin.service.insecureTlsDesc": "When true, any outgoing HTTPS requests will accept unverified, self-signed certificates. For example, outgoing webhooks to a server with a self-signed TLS certificate, using any domain, will be allowed. Note that this makes these connections susceptible to man-in-the-middle attacks.", "admin.service.webSessionDays": "Session Length for Web in Days:", "admin.service.webSessionDaysDesc": "The web session will expire after the number of days specified and will require a user to login again.", "admin.service.mobileSessionDays": "Session Length for Mobile Device in Days:", @@ -1087,8 +1089,7 @@ "user.settings.cmds.username_desc": "The username to use when overriding the post.", "user.settings.cmds.icon_url_desc": "URL to an icon", "user.settings.cmds.trigger_desc": "Word to trigger on", - "user.settings.cmds.auto_complete_desc_desc": "A short description of what this commands does", - "user.settings.cmds.auto_complete_help": "Show this command in autocomplete list.", + "user.settings.cmds.auto_complete_help": "Show this command in autocomplete list", "user.settings.cmds.auto_complete_hint_desc": "List parameters to be passed to the command.", "user.settings.cmds.request_type_desc": "Command request type issued to the callback URL.", "user.settings.cmds.url_desc": "URL that will receive the HTTP POST or GET event", @@ -1129,12 +1130,6 @@ "user.settings.advance.sendDesc": "If enabled 'Enter' inserts a new line and 'Ctrl + Enter' submits the message.", "user.settings.advance.preReleaseDesc": "Check any pre-released features you'd like to preview. You may also need to refresh the page before the setting will take effect.", "user.settings.advance.title": "Advanced Settings", - "user.settings.appearance.themeColors": "Theme Colors", - "user.settings.appearance.customTheme": "Custom Theme", - "user.settings.appearance.save": "Save", - "user.settings.appearance.cancel": "Cancel", - "user.settings.appearance.title": "Appearance Settings", - "user.settings.appearance.import": "Import theme colors from Slack", "user.settings.developer.applicationsPreview": "Applications (Preview)", "user.settings.developer.thirdParty": "Open to register a new third-party application", "user.settings.developer.register": "Register New Application", @@ -1194,7 +1189,6 @@ "user.settings.modal.general": "General", "user.settings.modal.security": "Security", "user.settings.modal.notifications": "Notifications", - "user.settings.modal.appearance": "Appearance", "user.settings.modal.developer": "Developer", "user.settings.modal.integrations": "Integrations", "user.settings.modal.display": "Display", @@ -1245,6 +1239,11 @@ "user.settings.security.title": "Security Settings", "user.settings.security.viewHistory": "View Access History", "user.settings.security.logoutActiveSessions": "View and Logout of Active Sessions", + "user.settings.display.theme.title": "Theme", + "user.settings.display.theme.describe": "Open to manage your theme", + "user.settings.display.theme.themeColors": "Theme Colors", + "user.settings.display.theme.customTheme": "Custom Theme", + "user.settings.display.theme.import": "Import theme colors from Slack", "view_image_popover.publicLink": "Get Public Link", "view_image_popover.file": "File {count} of {total}", "view_image_popover.download": "Download", @@ -1253,7 +1252,7 @@ "intro_messages.teammate": "This is the start of your direct message history with this teammate. Direct messages and files shared here are not shown to people outside this area.", "intro_messages.offTopic": "<h4 class=\"channel-intro__title\">Beginning of {display_name}</h4><p class=\"channel-intro__content\">This is the start of {display_name}, a channel for non-work-related conversations.<br/></p>", "intro_messages.inviteOthers": "Invite others to this team", - "intro_messages.default": "<h4 class='channel-intro__title'>Beginning of {display_name}</h4><p class='channel-intro__content'><strong>Welcome to {display_name}!'</strong><br/><br/>This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.</p>", + "intro_messages.default": "<h4 class='channel-intro__title'>Beginning of {display_name}</h4><p class='channel-intro__content'><strong>Welcome to {display_name}!</strong><br/><br/>This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.</p>", "intro_messages.group": "private group", "intro_messages.onlyInvited": " Only invited members can see this private group.", "intro_messages.channel": "channel", diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index b22a7cfd2..a65b20e4c 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -303,6 +303,8 @@ "admin.service.googleTitle": "Llave de desarrolador Google:", "admin.service.iconDescription": "Cuando es verdadero, se le permitirá cambiar el icono del mensaje desde webhooks. Nota, en combinación con permitir el cambio de nombre de usuario, podría exponer a los usuarios a sufrir ataques de phishing.", "admin.service.iconTitle": "Habilitar el cambio de icono desde los Webhooks: ", + "admin.service.insecureTlsDesc": "Cuando es verdadero, cualquier solicitud de salida por HTTPS será aceptada incluso si posee certificados no verificados o autofirmados. Por ejemplo, webhooks de salida a un servidor con un certificado TLS autofirmado, utilizando cualquier dominio, será permitido. Tenga en cuenta que esto hace que estas conexiones susceptibles a los ataques hombre-en-el-medio.", + "admin.service.insecureTlsTitle": "Habilitar Conexiones de Salida Inseguras: ", "admin.service.integrationAdmin": "Habilitar Integraciones sólo para administradores: ", "admin.service.integrationAdminDesc": "Cuando es verdadero, las integraciones creadas por usuarios solo pueden ser creadas por administradores.", "admin.service.listenAddress": "Dirección de escucha:", @@ -1057,12 +1059,6 @@ "user.settings.advance.sendDesc": "Si está habilitado 'Retorno' inserta una nueva linea y 'Ctrl + Retorno' envía el mensaje.", "user.settings.advance.sendTitle": "Enviar mensajes con Ctrl + Retorno", "user.settings.advance.title": "Configuración Avanzada", - "user.settings.appearance.cancel": "Cancelar", - "user.settings.appearance.customTheme": "Tema personalizado", - "user.settings.appearance.import": "Importar los colores de tema de Slack", - "user.settings.appearance.save": "Guardar", - "user.settings.appearance.themeColors": "Selecciona un Tema", - "user.settings.appearance.title": "Configuraciones de Apariencia", "user.settings.cmds.add": "Agregar", "user.settings.cmds.add_desc": "Crea comandos que permitan enviar eventos a integraciones externas. Por favor revisa <a href=\"http://mattermost.org/commands\">http://mattermost.org/commands</a> para aprender más.", "user.settings.cmds.add_display_name.placeholder": "Nombre a mostrar", @@ -1074,7 +1070,6 @@ "user.settings.cmds.auto_complete.yes": "sí", "user.settings.cmds.auto_complete_desc": "Descripción del Auto Completado: ", "user.settings.cmds.auto_complete_desc.placeholder": "Una pequeña descripción de que hace el comando.", - "user.settings.cmds.auto_complete_desc_desc": "Una pequeña descripción de que hace el comando", "user.settings.cmds.auto_complete_help": "Mostrar este comando en la lista de auto completado.", "user.settings.cmds.auto_complete_hint": "Pista de auto completado: ", "user.settings.cmds.auto_complete_hint.placeholder": "[código postal]", @@ -1136,6 +1131,11 @@ "user.settings.display.showNickname": "Mostrar el sobrenombre si existe, de lo contrario mostrar el nombre y apellido", "user.settings.display.showUsername": "Mostrar el nombre de usuario (predeterminado)", "user.settings.display.teammateDisplay": "Visualización del nombre de los integrantes", + "user.settings.display.theme.customTheme": "Tema Personalizado", + "user.settings.display.theme.describe": "Abrir para administrar tu tema", + "user.settings.display.theme.import": "Importar colores del tema desde Slack", + "user.settings.display.theme.themeColors": "Colores del Tema", + "user.settings.display.theme.title": "Tema", "user.settings.display.title": "Configuración de Visualización", "user.settings.general.checkEmail": "Revisa tu correo electrónico {email} para verificar la dirección.", "user.settings.general.checkEmailNoAddress": "Revisa tu correo electrónico para verificar la dirección", @@ -1205,7 +1205,6 @@ "user.settings.languages": "Cambiar Idioma", "user.settings.languages.change": "Cambia el idioma con el que se muestra la intefaz de usuario", "user.settings.modal.advanced": "Avanzada", - "user.settings.modal.appearance": "Apariencia", "user.settings.modal.confirmBtns": "Sí, Descartar", "user.settings.modal.confirmMsg": "Tienes cambios sin guardar, ¿Estás seguro que los quieres descartar?", "user.settings.modal.confirmTitle": "¿Descartar Cambios?", diff --git a/web/templates/head.html b/web/templates/head.html index b1ec905b5..da65e1779 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -122,6 +122,12 @@ } }); }); + + $(window).on('beforeunload', function(){ + if (window.SocketStore) { + SocketStore.close(); + } + }); </script> <script> |