diff --git a/api/oauth.go b/api/oauth.go
index 30efbdce3a..0726993217 100644
--- a/api/oauth.go
+++ b/api/oauth.go
@@ -204,7 +204,10 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
}
break
case model.OAUTH_ACTION_LOGIN:
- LoginByOAuth(c, w, r, service, body)
+ user := LoginByOAuth(c, w, r, service, body)
+ if len(teamId) > 0 {
+ c.Err = JoinUserToTeamById(teamId, user)
+ }
if c.Err == nil {
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
}
@@ -424,8 +427,17 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
service := params["service"]
loginHint := r.URL.Query().Get("login_hint")
+ teamId, err := getTeamIdFromQuery(r.URL.Query())
+ if err != nil {
+ c.Err = err
+ return
+ }
+
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_LOGIN
+ if len(teamId) != 0 {
+ stateProps["team_id"] = teamId
+ }
if authUrl, err := GetAuthorizationCode(c, service, stateProps, loginHint); err != nil {
c.Err = err
@@ -435,6 +447,36 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
+func getTeamIdFromQuery(query url.Values) (string, *model.AppError) {
+ hash := query.Get("h")
+ inviteId := query.Get("id")
+
+ if len(hash) > 0 {
+ data := query.Get("d")
+ props := model.MapFromJson(strings.NewReader(data))
+
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
+ return "", model.NewLocAppError("getTeamIdFromQuery", "web.singup_with_oauth.invalid_link.app_error", nil, "")
+ }
+
+ t, err := strconv.ParseInt(props["time"], 10, 64)
+ if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
+ return "", model.NewLocAppError("getTeamIdFromQuery", "web.singup_with_oauth.expired_link.app_error", nil, "")
+ }
+
+ return props["id"], nil
+ } else if len(inviteId) > 0 {
+ if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
+ // soft fail, so we still create user but don't auto-join team
+ l4g.Error("%v", result.Err)
+ } else {
+ return result.Data.(*model.Team).Id, nil
+ }
+ }
+
+ return "", nil
+}
+
func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
@@ -445,34 +487,10 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- hash := r.URL.Query().Get("h")
-
- teamId := ""
- inviteId := r.URL.Query().Get("id")
-
- if len(hash) > 0 {
- data := r.URL.Query().Get("d")
- props := model.MapFromJson(strings.NewReader(data))
-
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
- c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "")
- return
- }
-
- t, err := strconv.ParseInt(props["time"], 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
- c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "")
- return
- }
-
- teamId = props["id"]
- } else if len(inviteId) != 0 {
- if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
- // soft fail, so we still create user but don't auto-join team
- l4g.Error("%v", result.Err)
- } else {
- teamId = result.Data.(*model.Team).Id
- }
+ teamId, err := getTeamIdFromQuery(r.URL.Query())
+ if err != nil {
+ c.Err = err
+ return
}
stateProps := map[string]string{}
diff --git a/api/team.go b/api/team.go
index 8eb7c4fefc..46bff617b9 100644
--- a/api/team.go
+++ b/api/team.go
@@ -17,6 +17,7 @@ import (
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
@@ -247,6 +248,14 @@ func CreateTeam(c *Context, team *model.Team) *model.Team {
}
}
+func JoinUserToTeamById(teamId string, user *model.User) *model.AppError {
+ if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
+ return result.Err
+ } else {
+ return JoinUserToTeam(result.Data.(*model.Team), user)
+ }
+}
+
func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError {
tm := &model.TeamMember{TeamId: team.Id, UserId: user.Id}
@@ -258,6 +267,9 @@ func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError {
}
if tmr := <-Srv.Store.Team().SaveMember(tm); tmr.Err != nil {
+ if tmr.Err.Id == store.TEAM_MEMBER_EXISTS_ERROR {
+ return nil
+ }
return tmr.Err
}
diff --git a/api/user.go b/api/user.go
index de7a560bf3..aae3dffa5a 100644
--- a/api/user.go
+++ b/api/user.go
@@ -285,11 +285,6 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service
suchan := Srv.Store.User().GetByAuth(user.AuthData, service)
euchan := Srv.Store.User().GetByEmail(user.Email)
- var tchan store.StoreChannel
- if len(teamId) != 0 {
- tchan = Srv.Store.Team().Get(teamId)
- }
-
found := true
count := 0
for found {
@@ -319,20 +314,14 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service
return nil
}
- if tchan != nil {
- if result := <-tchan; result.Err != nil {
- c.Err = result.Err
+ if len(teamId) > 0 {
+ err = JoinUserToTeamById(teamId, user)
+ if err != nil {
+ c.Err = err
return nil
- } else {
- team := result.Data.(*model.Team)
- err = JoinUserToTeam(team, user)
- if err != nil {
- c.Err = err
- return nil
- }
-
- go addDirectChannels(team.Id, user)
}
+
+ go addDirectChannels(teamId, user)
}
doLogin(c, w, r, ruser, "")
diff --git a/mattermost.go b/mattermost.go
index d6353c14a8..67428275a1 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -868,6 +868,7 @@ func cmdVersion() {
fmt.Fprintln(os.Stderr, "Build Date: "+model.BuildDate)
fmt.Fprintln(os.Stderr, "Build Hash: "+model.BuildHash)
fmt.Fprintln(os.Stderr, "Build Enterprise Ready: "+model.BuildEnterpriseReady)
+ fmt.Fprintln(os.Stderr, "DB Version: "+api.Srv.Store.(*store.SqlStore).SchemaVersion)
os.Exit(0)
}
diff --git a/model/config.go b/model/config.go
index b9177204bd..08510fc44f 100644
--- a/model/config.go
+++ b/model/config.go
@@ -6,6 +6,7 @@ package model
import (
"encoding/json"
"io"
+ "strings"
)
const (
@@ -35,6 +36,15 @@ const (
FAKE_SETTING = "********************************"
)
+// should match the values in webapp/i18n/i18n.jsx
+var LOCALES = []string{
+ "en",
+ "es",
+ "fr",
+ "ja",
+ "pt-BR",
+}
+
type ServiceSettings struct {
ListenAddress string
MaximumLoginAttempts int
@@ -420,19 +430,39 @@ func (o *Config) SetDefaults() {
*o.SupportSettings.SupportEmail = "feedback@mattermost.com"
}
+ if o.LdapSettings.Enable == nil {
+ o.LdapSettings.Enable = new(bool)
+ *o.LdapSettings.Enable = false
+ }
+
+ if o.LdapSettings.LdapServer == nil {
+ o.LdapSettings.LdapServer = new(string)
+ *o.LdapSettings.LdapServer = ""
+ }
+
if o.LdapSettings.LdapPort == nil {
o.LdapSettings.LdapPort = new(int)
*o.LdapSettings.LdapPort = 389
}
- if o.LdapSettings.QueryTimeout == nil {
- o.LdapSettings.QueryTimeout = new(int)
- *o.LdapSettings.QueryTimeout = 60
+ if o.LdapSettings.ConnectionSecurity == nil {
+ o.LdapSettings.ConnectionSecurity = new(string)
+ *o.LdapSettings.ConnectionSecurity = ""
}
- if o.LdapSettings.Enable == nil {
- o.LdapSettings.Enable = new(bool)
- *o.LdapSettings.Enable = false
+ if o.LdapSettings.BaseDN == nil {
+ o.LdapSettings.BaseDN = new(string)
+ *o.LdapSettings.BaseDN = ""
+ }
+
+ if o.LdapSettings.BindUsername == nil {
+ o.LdapSettings.BindUsername = new(string)
+ *o.LdapSettings.BindUsername = ""
+ }
+
+ if o.LdapSettings.BindPassword == nil {
+ o.LdapSettings.BindPassword = new(string)
+ *o.LdapSettings.BindPassword = ""
}
if o.LdapSettings.UserFilter == nil {
@@ -440,9 +470,29 @@ func (o *Config) SetDefaults() {
*o.LdapSettings.UserFilter = ""
}
- if o.LdapSettings.LoginFieldName == nil {
- o.LdapSettings.LoginFieldName = new(string)
- *o.LdapSettings.LoginFieldName = ""
+ if o.LdapSettings.FirstNameAttribute == nil {
+ o.LdapSettings.FirstNameAttribute = new(string)
+ *o.LdapSettings.FirstNameAttribute = ""
+ }
+
+ if o.LdapSettings.LastNameAttribute == nil {
+ o.LdapSettings.LastNameAttribute = new(string)
+ *o.LdapSettings.LastNameAttribute = ""
+ }
+
+ if o.LdapSettings.EmailAttribute == nil {
+ o.LdapSettings.EmailAttribute = new(string)
+ *o.LdapSettings.EmailAttribute = ""
+ }
+
+ if o.LdapSettings.NicknameAttribute == nil {
+ o.LdapSettings.NicknameAttribute = new(string)
+ *o.LdapSettings.NicknameAttribute = ""
+ }
+
+ if o.LdapSettings.IdAttribute == nil {
+ o.LdapSettings.IdAttribute = new(string)
+ *o.LdapSettings.IdAttribute = ""
}
if o.LdapSettings.SyncIntervalMinutes == nil {
@@ -450,6 +500,21 @@ func (o *Config) SetDefaults() {
*o.LdapSettings.SyncIntervalMinutes = 60
}
+ if o.LdapSettings.SkipCertificateVerification == nil {
+ o.LdapSettings.SkipCertificateVerification = new(bool)
+ *o.LdapSettings.SkipCertificateVerification = false
+ }
+
+ if o.LdapSettings.QueryTimeout == nil {
+ o.LdapSettings.QueryTimeout = new(int)
+ *o.LdapSettings.QueryTimeout = 60
+ }
+
+ if o.LdapSettings.LoginFieldName == nil {
+ o.LdapSettings.LoginFieldName = new(string)
+ *o.LdapSettings.LoginFieldName = ""
+ }
+
if o.ServiceSettings.SessionLengthWebInDays == nil {
o.ServiceSettings.SessionLengthWebInDays = new(int)
*o.ServiceSettings.SessionLengthWebInDays = 30
@@ -515,21 +580,6 @@ func (o *Config) SetDefaults() {
*o.ComplianceSettings.EnableDaily = false
}
- if o.LdapSettings.ConnectionSecurity == nil {
- o.LdapSettings.ConnectionSecurity = new(string)
- *o.LdapSettings.ConnectionSecurity = ""
- }
-
- if o.LdapSettings.SkipCertificateVerification == nil {
- o.LdapSettings.SkipCertificateVerification = new(bool)
- *o.LdapSettings.SkipCertificateVerification = false
- }
-
- if o.LdapSettings.NicknameAttribute == nil {
- o.LdapSettings.NicknameAttribute = new(string)
- *o.LdapSettings.NicknameAttribute = ""
- }
-
if o.LocalizationSettings.DefaultServerLocale == nil {
o.LocalizationSettings.DefaultServerLocale = new(string)
*o.LocalizationSettings.DefaultServerLocale = DEFAULT_LOCALE
@@ -542,7 +592,7 @@ func (o *Config) SetDefaults() {
if o.LocalizationSettings.AvailableLocales == nil {
o.LocalizationSettings.AvailableLocales = new(string)
- *o.LocalizationSettings.AvailableLocales = *o.LocalizationSettings.DefaultClientLocale
+ *o.LocalizationSettings.AvailableLocales = strings.Join(LOCALES, ",")
}
}
diff --git a/model/version.go b/model/version.go
index dde9eccd7d..d486f5c57b 100644
--- a/model/version.go
+++ b/model/version.go
@@ -13,6 +13,7 @@ import (
// It should be maitained in chronological order with most current
// release at the front of the list.
var versions = []string{
+ "3.1.0",
"3.0.0",
"2.2.0",
"2.1.0",
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 6e1deeb209..c668988dcf 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -10,6 +10,10 @@ import (
"github.com/mattermost/platform/utils"
)
+const (
+ TEAM_MEMBER_EXISTS_ERROR = "store.sql_team.save_member.exists.app_error"
+)
+
type SqlTeamStore struct {
*SqlStore
}
@@ -372,8 +376,8 @@ func (s SqlTeamStore) SaveMember(member *model.TeamMember) StoreChannel {
}
if err := s.GetMaster().Insert(member); err != nil {
- if IsUniqueConstraintError(err.Error(), []string{"TeamId", "teammembers_pkey"}) {
- result.Err = model.NewLocAppError("SqlTeamStore.SaveMember", "store.sql_team.save_member.exists.app_error", nil, "team_id="+member.TeamId+", user_id="+member.UserId+", "+err.Error())
+ if IsUniqueConstraintError(err.Error(), []string{"TeamId", "teammembers_pkey", "PRIMARY"}) {
+ result.Err = model.NewLocAppError("SqlTeamStore.SaveMember", TEAM_MEMBER_EXISTS_ERROR, nil, "team_id="+member.TeamId+", user_id="+member.UserId+", "+err.Error())
} else {
result.Err = model.NewLocAppError("SqlTeamStore.SaveMember", "store.sql_team.save_member.save.app_error", nil, "team_id="+member.TeamId+", user_id="+member.UserId+", "+err.Error())
}
diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx
index ca3bf63628..0c07173ac2 100644
--- a/webapp/actions/global_actions.jsx
+++ b/webapp/actions/global_actions.jsx
@@ -139,8 +139,8 @@ export function doFocusPost(channelId, postId, data) {
});
AsyncClient.getChannels(true);
AsyncClient.getChannelExtraInfo(channelId);
- AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS);
- AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS);
+ AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS, true);
+ AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS, true);
}
export function emitPostFocusEvent(postId) {
diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx
index d760e3db9c..c947be5cb1 100644
--- a/webapp/components/admin_console/admin_sidebar.jsx
+++ b/webapp/components/admin_console/admin_sidebar.jsx
@@ -239,6 +239,22 @@ export default class AdminSidebar extends React.Component {
);
}
+ let customBranding = null;
+
+ if (window.mm_license.IsLicensed === 'true') {
+ customBranding = (
+
+ }
+ />
+ );
+ }
+
return (
@@ -515,16 +531,7 @@ export default class AdminSidebar extends React.Component {
/>
}
>
-
-
- }
- />
+ {customBranding}
}
value={this.state.syncIntervalMinutes}
diff --git a/webapp/components/admin_console/multiselect_settings.jsx b/webapp/components/admin_console/multiselect_settings.jsx
index deba983de4..ca0bdc9aad 100644
--- a/webapp/components/admin_console/multiselect_settings.jsx
+++ b/webapp/components/admin_console/multiselect_settings.jsx
@@ -50,6 +50,7 @@ export default class MultiSelectSetting extends React.Component {
labelKey='text'
options={this.props.values}
joinValues={true}
+ clearable={false}
disabled={this.props.disabled}
noResultsText={this.props.noResultText}
onChange={this.handleChange}
diff --git a/webapp/components/admin_console/storage_settings.jsx b/webapp/components/admin_console/storage_settings.jsx
index 60eedc2d24..077d75c737 100644
--- a/webapp/components/admin_console/storage_settings.jsx
+++ b/webapp/components/admin_console/storage_settings.jsx
@@ -22,8 +22,8 @@ export default class StorageSettings extends AdminSettings {
this.renderSettings = this.renderSettings.bind(this);
+ //maxFileSize: props.config.FileSettings.MaxFileSize,
this.state = Object.assign(this.state, {
- maxFileSize: props.config.FileSettings.MaxFileSize,
driverName: props.config.FileSettings.DriverName,
directory: props.config.FileSettings.Directory,
amazonS3AccessKeyId: props.config.FileSettings.AmazonS3AccessKeyId,
@@ -34,7 +34,7 @@ export default class StorageSettings extends AdminSettings {
}
getConfigFromState(config) {
- config.FileSettings.MaxFileSize = this.parseInt(this.state.maxFileSize);
+ //config.FileSettings.MaxFileSize = this.parseInt(this.state.maxFileSize);
config.FileSettings.DriverName = this.state.driverName;
config.FileSettings.Directory = this.state.directory;
config.FileSettings.AmazonS3AccessKeyId = this.state.amazonS3AccessKeyId;
@@ -57,26 +57,27 @@ export default class StorageSettings extends AdminSettings {
}
renderSettings() {
+ /*
+ }
+ placeholder={Utils.localizeMessage('admin.image.maxFileSizeExample', 'Ex "52428800"')}
+ helpText={
+
+ }
+ value={this.state.maxFileSize}
+ onChange={this.handleChange}
+ />*/
+
return (
-
- }
- placeholder={Utils.localizeMessage('admin.image.maxFileSizeExample', 'Ex "52428800"')}
- helpText={
-
- }
- value={this.state.maxFileSize}
- onChange={this.handleChange}
- />
{
- // DO nothing. Websockets will handle this.
+ PostStore.removePendingPost(post.channel_id, post.pending_post_id);
},
(err) => {
if (err.id === 'api.post.create_post.root_id.app_error') {
diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx
index 7aa0a586b9..caf58c7442 100644
--- a/webapp/components/create_post.jsx
+++ b/webapp/components/create_post.jsx
@@ -180,7 +180,7 @@ class CreatePost extends React.Component {
Client.createPost(post,
() => {
- // DO nothing. Websockets will handle this.
+ PostStore.removePendingPost(post.pending_post_id);
},
(err) => {
if (err.id === 'api.post.create_post.root_id.app_error') {
diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx
index bdd540b09b..4bd23a26d9 100644
--- a/webapp/components/edit_post_modal.jsx
+++ b/webapp/components/edit_post_modal.jsx
@@ -67,6 +67,7 @@ class EditPostModal extends React.Component {
Client.updatePost(
updatedPost,
() => {
+ AsyncClient.getPosts(updatedPost.channel_id);
window.scrollTo(0, 0);
},
(err) => {
diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx
index dd4a92f924..ab8b49392d 100644
--- a/webapp/components/login/login_controller.jsx
+++ b/webapp/components/login/login_controller.jsx
@@ -69,13 +69,13 @@ export default class LoginController extends React.Component {
this.setState({loginId});
}
- let password = this.refs.password.value;
+ const password = this.refs.password.value;
if (password !== this.state.password) {
this.setState({password});
}
+ // don't trim the password since we support spaces in passwords
loginId = loginId.trim();
- password = password.trim();
if (!loginId) {
// it's slightly weird to be constructing the message ID, but it's a bit nicer than triply nested if statements
@@ -444,7 +444,7 @@ export default class LoginController extends React.Component {
@@ -529,4 +529,4 @@ export default class LoginController extends React.Component {
);
}
-}
\ No newline at end of file
+}
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index 8996381bae..ff64520354 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -175,6 +175,7 @@ export default class RhsRootPost extends React.Component {
filenames={post.filenames}
channelId={post.channel_id}
userId={post.user_id}
+ compactDisplay={this.props.compactDisplay}
/>
);
}
diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx
index b3825f72f3..a9d96e320a 100644
--- a/webapp/components/signup_user_complete.jsx
+++ b/webapp/components/signup_user_complete.jsx
@@ -173,11 +173,24 @@ export default class SignupUserComplete extends React.Component {
this.state.ldapPassword,
null,
() => {
- GlobalActions.emitInitialLoad(
- () => {
- browserHistory.push('/select_team');
- }
- );
+ if (this.props.location.query.id || this.props.location.query.h) {
+ Client.addUserToTeamFromInvite(
+ this.props.location.query.d,
+ this.props.location.query.h,
+ this.props.location.query.id,
+ () => {
+ this.finishSignup();
+ },
+ () => {
+ // there's not really a good way to deal with this, so just let the user log in like normal
+ this.finishSignup();
+ }
+ );
+
+ return;
+ }
+
+ this.finishSignup();
},
(err) => {
if (err.id === 'ent.ldap.do_login.user_not_registered.app_error' || err.id === 'ent.ldap.do_login.user_filtered.app_error') {
@@ -205,6 +218,15 @@ export default class SignupUserComplete extends React.Component {
);
}
+ finishSignup() {
+ GlobalActions.emitInitialLoad(
+ () => {
+ GlobalActions.loadDefaultLocale();
+ browserHistory.push('/select_team');
+ }
+ );
+ }
+
handleUserCreated(user, data) {
track('signup', 'signup_user_02_complete');
Client.loginById(
@@ -761,4 +783,4 @@ export default class SignupUserComplete extends React.Component {
);
}
-}
\ No newline at end of file
+}
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 1e0788ebe7..85f221c7c6 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -181,6 +181,7 @@
"admin.email.smtpUsernameTitle": "SMTP Username:",
"admin.email.testing": "Testing...",
"admin.false": "false",
+ "admin.general.localization": "Localization",
"admin.general.localization.availableLocalesDescription": "Determines which languages are available for users in Account Settings.",
"admin.general.localization.clientLocaleDescription": "Default language for newly created users and pages where the user hasn't logged in.",
"admin.general.localization.serverLocaleDescription": "Default language for system messages and logs. Changing this will require a server restart before taking effect.",
@@ -283,7 +284,7 @@
"admin.ldap.serverTitle": "LDAP Server:",
"admin.ldap.skipCertificateVerification": "Skip Certificate Verification",
"admin.ldap.skipCertificateVerificationDesc": "Skips the certificate verification step for TLS or STARTTLS connections. Not recommended for production environments where TLS is required. For testing only.",
- "admin.ldap.syncIntervalHelpText": "LDAP Synchronization is the process by which Mattermost updates its users to reflect any updated data on the LDAP server. For example if a name for a user is updated on the LDAP server, the change will be reflected in Mattermost when the synchronization is performed. Accounts that have been removed from the LDAP server will have their active sessions cleared and no longer be able to login to Mattermost. Mattermost will perform this synchronization regularly according to the interval supplied here. For example, if 60 is supplied, Mattermost will update the users every hour.",
+ "admin.ldap.syncIntervalHelpText": "LDAP Synchronization is the process by which Mattermost updates its users to reflect any updated data on the LDAP server. For example if a name for a user is updated on the LDAP server, the change will be reflected in Mattermost when the synchronization is performed. Accounts that have been removed from the LDAP server will have their active sessions cleared and no longer be able to login to Mattermost. Mattermost will perform this synchronization regularly according to the interval supplied here. For example, if 60 is supplied, Mattermost will update the users every hour. Changing this will require a server restart before taking effect.",
"admin.ldap.syncIntervalTitle": "Synchronization Interval (In Minutes)",
"admin.ldap.uernameAttrDesc": "The attribute in the LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.",
"admin.ldap.userFilterDisc": "Optionally enter an LDAP Filter to use when searching for user objects. Only the users selected by the query will be able to access Mattermost. For Active Directory, the query to filter out disabled users is (&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))).",
@@ -433,6 +434,7 @@
"admin.sidebar.integrations": "Integrations",
"admin.sidebar.ldap": "LDAP",
"admin.sidebar.license": "Edition and License",
+ "admin.sidebar.localization": "Localization",
"admin.sidebar.logging": "Logging",
"admin.sidebar.login": "Login",
"admin.sidebar.logs": "Logs",
diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json
index 5680d7cb96..6ce2e77c3c 100644
--- a/webapp/i18n/es.json
+++ b/webapp/i18n/es.json
@@ -181,6 +181,7 @@
"admin.email.smtpUsernameTitle": "Usuario SMTP:",
"admin.email.testing": "Probando...",
"admin.false": "falso",
+ "admin.general.localization": "Idiomas",
"admin.general.localization.availableLocalesDescription": "Determina qué idiomas están disponibles para los usuarios en la Configuración de la Cuenta.",
"admin.general.localization.clientLocaleDescription": "Idioma predeterminado para nuevos usuarios y páginas donde el usuario no ha iniciado sesión.",
"admin.general.localization.serverLocaleDescription": "Idioma predeterminado para los mensajes del sistema y los registros. Cambiar esto requerirá un reinicio del servidor antes de tomar efecto.",
@@ -431,6 +432,7 @@
"admin.sidebar.integrations": "Integraciones",
"admin.sidebar.ldap": "LDAP",
"admin.sidebar.license": "Edición y Licencia",
+ "admin.sidebar.localization": "Idiomas",
"admin.sidebar.logging": "Registros",
"admin.sidebar.login": "Inicio de Sesión",
"admin.sidebar.logs": "Registros",
diff --git a/webapp/i18n/i18n.jsx b/webapp/i18n/i18n.jsx
index 783cef975b..3a66c5c338 100644
--- a/webapp/i18n/i18n.jsx
+++ b/webapp/i18n/i18n.jsx
@@ -13,6 +13,7 @@ import frLocaleData from 'react-intl/locale-data/fr';
import jaLocaleData from 'react-intl/locale-data/ja';
import ptLocaleData from 'react-intl/locale-data/pt';
+// should match the values in model/config.go
const languages = {
en: {
value: 'en',
diff --git a/webapp/package.json b/webapp/package.json
index 7535f5e04e..5bc9445579 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -18,7 +18,7 @@
"keymirror": "0.1.1",
"marked": "mattermost/marked#2d92082cd542dc0adc175a1407194f6a4c3cb621",
"match-at": "0.1.0",
- "mattermost": "mattermost/mattermost-javascript#master",
+ "mattermost": "mattermost/mattermost-javascript#release-3.1",
"object-assign": "4.1.0",
"perfect-scrollbar": "0.6.11",
"react": "15.0.2",
diff --git a/webapp/sass/components/_mentions.scss b/webapp/sass/components/_mentions.scss
index 9e47e2a713..82f626eafe 100644
--- a/webapp/sass/components/_mentions.scss
+++ b/webapp/sass/components/_mentions.scss
@@ -13,10 +13,10 @@
@include clearfix;
cursor: pointer;
font-size: 13px;
- height: 36px;
- line-height: 36px;
- margin: 5px 0;
- padding: 2px;
+ height: 39px;
+ line-height: 35px;
+ margin: 0;
+ padding: 3px 8px;
position: relative;
white-space: nowrap;
width: 100%;
diff --git a/webapp/stores/post_store.jsx b/webapp/stores/post_store.jsx
index 73cb603146..b77c7bd3cd 100644
--- a/webapp/stores/post_store.jsx
+++ b/webapp/stores/post_store.jsx
@@ -552,7 +552,7 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
switch (action.type) {
case ActionTypes.RECEIVED_POSTS: {
- const id = PostStore.currentFocusedPostId == null ? action.id : PostStore.currentFocusedPostId;
+ const id = PostStore.currentFocusedPostId !== null && action.isPost ? PostStore.currentFocusedPostId : action.id;
PostStore.storePosts(id, makePostListNonNull(action.post_list));
PostStore.checkBounds(id, action.numRequested, makePostListNonNull(action.post_list), action.before);
PostStore.emitChange();
diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx
index f57ecf1cd6..218a7f1dbd 100644
--- a/webapp/stores/user_store.jsx
+++ b/webapp/stores/user_store.jsx
@@ -104,7 +104,7 @@ class UserStoreClass extends EventEmitter {
this.currentUserId = user.id;
global.window.mm_current_user_id = this.currentUserId;
if (LocalizationStore.getLocale() !== user.locale) {
- GlobalActions.newLocalizationSelected(user.locale);
+ setTimeout(() => GlobalActions.newLocalizationSelected(user.locale), 0);
}
}
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index 6f5f8a2cd0..65cb2d2587 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -603,7 +603,7 @@ export function getPosts(id) {
);
}
-export function getPostsBefore(postId, offset, numPost) {
+export function getPostsBefore(postId, offset, numPost, isPost) {
const channelId = ChannelStore.getCurrentId();
if (channelId == null) {
return;
@@ -624,7 +624,8 @@ export function getPostsBefore(postId, offset, numPost) {
id: channelId,
before: true,
numRequested: numPost,
- post_list: data
+ post_list: data,
+ isPost
});
getProfiles();
@@ -638,7 +639,7 @@ export function getPostsBefore(postId, offset, numPost) {
);
}
-export function getPostsAfter(postId, offset, numPost) {
+export function getPostsAfter(postId, offset, numPost, isPost) {
const channelId = ChannelStore.getCurrentId();
if (channelId == null) {
return;
@@ -659,7 +660,8 @@ export function getPostsAfter(postId, offset, numPost) {
id: channelId,
before: false,
numRequested: numPost,
- post_list: data
+ post_list: data,
+ isPost
});
getProfiles();
diff --git a/webapp/utils/markdown.jsx b/webapp/utils/markdown.jsx
index 73af4ae91a..bd1e998b4a 100644
--- a/webapp/utils/markdown.jsx
+++ b/webapp/utils/markdown.jsx
@@ -163,7 +163,7 @@ class MattermostMarkdownRenderer extends marked.Renderer {
}
// remove any links added to the text by hashtag or mention parsing since they'll break this link
- output += '>' + text.replace(/<\/?a[^>]*>/, '') + '';
+ output += '>' + text.replace(/<\/?a[^>]*>/g, '') + '';
return output;
}