diff options
-rw-r--r-- | api/user.go | 2 | ||||
-rw-r--r-- | config/config.json | 2 | ||||
-rw-r--r-- | model/search_params.go | 30 | ||||
-rw-r--r-- | model/search_params_test.go | 142 | ||||
-rw-r--r-- | model/user.go | 7 | ||||
-rw-r--r-- | model/utils.go | 4 | ||||
-rw-r--r-- | store/sql_user_store.go | 4 | ||||
-rw-r--r-- | web/react/components/tutorial/tutorial_intro_screens.jsx | 54 | ||||
-rw-r--r-- | web/react/components/user_settings/user_settings_general.jsx | 90 | ||||
-rw-r--r-- | web/react/stores/browser_store.jsx | 37 | ||||
-rw-r--r-- | web/react/stores/user_store.jsx | 9 | ||||
-rw-r--r-- | web/react/utils/client.jsx | 1 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_tutorial.scss | 6 | ||||
-rw-r--r-- | web/templates/head.html | 9 |
14 files changed, 328 insertions, 69 deletions
diff --git a/api/user.go b/api/user.go index c871d7c79..774ceddbf 100644 --- a/api/user.go +++ b/api/user.go @@ -114,7 +114,7 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { sendWelcomeEmail = false } - if len(user.AuthData) > 0 && len(user.AuthService) > 0 { + if user.IsSSOUser() { user.EmailVerified = true } diff --git a/config/config.json b/config/config.json index 2738546c0..a927620b5 100644 --- a/config/config.json +++ b/config/config.json @@ -92,4 +92,4 @@ "TokenEndpoint": "", "UserApiEndpoint": "" } -} +}
\ No newline at end of file diff --git a/model/search_params.go b/model/search_params.go index 144e8e461..17a64d980 100644 --- a/model/search_params.go +++ b/model/search_params.go @@ -16,7 +16,7 @@ type SearchParams struct { var searchFlags = [...]string{"from", "channel", "in"} -func splitWords(text string) []string { +func splitWordsNoQuotes(text string) []string { words := []string{} for _, word := range strings.Fields(text) { @@ -31,6 +31,32 @@ func splitWords(text string) []string { return words } +func splitWords(text string) []string { + words := []string{} + + foundQuote := false + location := 0 + for i, char := range text { + if char == '"' { + if foundQuote { + // Grab the quoted section + word := text[location : i+1] + words = append(words, word) + foundQuote = false + location = i + 1 + } else { + words = append(words, splitWordsNoQuotes(text[location:i])...) + foundQuote = true + location = i + } + } + } + + words = append(words, splitWordsNoQuotes(text[location:])...) + + return words +} + func parseSearchFlags(input []string) ([]string, [][2]string) { words := []string{} flags := [][2]string{} @@ -127,7 +153,7 @@ func ParseSearchParams(text string) []*SearchParams { } // special case for when no terms are specified but we still have a filter - if len(plainTerms) == 0 && len(hashtagTerms) == 0 { + if len(plainTerms) == 0 && len(hashtagTerms) == 0 && (len(inChannels) != 0 || len(fromUsers) != 0) { paramsList = append(paramsList, &SearchParams{ Terms: "", IsHashtag: true, diff --git a/model/search_params_test.go b/model/search_params_test.go index e03e82c5a..af4cbe595 100644 --- a/model/search_params_test.go +++ b/model/search_params_test.go @@ -7,11 +7,73 @@ import ( "testing" ) +func TestSplitWords(t *testing.T) { + if words := splitWords(""); len(words) != 0 { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords(" "); len(words) != 0 { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords("word"); len(words) != 1 || words[0] != "word" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords("wo\"rd"); len(words) != 2 || words[0] != "wo" || words[1] != "\"rd" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords("wo\"rd\""); len(words) != 2 || words[0] != "wo" || words[1] != "\"rd\"" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords("word1 word2 word3"); len(words) != 3 || words[0] != "word1" || words[1] != "word2" || words[2] != "word3" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords("word1 \"word2 word3"); len(words) != 3 || words[0] != "word1" || words[1] != "\"word2" || words[2] != "word3" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords("\"word1 word2 word3"); len(words) != 3 || words[0] != "\"word1" || words[1] != "word2" || words[2] != "word3" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords("word1 word2 word3\""); len(words) != 4 || words[0] != "word1" || words[1] != "word2" || words[2] != "word3" || words[3] != "\"" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords("word1 #word2 ##word3"); len(words) != 3 || words[0] != "word1" || words[1] != "#word2" || words[2] != "##word3" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords(" word1 word2 word3 "); len(words) != 3 || words[0] != "word1" || words[1] != "word2" || words[2] != "word3" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords("\"quoted\""); len(words) != 1 || words[0] != "\"quoted\"" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords("\"quoted multiple words\""); len(words) != 1 || words[0] != "\"quoted multiple words\"" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords("some stuff \"quoted multiple words\" more stuff"); len(words) != 5 || words[0] != "some" || words[1] != "stuff" || words[2] != "\"quoted multiple words\"" || words[3] != "more" || words[4] != "stuff" { + t.Fatalf("Incorrect output splitWords: %v", words) + } + + if words := splitWords("some \"stuff\" \"quoted multiple words\" #some \"more stuff\""); len(words) != 5 || words[0] != "some" || words[1] != "\"stuff\"" || words[2] != "\"quoted multiple words\"" || words[3] != "#some" || words[4] != "\"more stuff\"" { + t.Fatalf("Incorrect output splitWords: %v", words) + } +} + func TestParseSearchFlags(t *testing.T) { if words, flags := parseSearchFlags(splitWords("")); len(words) != 0 { - t.Fatal("got words from empty input") + t.Fatalf("got words from empty input") } else if len(flags) != 0 { - t.Fatal("got flags from empty input") + t.Fatalf("got flags from empty input") } if words, flags := parseSearchFlags(splitWords("word")); len(words) != 1 || words[0] != "word" { @@ -32,6 +94,12 @@ func TestParseSearchFlags(t *testing.T) { t.Fatalf("got incorrect flags %v", flags) } + if words, flags := parseSearchFlags(splitWords("#apple #banana from:chan")); len(words) != 2 || words[0] != "#apple" || words[1] != "#banana" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 1 || flags[0][0] != "from" || flags[0][1] != "chan" { + t.Fatalf("got incorrect flags %v", flags) + } + if words, flags := parseSearchFlags(splitWords("apple banana from: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { t.Fatalf("got incorrect words %v", words) } else if len(flags) != 1 || flags[0][0] != "from" || flags[0][1] != "chan" { @@ -74,4 +142,74 @@ func TestParseSearchFlags(t *testing.T) { flags[2][0] != "from" || flags[2][1] != "third" || flags[3][0] != "from" || flags[3][1] != "fourth" { t.Fatalf("got incorrect flags %v", flags) } + + if words, flags := parseSearchFlags(splitWords("\"quoted\"")); len(words) != 1 || words[0] != "\"quoted\"" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 0 { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("\"quoted multiple words\"")); len(words) != 1 || words[0] != "\"quoted multiple words\"" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 0 { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("some \"stuff\" \"quoted multiple words\" some \"more stuff\"")); len(words) != 5 || words[0] != "some" || words[1] != "\"stuff\"" || words[2] != "\"quoted multiple words\"" || words[3] != "some" || words[4] != "\"more stuff\"" { + t.Fatalf("Incorrect output splitWords: %v", words) + } else if len(flags) != 0 { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("some in:here \"stuff\" \"quoted multiple words\" from:someone \"more stuff\"")); len(words) != 4 || words[0] != "some" || words[1] != "\"stuff\"" || words[2] != "\"quoted multiple words\"" || words[3] != "\"more stuff\"" { + t.Fatalf("Incorrect output splitWords: %v", words) + } else if len(flags) != 2 || flags[0][0] != "in" || flags[0][1] != "here" || flags[1][0] != "from" || flags[1][1] != "someone" { + t.Fatalf("got incorrect flags %v", flags) + } +} + +func TestParseSearchParams(t *testing.T) { + if sp := ParseSearchParams(""); len(sp) != 0 { + t.Fatalf("Incorrect output from parse search params: %v", sp) + } + + if sp := ParseSearchParams(" "); len(sp) != 0 { + t.Fatalf("Incorrect output from parse search params: %v", sp) + } + + if sp := ParseSearchParams("words words"); len(sp) != 1 || sp[0].Terms != "words words" || sp[0].IsHashtag != false || len(sp[0].InChannels) != 0 || len(sp[0].FromUsers) != 0 { + t.Fatalf("Incorrect output from parse search params: %v", sp) + } + + if sp := ParseSearchParams("\"my stuff\""); len(sp) != 1 || sp[0].Terms != "\"my stuff\"" || sp[0].IsHashtag != false || len(sp[0].InChannels) != 0 || len(sp[0].FromUsers) != 0 { + t.Fatalf("Incorrect output from parse search params: %v", sp) + } + + if sp := ParseSearchParams("#words #words"); len(sp) != 1 || sp[0].Terms != "#words #words" || sp[0].IsHashtag != true || len(sp[0].InChannels) != 0 || len(sp[0].FromUsers) != 0 { + t.Fatalf("Incorrect output from parse search params: %v", sp) + } + + if sp := ParseSearchParams("#words words"); len(sp) != 2 || sp[1].Terms != "#words" || sp[1].IsHashtag != true || len(sp[1].InChannels) != 0 || len(sp[1].FromUsers) != 0 || sp[0].Terms != "words" || sp[0].IsHashtag != false || len(sp[0].InChannels) != 0 { + t.Fatalf("Incorrect output from parse search params: %v", sp) + } + + if sp := ParseSearchParams("in:channel"); len(sp) != 1 || sp[0].Terms != "" || len(sp[0].InChannels) != 1 || sp[0].InChannels[0] != "channel" || len(sp[0].FromUsers) != 0 { + t.Fatalf("Incorrect output from parse search params: %v", sp) + } + + if sp := ParseSearchParams("testing in:channel"); len(sp) != 1 || sp[0].Terms != "testing" || len(sp[0].InChannels) != 1 || sp[0].InChannels[0] != "channel" || len(sp[0].FromUsers) != 0 { + t.Fatalf("Incorrect output from parse search params: %v", sp) + } + + if sp := ParseSearchParams("in:channel testing"); len(sp) != 1 || sp[0].Terms != "testing" || len(sp[0].InChannels) != 1 || sp[0].InChannels[0] != "channel" || len(sp[0].FromUsers) != 0 { + t.Fatalf("Incorrect output from parse search params: %v", sp) + } + + if sp := ParseSearchParams("in:channel in:otherchannel"); len(sp) != 1 || sp[0].Terms != "" || len(sp[0].InChannels) != 2 || sp[0].InChannels[0] != "channel" || sp[0].InChannels[1] != "otherchannel" || len(sp[0].FromUsers) != 0 { + t.Fatalf("Incorrect output from parse search params: %v", sp) + } + + if sp := ParseSearchParams("testing in:channel from:someone"); len(sp) != 1 || sp[0].Terms != "testing" || len(sp[0].InChannels) != 1 || sp[0].InChannels[0] != "channel" || len(sp[0].FromUsers) != 1 || sp[0].FromUsers[0] != "someone" { + t.Fatalf("Incorrect output from parse search params: %v", sp[0]) + } } diff --git a/model/user.go b/model/user.go index 871d1bf2d..4365f47d2 100644 --- a/model/user.go +++ b/model/user.go @@ -326,6 +326,13 @@ func IsInRole(userRoles string, inRole string) bool { return false } +func (u *User) IsSSOUser() bool { + if len(u.AuthData) != 0 && len(u.AuthService) != 0 { + return true + } + return false +} + func (u *User) PreExport() { u.Password = "" u.AuthData = "" diff --git a/model/utils.go b/model/utils.go index 1e71836c1..6d6eb452d 100644 --- a/model/utils.go +++ b/model/utils.go @@ -262,8 +262,8 @@ func Etag(parts ...interface{}) string { } var validHashtag = regexp.MustCompile(`^(#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$`) -var puncStart = regexp.MustCompile(`^[.,()&$!\[\]{}"':;\\]+`) -var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"';\\]+$`) +var puncStart = regexp.MustCompile(`^[.,()&$!\[\]{}':;\\]+`) +var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}';\\]+$`) func ParseHashtags(text string) (string, string) { words := strings.Fields(text) diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 3347df08b..686949a4d 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -140,7 +140,9 @@ func (us SqlUserStore) Update(user *model.User, allowActiveUpdate bool) StoreCha user.DeleteAt = oldUser.DeleteAt } - if user.Email != oldUser.Email { + if user.IsSSOUser() { + user.Email = oldUser.Email + } else if user.Email != oldUser.Email { user.EmailVerified = false } diff --git a/web/react/components/tutorial/tutorial_intro_screens.jsx b/web/react/components/tutorial/tutorial_intro_screens.jsx index a99e9fe28..66ca556c6 100644 --- a/web/react/components/tutorial/tutorial_intro_screens.jsx +++ b/web/react/components/tutorial/tutorial_intro_screens.jsx @@ -11,12 +11,15 @@ const AsyncClient = require('../../utils/async_client.jsx'); const Constants = require('../../utils/constants.jsx'); const Preferences = Constants.Preferences; +const NUM_SCREENS = 3; + export default class TutorialIntroScreens extends React.Component { constructor(props) { super(props); this.handleNext = this.handleNext.bind(this); this.createScreen = this.createScreen.bind(this); + this.createCircles = this.createCircles.bind(this); this.state = {currentScreen: 0}; } @@ -49,31 +52,27 @@ export default class TutorialIntroScreens extends React.Component { } } createScreenOne() { + const circles = this.createCircles(); + return ( <div> <h3>{'Welcome to:'}</h3> <h1>{'Mattermost'}</h1> <p>{'Your team communication all in one place, instantly searchable and available anywhere.'}</p> <p>{'Keep your team connected to help them achieve what matters most.'}</p> - <div className='tutorial__circles'> - <div className='circle active'/> - <div className='circle'/> - <div className='circle'/> - </div> + {circles} </div> ); } createScreenTwo() { + const circles = this.createCircles(); + return ( <div> <h3>{'How Mattermost works:'}</h3> <p>{'Communication happens in public discussion channels, private groups and direct messages.'}</p> <p>{'Everything is archived and searchable from any web-enabled desktop, laptop or phone.'}</p> - <div className='tutorial__circles'> - <div className='circle'/> - <div className='circle active'/> - <div className='circle'/> - </div> + {circles} </div> ); } @@ -106,6 +105,8 @@ export default class TutorialIntroScreens extends React.Component { ); } + const circles = this.createCircles(); + return ( <div> <h3>{'You’re all set'}</h3> @@ -124,11 +125,34 @@ export default class TutorialIntroScreens extends React.Component { {'.'} </p> {'Click “Next” to enter Town Square. This is the first channel teammates see when they sign up. Use it for posting updates everyone needs to know.'} - <div className='tutorial__circles'> - <div className='circle'/> - <div className='circle'/> - <div className='circle active'/> - </div> + {circles} + </div> + ); + } + createCircles() { + const circles = []; + for (let i = 0; i < NUM_SCREENS; i++) { + let className = 'circle'; + if (i === this.state.currentScreen) { + className += ' active'; + } + + circles.push( + <a + href='#' + key={'circle' + i} + className={className} + onClick={(e) => { //eslint-disable-line no-loop-func + e.preventDefault(); + this.setState({currentScreen: i}); + }} + /> + ); + } + + return ( + <div className='tutorial__circles'> + {circles} </div> ); } diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index 9f0c16194..1bfae6930 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -451,44 +451,60 @@ export default class UserSettingsGeneralTab extends React.Component { } } - inputs.push( - <div key='emailSetting'> - <div className='form-group'> - <label className='col-sm-5 control-label'>{'Primary Email'}</label> - <div className='col-sm-7'> - <input - className='form-control' - type='text' - onChange={this.updateEmail} - value={this.state.email} - /> + let submit = null; + + if (this.props.user.auth_service === '') { + inputs.push( + <div key='emailSetting'> + <div className='form-group'> + <label className='col-sm-5 control-label'>{'Primary Email'}</label> + <div className='col-sm-7'> + <input + className='form-control' + type='text' + onChange={this.updateEmail} + value={this.state.email} + /> + </div> </div> </div> - </div> - ); - - inputs.push( - <div key='confirmEmailSetting'> - <div className='form-group'> - <label className='col-sm-5 control-label'>{'Confirm Email'}</label> - <div className='col-sm-7'> - <input - className='form-control' - type='text' - onChange={this.updateConfirmEmail} - value={this.state.confirmEmail} - /> + ); + + inputs.push( + <div key='confirmEmailSetting'> + <div className='form-group'> + <label className='col-sm-5 control-label'>{'Confirm Email'}</label> + <div className='col-sm-7'> + <input + className='form-control' + type='text' + onChange={this.updateConfirmEmail} + value={this.state.confirmEmail} + /> + </div> </div> + {helpText} </div> - {helpText} - </div> - ); + ); + + submit = this.submitEmail; + } else { + inputs.push( + <div + key='oauthEmailInfo' + className='form-group' + > + <div className='setting-list__hint'>{'Log in occurs through GitLab. Email cannot be updated.'}</div> + {helpText} + </div> + ); + } emailSection = ( <SettingItemMax title='Email' inputs={inputs} - submit={this.submitEmail} + submit={submit} server_error={serverError} client_error={emailError} updateSection={function clearSection(e) { @@ -499,15 +515,19 @@ export default class UserSettingsGeneralTab extends React.Component { ); } else { let describe = ''; - if (this.state.emailChangeInProgress) { - const newEmail = UserStore.getCurrentUser().email; - if (newEmail) { - describe = 'New Address: ' + newEmail + '\nCheck your email to verify the above address.'; + if (this.props.user.auth_service === '') { + if (this.state.emailChangeInProgress) { + const newEmail = UserStore.getCurrentUser().email; + if (newEmail) { + describe = 'New Address: ' + newEmail + '\nCheck your email to verify the above address.'; + } else { + describe = 'Check your email to verify your new address'; + } } else { - describe = 'Check your email to verify your new address'; + describe = UserStore.getCurrentUser().email; } } else { - describe = UserStore.getCurrentUser().email; + describe = 'Log in done through GitLab'; } emailSection = ( diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index 75fb8aa3c..8e86ce32f 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -24,11 +24,17 @@ class BrowserStoreClass { this.setLastServerVersion = this.setLastServerVersion.bind(this); this.clear = this.clear.bind(this); this.clearAll = this.clearAll.bind(this); + this.checkedLocalStorageSupported = ''; + this.signalLogout = this.signalLogout.bind(this); var currentVersion = sessionStorage.getItem('storage_version'); if (currentVersion !== global.window.mm_config.Version) { sessionStorage.clear(); - sessionStorage.setItem('storage_version', global.window.mm_config.Version); + try { + sessionStorage.setItem('storage_version', global.window.mm_config.Version); + } catch (e) { + // Do nothing + } } } @@ -105,6 +111,13 @@ class BrowserStoreClass { sessionStorage.setItem('last_server_version', version); } + signalLogout() { + if (this.isLocalStorageSupported()) { + localStorage.setItem('__logout__', 'yes'); + localStorage.removeItem('__logout__'); + } + } + /** * Preforms the given action on each item that has the given prefix * Signature for action is action(key, value) @@ -147,20 +160,26 @@ class BrowserStoreClass { } isLocalStorageSupported() { + if (this.checkedLocalStorageSupported !== '') { + return this.checkedLocalStorageSupported; + } + try { - sessionStorage.setItem('testSession', '1'); - sessionStorage.removeItem('testSession'); + sessionStorage.setItem('__testSession__', '1'); + sessionStorage.removeItem('__testSession__'); - localStorage.setItem('testLocal', '1'); - if (localStorage.getItem('testLocal') !== '1') { - return false; + localStorage.setItem('__testLocal__', '1'); + if (localStorage.getItem('__testLocal__') !== '1') { + this.checkedLocalStorageSupported = false; } - localStorage.removeItem('testLocal', '1'); + localStorage.removeItem('__testLocal__', '1'); - return true; + this.checkedLocalStorageSupported = true; } catch (e) { - return false; + this.checkedLocalStorageSupported = false; } + + return this.checkedLocalStorageSupported; } } diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index 4fa7224b7..6b7d671fc 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -58,6 +58,8 @@ class UserStoreClass extends EventEmitter { this.setStatus = this.setStatus.bind(this); this.getStatuses = this.getStatuses.bind(this); this.getStatus = this.getStatus.bind(this); + + this.profileCache = null; } emitChange(userId) { @@ -184,6 +186,10 @@ class UserStoreClass extends EventEmitter { } getProfiles() { + if (this.profileCache !== null) { + return this.profileCache; + } + return BrowserStore.getItem('profiles', {}); } @@ -218,6 +224,7 @@ class UserStoreClass extends EventEmitter { saveProfile(profile) { var ps = this.getProfiles(); ps[profile.id] = profile; + this.profileCache = ps; BrowserStore.setItem('profiles', ps); } @@ -226,6 +233,8 @@ class UserStoreClass extends EventEmitter { if (currentId in profiles) { delete profiles[currentId]; } + + this.profileCache = profiles; BrowserStore.setItem('profiles', profiles); } diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 003e24d33..d27fe16cf 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -231,6 +231,7 @@ export function resetPassword(data, success, error) { export function logout() { track('api', 'api_users_logout'); var currentTeamUrl = TeamStore.getCurrentTeamUrl(); + BrowserStore.signalLogout(); BrowserStore.clear(); ErrorStore.storeLastError(null); window.location.href = currentTeamUrl + '/logout'; diff --git a/web/sass-files/sass/partials/_tutorial.scss b/web/sass-files/sass/partials/_tutorial.scss index c1bf5fd59..08d491fd9 100644 --- a/web/sass-files/sass/partials/_tutorial.scss +++ b/web/sass-files/sass/partials/_tutorial.scss @@ -171,6 +171,10 @@ margin-bottom: 30px; font-weight: 600; } + .tutorial__circles { + position: absolute; + bottom: 40px; + } } .tutorial__circles { @@ -188,4 +192,4 @@ @include opacity(1); } } -}
\ No newline at end of file +} diff --git a/web/templates/head.html b/web/templates/head.html index a73e809a7..2bbf921ee 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -52,6 +52,15 @@ headers: { 'X-MM-TokenIndex': mm_session_token_index } }); } + + $(function () { + $(window).bind('storage', function (e) { + if (e.originalEvent.key === '__logout__') { + console.log('detected logout from a different tab'); + window.location.href = '/' + window.mm_team.name; + } + }); + }); </script> <script> |