summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/command.go72
-rw-r--r--api/post.go11
-rw-r--r--api/user.go6
-rw-r--r--api/webhook.go2
-rw-r--r--config/config.json3
-rw-r--r--docker/dev/config_docker.json1
-rw-r--r--docker/local/config_docker.json1
-rw-r--r--model/config.go48
-rw-r--r--web/react/components/admin_console/analytics.jsx37
-rw-r--r--web/react/components/admin_console/service_settings.jsx50
-rw-r--r--web/react/components/channel_header.jsx2
-rw-r--r--web/react/components/create_comment.jsx14
-rw-r--r--web/react/components/create_post.jsx15
-rw-r--r--web/react/components/signup_user_complete.jsx2
-rw-r--r--web/react/components/suggestion/command_provider.jsx3
-rw-r--r--web/react/components/team_signup_username_page.jsx2
-rw-r--r--web/react/components/time_since.jsx3
-rw-r--r--web/react/components/user_settings/manage_command_hooks.jsx16
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx1
-rw-r--r--web/react/stores/socket_store.jsx24
-rw-r--r--web/react/stores/suggestion_store.jsx4
-rw-r--r--web/react/utils/async_client.jsx18
-rw-r--r--web/react/utils/constants.jsx5
-rw-r--r--web/react/utils/text_formatting.jsx8
-rw-r--r--web/react/utils/utils.jsx15
-rw-r--r--web/sass-files/sass/partials/_base.scss236
-rw-r--r--web/sass-files/sass/partials/_headers.scss7
-rw-r--r--web/sass-files/sass/partials/_markdown.scss10
-rw-r--r--web/sass-files/sass/partials/_post.scss28
-rw-r--r--web/sass-files/sass/partials/_responsive.scss12
-rw-r--r--web/sass-files/sass/partials/_search.scss5
-rw-r--r--web/static/i18n/en.json3
-rw-r--r--web/static/i18n/es.json1
-rw-r--r--web/templates/head.html6
34 files changed, 448 insertions, 223 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/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/analytics.jsx b/web/react/components/admin_console/analytics.jsx
index 0a159d2e3..ec9ad4da0 100644
--- a/web/react/components/admin_console/analytics.jsx
+++ b/web/react/components/admin_console/analytics.jsx
@@ -1,7 +1,6 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as Utils from '../../utils/utils.jsx';
import Constants from '../../utils/constants.jsx';
import LineChart from './line_chart.jsx';
import DoughnutChart from './doughnut_chart.jsx';
@@ -10,7 +9,7 @@ import StatisticCount from './statistic_count.jsx';
var Tooltip = ReactBootstrap.Tooltip;
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
+import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl';
const holders = defineMessages({
analyticsTotalUsers: {
@@ -75,10 +74,12 @@ export default class Analytics extends React.Component {
}
let loading = (
- <FormattedMessage
- id='admin.analytics.loading'
- defaultMessage='Loading...'
- />
+ <h5>
+ <FormattedMessage
+ id='admin.analytics.loading'
+ defaultMessage='Loading...'
+ />
+ </h5>
);
let firstRow;
@@ -322,7 +323,17 @@ export default class Analytics extends React.Component {
</time>
</OverlayTrigger>
</td>
- <td>{Utils.displayDateTime(user.last_activity_at)}</td>
+ <td>
+ <FormattedDate
+ value={user.last_activity_at}
+ day='numeric'
+ month='long'
+ year='numeric'
+ hour12={true}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ </td>
</tr>
);
})
@@ -378,7 +389,17 @@ export default class Analytics extends React.Component {
</time>
</OverlayTrigger>
</td>
- <td>{Utils.displayDateTime(user.create_at)}</td>
+ <td>
+ <FormattedDate
+ value={user.create_at}
+ day='numeric'
+ month='long'
+ year='numeric'
+ hour12={true}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ </td>
</tr>
);
})
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/channel_header.jsx b/web/react/components/channel_header.jsx
index 005a82209..8fc3cd63d 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -419,7 +419,7 @@ export default class ChannelHeader extends React.Component {
</ul>
</div>
<OverlayTrigger
- trigger={['hover', 'focus']}
+ trigger={'click'}
placement='bottom'
overlay={popoverContent}
ref='headerOverlay'
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 9e7c67515..709485991 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -59,6 +59,7 @@ class CreateComment extends React.Component {
this.getFileCount = this.getFileCount.bind(this);
this.handleResize = this.handleResize.bind(this);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
+ this.focusTextbox = this.focusTextbox.bind(this);
PostStore.clearCommentDraftUploads();
@@ -76,7 +77,7 @@ class CreateComment extends React.Component {
PreferenceStore.addChangeListener(this.onPreferenceChange);
window.addEventListener('resize', this.handleResize);
- this.refs.textbox.focus();
+ this.focusTextbox();
}
componentWillUnmount() {
PreferenceStore.removeChangeListener(this.onPreferenceChange);
@@ -99,7 +100,7 @@ class CreateComment extends React.Component {
}
if (prevProps.rootId !== this.props.rootId) {
- this.refs.textbox.focus();
+ this.focusTextbox();
}
}
handleSubmit(e) {
@@ -226,7 +227,7 @@ class CreateComment extends React.Component {
}
}
handleUploadClick() {
- this.refs.textbox.focus();
+ this.focusTextbox();
}
handleUploadStart(clientIds) {
let draft = PostStore.getCommentDraft(this.props.rootId);
@@ -238,7 +239,7 @@ class CreateComment extends React.Component {
// this is a bit redundant with the code that sets focus when the file input is clicked,
// but this also resets the focus after a drag and drop
- this.refs.textbox.focus();
+ this.focusTextbox();
}
handleFileUploadComplete(filenames, clientIds) {
let draft = PostStore.getCommentDraft(this.props.rootId);
@@ -306,6 +307,11 @@ class CreateComment extends React.Component {
getFileCount() {
return this.state.previews.length + this.state.uploadsInProgress.length;
}
+ focusTextbox() {
+ if (!Utils.isMobile()) {
+ this.refs.textbox.focus();
+ }
+ }
render() {
let serverError = null;
if (this.state.serverError) {
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 6ea80cd13..ecabdaee6 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -63,6 +63,7 @@ class CreatePost extends React.Component {
this.getFileCount = this.getFileCount.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.sendMessage = this.sendMessage.bind(this);
+ this.focusTextbox = this.focusTextbox.bind(this);
PostStore.clearDraftUploads();
@@ -193,6 +194,11 @@ class CreatePost extends React.Component {
}
);
}
+ focusTextbox() {
+ if (!Utils.isMobile()) {
+ this.refs.textbox.focus();
+ }
+ }
postMsgKeyPress(e) {
if (this.state.ctrlSend && e.ctrlKey || !this.state.ctrlSend) {
if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
@@ -216,7 +222,7 @@ class CreatePost extends React.Component {
PostStore.storeCurrentDraft(draft);
}
handleUploadClick() {
- this.refs.textbox.focus();
+ this.focusTextbox();
}
handleUploadStart(clientIds, channelId) {
const draft = PostStore.getDraft(channelId);
@@ -228,7 +234,7 @@ class CreatePost extends React.Component {
// this is a bit redundant with the code that sets focus when the file input is clicked,
// but this also resets the focus after a drag and drop
- this.refs.textbox.focus();
+ this.focusTextbox();
}
handleFileUploadComplete(filenames, clientIds, channelId) {
const draft = PostStore.getDraft(channelId);
@@ -305,11 +311,12 @@ class CreatePost extends React.Component {
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
PreferenceStore.addChangeListener(this.onPreferenceChange);
- this.refs.textbox.focus();
+
+ this.focusTextbox();
}
componentDidUpdate(prevProps, prevState) {
if (prevState.channelId !== this.state.channelId) {
- this.refs.textbox.focus();
+ this.focusTextbox();
}
}
componentWillUnmount() {
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index 98a832542..672213d1a 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -303,7 +303,7 @@ class SignupUserComplete extends React.Component {
ref='name'
className='form-control'
placeholder=''
- maxLength='128'
+ maxLength={Constants.MAX_USERNAME_LENGTH}
spellCheck='false'
/>
{nameError}
diff --git a/web/react/components/suggestion/command_provider.jsx b/web/react/components/suggestion/command_provider.jsx
index 91d556bb9..09c9b9982 100644
--- a/web/react/components/suggestion/command_provider.jsx
+++ b/web/react/components/suggestion/command_provider.jsx
@@ -2,7 +2,6 @@
// See License.txt for license information.
import * as AsyncClient from '../../utils/async_client.jsx';
-import SuggestionStore from '../../stores/suggestion_store.jsx';
class CommandSuggestion extends React.Component {
render() {
@@ -38,8 +37,6 @@ CommandSuggestion.propTypes = {
export default class CommandProvider {
handlePretextChanged(suggestionId, pretext) {
if (pretext.startsWith('/')) {
- SuggestionStore.setMatchedPretext(suggestionId, pretext);
-
AsyncClient.getSuggestedCommands(pretext, suggestionId, CommandSuggestion);
}
}
diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx
index a7332975d..0fa9cb103 100644
--- a/web/react/components/team_signup_username_page.jsx
+++ b/web/react/components/team_signup_username_page.jsx
@@ -115,7 +115,7 @@ class TeamSignupUsernamePage extends React.Component {
className='form-control'
placeholder=''
defaultValue={this.props.state.user.username}
- maxLength='128'
+ maxLength={Constants.MAX_USERNAME_LENGTH}
spellCheck='false'
/>
{nameHelpText}
diff --git a/web/react/components/time_since.jsx b/web/react/components/time_since.jsx
index ba8dbffcc..1560d2469 100644
--- a/web/react/components/time_since.jsx
+++ b/web/react/components/time_since.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import Constants from '../utils/constants.jsx';
+import * as Utils from '../utils/utils.jsx';
import {FormattedRelative, FormattedDate} from 'mm-intl';
@@ -24,7 +25,7 @@ export default class TimeSince extends React.Component {
if (this.props.sameUser) {
return (
<time className='post__time'>
- <FormattedRelative value={this.props.eventTime} />
+ {Utils.displayTimeFormatted(this.props.eventTime)}
</time>
);
}
diff --git a/web/react/components/user_settings/manage_command_hooks.jsx b/web/react/components/user_settings/manage_command_hooks.jsx
index b2fc0a4e1..948ab7885 100644
--- a/web/react/components/user_settings/manage_command_hooks.jsx
+++ b/web/react/components/user_settings/manage_command_hooks.jsx
@@ -537,17 +537,11 @@ export default class ManageCommandCmds extends React.Component {
onChange={this.updateAutoComplete}
/>
<FormattedMessage
- id='user.settings.cmds.auto_complete_desc_desc'
- defaultMessage='A short description of what this commands does'
+ id='user.settings.cmds.auto_complete_help'
+ defaultMessage='Show this command in autocomplete list'
/>
</label>
</div>
- <div className='padding-top'>
- <FormattedMessage
- id='user.settings.cmds.auto_complete_help'
- defaultMessage='Show this command in autocomplete list.'
- />
- </div>
</div>
<div className='padding-top x2'>
<label className='control-label'>
@@ -565,12 +559,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'>
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index f20b4b807..cd229775f 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -514,6 +514,7 @@ class UserSettingsGeneralTab extends React.Component {
<label className='col-sm-5 control-label'>{usernameLabel}</label>
<div className='col-sm-7'>
<input
+ maxLength={Constants.MAX_USERNAME_LENGTH}
className='form-control'
type='text'
onChange={this.updateUsername}
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/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx
index 9cd566c22..dd5c107e0 100644
--- a/web/react/stores/suggestion_store.jsx
+++ b/web/react/stores/suggestion_store.jsx
@@ -223,7 +223,9 @@ class SuggestionStore extends EventEmitter {
this.emitSuggestionsChanged(id);
break;
case ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS:
- if (other.matchedPretext === this.getMatchedPretext(id)) {
+ if (this.getMatchedPretext(id) === '') {
+ this.setMatchedPretext(id, other.matchedPretext);
+
// ensure the matched pretext hasn't changed so that we don't receive suggestions for outdated pretext
this.addSuggestions(id, other.terms, other.items, other.component);
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 328a7a7f2..c5957e8cc 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -789,14 +789,16 @@ export function getSuggestedCommands(command, suggestionId, component) {
// pull out the suggested commands from the returned data
const terms = matches.map((suggestion) => suggestion.suggestion);
- AppDispatcher.handleServerAction({
- type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
- id: suggestionId,
- matchedPretext: command,
- terms,
- items: matches,
- component
- });
+ if (terms.length > 0) {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
+ id: suggestionId,
+ matchedPretext: command,
+ terms,
+ items: matches,
+ component
+ });
+ }
},
(err) => {
dispatchError(err, 'getCommandSuggestions');
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index c1bd41b88..d78776aa3 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -464,8 +464,9 @@ export default {
},
OVERLAY_TIME_DELAY: 400,
MIN_USERNAME_LENGTH: 3,
- MAX_USERNAME_LENGTH: 15,
+ MAX_USERNAME_LENGTH: 64,
MIN_PASSWORD_LENGTH: 5,
MAX_PASSWORD_LENGTH: 50,
- TIME_SINCE_UPDATE_INTERVAL: 30000
+ TIME_SINCE_UPDATE_INTERVAL: 30000,
+ MIN_HASHTAG_LINK_LENGTH: 3
};
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index e837ded53..dae2252a6 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -248,8 +248,14 @@ function autolinkHashtags(text, tokens) {
const index = tokens.size;
const alias = `MM_HASHTAG${index}`;
+ let value = hashtag;
+
+ if (hashtag.length > Constants.MIN_HASHTAG_LINK_LENGTH) {
+ value = `<a class='mention-link' href='#' data-hashtag='${hashtag}'>${hashtag}</a>`;
+ }
+
tokens.set(alias, {
- value: `<a class='mention-link' href='#' data-hashtag='${hashtag}'>${hashtag}</a>`,
+ value,
originalText: hashtag
});
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 6bb7baa64..4beec8d64 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -14,6 +14,8 @@ import * as AsyncClient from './async_client.jsx';
import * as client from './client.jsx';
import Autolinker from 'autolinker';
+import {FormattedTime} from 'mm-intl';
+
export function isEmail(email) {
// writing a regex to match all valid email addresses is really, really hard (see http://stackoverflow.com/a/201378)
// so we just do a simple check and rely on a verification email to tell if it's a real address
@@ -245,6 +247,19 @@ export function displayTime(ticks, utc) {
return hours + ':' + minutes + ampm + timezone;
}
+export function displayTimeFormatted(ticks) {
+ const useMilitaryTime = PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time');
+
+ return (
+ <FormattedTime
+ value={ticks}
+ hour='numeric'
+ minute='numeric'
+ hour12={!useMilitaryTime}
+ />
+ );
+}
+
export function displayDateTime(ticks) {
var seconds = Math.floor((Date.now() - ticks) / 1000);
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index 04ba9c51e..a13689382 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -1,28 +1,28 @@
@charset "UTF-8";
html, body {
- height: 100%;
+ height: 100%;
}
body {
- font-family: 'Open Sans', sans-serif;
- -webkit-font-smoothing: antialiased;
- background: $body-bg;
- position: relative;
- width: 100%;
- height: 100%;
- &.white {
- background: #fff;
- > .container-fluid {
- overflow: auto;
- }
- .inner__wrap {
- > .row.content {
- min-height: 100%;
- margin-bottom: -89px;
- }
- }
- }
+ font-family: 'Open Sans', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ background: $body-bg;
+ position: relative;
+ width: 100%;
+ height: 100%;
+ &.white {
+ background: #fff;
+ > .container-fluid {
+ overflow: auto;
+ }
+ .inner__wrap {
+ > .row.content {
+ min-height: 100%;
+ margin-bottom: -89px;
+ }
+ }
+ }
}
.inner__wrap {
@@ -46,175 +46,177 @@ body {
}
img {
- max-width: 100%;
- height: auto;
- &.rounded {
- @include border-radius(100%);
- }
+ max-width: 100%;
+ height: auto;
+ &.rounded {
+ @include border-radius(100%);
+ }
}
.popover {
- @include border-radius(3px);
- color: #333;
- &.bottom, &.right, &.top, &.left {
- >.arrow:after {
- border-color: transparent;
+ @include border-radius(3px);
+ color: #333;
+ &.bottom, &.right, &.top, &.left {
+ >.arrow:after {
+ border-color: transparent;
+ }
+ }
+ .popover-title {
+ background: rgba(black, 0.05);
+ }
+ .popover-content {
+ p:last-child {
+ margin-bottom: 5px;
+ }
}
- }
- .popover-title {
- background: rgba(black, 0.05);
- }
- .popover-content {
- white-space: pre-wrap;
- }
}
.dropdown-menu {
- .divider {
- @include opacity(0.15);
- }
- > li > a {
- color: inherit;
- &:focus, &:hover {
- color: inherit;
+ .divider {
+ @include opacity(0.15);
+ }
+ > li > a {
+ color: inherit;
+ &:focus, &:hover {
+ color: inherit;
+ }
}
- }
}
.word-break--all {
- word-break: break-all;
+ word-break: break-all;
}
a {
- word-break: break-word;
- color: $primary-color;
- cursor: pointer;
+ word-break: break-word;
+ color: $primary-color;
+ cursor: pointer;
}
a:focus, a:hover {
- color: $primary-color;
+ color: $primary-color;
}
.tooltip {
- .tooltip-inner {
- word-break: break-word;
- font-size: 13px;
- padding: 3px 10px 4px;
- font-weight: 500;
- }
+ .tooltip-inner {
+ word-break: break-word;
+ font-size: 13px;
+ padding: 3px 10px 4px;
+ font-weight: 500;
+ }
}
.nopadding {
- padding: 0;
- margin: 0;
+ padding: 0;
+ margin: 0;
}
.text-danger, a.text-danger {
- color: #E05F5D;
- &:hover, &:focus {
color: #E05F5D;
- }
+ &:hover, &:focus {
+ color: #E05F5D;
+ }
}
.btn {
- &.btn-danger {
- color: #fff;
- &:hover, &:active, &:focus {
- color: #fff;
+ &.btn-danger {
+ color: #fff;
+ &:hover, &:active, &:focus {
+ color: #fff;
+ }
}
- }
}
.form-control {
- @include border-radius(2px);
- &:focus {
- @include box-shadow(none);
- }
- &.no-padding {
- line-height: 32px;
- padding: 0;
- }
- &.no-resize {
- resize: none;
- }
+ @include border-radius(2px);
+ &:focus {
+ @include box-shadow(none);
+ }
+ &.no-padding {
+ line-height: 32px;
+ padding: 0;
+ }
+ &.no-resize {
+ resize: none;
+ }
}
.form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control {
- cursor: auto;
- background: rgba(#fff, 0.1);
- color: inherit;
+ cursor: auto;
+ background: rgba(#fff, 0.1);
+ color: inherit;
}
.form-group {
- &.form-group--small {
- margin-bottom: 10px;
- }
+ &.form-group--small {
+ margin-bottom: 10px;
+ }
}
.error-panel {
- max-width: 275px;
- position: absolute;
- right: 10px;
- top: 40px;
- z-index: 100;
+ max-width: 275px;
+ position: absolute;
+ right: 10px;
+ top: 40px;
+ z-index: 100;
}
.nav>li>a:focus, .nav>li>a:hover {
- background: transparent;
+ background: transparent;
}
.btn {
- @include single-transition(all, 0.25s, ease-in);
- @include border-radius(1px);
- &.btn-primary {
- border-color: transparent;
- background: $primary-color;
- &:hover, &:focus, &:active {
- background: $primary-color--hover;
- }
- }
- &.btn-inactive {
- border-color: transparent;
- background: #707070;
- color: #fff;
- }
+ @include single-transition(all, 0.25s, ease-in);
+ @include border-radius(1px);
+ &.btn-primary {
+ border-color: transparent;
+ background: $primary-color;
+ &:hover, &:focus, &:active {
+ background: $primary-color--hover;
+ }
+ }
+ &.btn-inactive {
+ border-color: transparent;
+ background: #707070;
+ color: #fff;
+ }
}
.relative-div {
- position:relative;
+ position:relative;
}
@-webkit-keyframes spin2 {
- from { -webkit-transform: rotate(0deg);}
- to { -webkit-transform: rotate(360deg);}
+ from { -webkit-transform: rotate(0deg);}
+ to { -webkit-transform: rotate(360deg);}
}
@keyframes spin {
- from { transform: scale(1) rotate(0deg);}
- to { transform: scale(1) rotate(360deg);}
+ from { transform: scale(1) rotate(0deg);}
+ to { transform: scale(1) rotate(360deg);}
}
.glyphicon-refresh-animate {
- @include animation(spin .7s infinite linear);
+ @include animation(spin .7s infinite linear);
}
.black-bg {
- background-color: black !important;
+ background-color: black !important;
}
.white-bg {
- background-color: white !important;
+ background-color: white !important;
}
.alert {
- padding: 8px 12px;
- @include border-radius(2px);
+ padding: 8px 12px;
+ @include border-radius(2px);
}
.emoji {
- width: 1.5em;
- height: 1.5em;
- display: inline-block;
- margin-bottom: 0.25em;
- background-size: contain;
+ width: 1.5em;
+ height: 1.5em;
+ display: inline-block;
+ margin-bottom: 0.25em;
+ background-size: contain;
}
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index 4a4de5c3b..93cb04198 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -298,8 +298,13 @@
height: 30px;
width: 24px;
line-height: 26px;
- margin-right: 10px;
+ margin-right: 9px;
font-size: 22px;
+ .channel__wrap.move--left & {
+ position: absolute;
+ right: -400px;
+ top: 14px;
+ }
> a {
color: inherit;
text-decoration: none;
diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss
index 7aa29d95d..14e12ecd2 100644
--- a/web/sass-files/sass/partials/_markdown.scss
+++ b/web/sass-files/sass/partials/_markdown.scss
@@ -20,6 +20,14 @@
.post-body--code {
position: relative;
+
+ pre {
+ margin-bottom: 0;
+ word-break: normal;
+ overflow: auto;
+ word-wrap: normal;
+ }
+
}
.post-body--code__language {
@@ -43,7 +51,7 @@
@include opacity(0.2);
}
code {
- white-space: pre-line;
+ white-space: pre;
}
}
.markdown__table {
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 77b66a1a8..a018315e3 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -283,8 +283,8 @@ body.ios {
width: 40px;
height: 40px;
position: absolute;
- bottom: 50px;
- right: 5px;
+ bottom: 0;
+ left: 10px;
z-index: 50;
@include opacity(0);
@include single-transition(all, 0.3s);
@@ -647,10 +647,26 @@ body.ios {
}
.post__remove {
+ font-family: 'Open Sans', sans-serif;
+ position: relative;
display: inline-block;
+ vertical-align: top;
+ right: 15px;
+ top: -5px;
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ line-height: 20px;
+ font-weight: 600;
visibility: hidden;
- margin-right: 5px;
- top: -1px;
+ color: inherit;
+ @include opacity(0.5);
+ text-decoration: none;
+
+ &:hover {
+ @include opacity(0.8);
+ }
+
}
.post__body {
@@ -663,6 +679,10 @@ body.ios {
margin: 0 0 0.4em;
}
+ p + p {
+ margin-top: 1.4em;
+ }
+
img {
max-height: 400px;
}
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 832481cc5..09d498a69 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -60,6 +60,11 @@
&.move--left {
@include translateX(0);
+
+ .search-bar__container {
+ padding-right: 8px;
+ }
+
}
}
@@ -68,6 +73,13 @@
&.move--left {
margin-right: 0;
+
+ .channel-header__links {
+ position: relative;
+ right: auto;
+ top: auto;
+ }
+
}
}
diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss
index 693c59a31..aa398e916 100644
--- a/web/sass-files/sass/partials/_search.scss
+++ b/web/sass-files/sass/partials/_search.scss
@@ -1,11 +1,14 @@
@charset "UTF-8";
#channel-header .search-bar__container {
- padding: 0 8px 0 3px;
+ padding: 0 9px 0 3px;
}
.search-bar__container {
padding: 12px 8px 0 0;
@include flex(0 0 56px);
+ .sidebar--right.move--left & {
+ padding-right: 42px;
+ }
}
.search__clear {
display: none;
diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json
index 53dfd2d4e..5b03f0130 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,7 +1089,6 @@
"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_hint_desc": "List parameters to be passed to the command.",
"user.settings.cmds.request_type_desc": "Command request type issued to the callback URL.",
diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json
index 0e5c7cfae..25515ca86 100644
--- a/web/static/i18n/es.json
+++ b/web/static/i18n/es.json
@@ -1068,7 +1068,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]",
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>