diff options
24 files changed, 150 insertions, 176 deletions
diff --git a/.gitignore b/.gitignore index 6e433df3c..dab6b8373 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,12 @@ _testmain.go *npm-debug.log* # Vim temporary files -*.swp +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist +*~ # Build files *bundle.js diff --git a/LICENSE.txt b/LICENSE.txt index c0c337525..88200cdba 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -2,34 +2,22 @@ Mattermost Licensing SOFTWARE LICENSING -Mattermost server is made available under two separate licensing options: +You are licensed to use compiled versions of the Mattermost platform produced by Mattermost, Inc. under an MIT LICENSE -- Free Software Foundation’s GNU AGPL v.3.0, subject to the exceptions outlined in this policy; or -- Commercial licenses available from Mattermost, Inc. by contacting commercial@mattermost.com +- See MIT-COMPILED-LICENSE.md included in compiled versions for details. -Admin Tools and Configuration Files (api/templates/, config/, model/, web/react/utils/, web/static/, web/templates/ and all -subdirectories thereof) are made available under: +You may be licensed to use source code to create compiled versions not produced by Mattermost, Inc. in one of two ways: -- Apache License v2.0 +1. Under the Free Software Foundation’s GNU AGPL v.3.0, subject to the exceptions outlined in this policy; or +2. Under a commercial license available from Mattermost, Inc. by contacting commercial@mattermost.com -LICENSING POLICY +You are licensed to use the source code in Admin Tools and Configuration Files (api/templates/, config/, model/, +web/react/utils/, web/static/, web/templates/ and all subdirectories thereof) under the Apache License v2.0. -The objective of the Mattermost server license is to require enhancements to Mattermost server be shared with the community -while allowing for non-enhanced use in proprietary applications. - -Therefore, the Mattermost server is free to use, modify and redistribute in open source applications via the -copyleft AGPL license. For proprietary applications (systems that don’t share source back to the community), -Mattermost is free to use and redistribute so long as you’re not withholding proprietary enhancements to the -Mattermost server and you’re only linking directly to or changing Admin Tools and Configuration Files (defined above), which -are released under an Apache 2.0 license, and copyleft free. - -We promise that we will not enforce the copyleft provisions in AGPL v3.0 against you if your application (a) does -not link to the Mattermost server directly, but exclusively uses the Mattermost Admin Tools and Configuration Files, -and (b) you have not modified, added to or adapted the source code of Mattermost in a way that results in the creation -of a “modified version” or “work based on” Mattermost as these terms are defined in the AGPL v3.0 license. - -If the above is not enough to satisfy your organization’s legal department (some will not approve GPL in any form), -commercial licenses are available from commercial@mattermost.com. +We promise that we will not enforce the copyleft provisions in AGPL v3.0 against you if your application (a) does not +link to the Mattermost Platform directly, but exclusively uses the Mattermost Admin Tools and Configuration Files, and +(b) you have not modified, added to or adapted the source code of Mattermost in a way that results in the creation of +a “modified version” or “work based on” Mattermost as these terms are defined in the AGPL v3.0 license. MATTERMOST TRADEMARK GUIDELINES diff --git a/api/post.go b/api/post.go index 81cc9a1c6..40c5efe8c 100644 --- a/api/post.go +++ b/api/post.go @@ -153,9 +153,6 @@ func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIc linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})") - linkRegex := regexp.MustCompile(`<\s*(\S*)\s*>`) - text = linkRegex.ReplaceAllString(text, "${1}") - post := &model.Post{UserId: c.Session.UserId, ChannelId: channelId, Message: text, Type: postType} post.AddProp("from_webhook", "true") @@ -177,7 +174,21 @@ func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIc if len(props) > 0 { for key, val := range props { - if key != "override_icon_url" && key != "override_username" && key != "from_webhook" { + if key == "attachments" { + if list, success := val.([]interface{}); success { + // parse attachment links into Markdown format + for i, aInt := range list { + attachment := aInt.(map[string]interface{}) + if _, ok := attachment["text"]; ok { + aText := attachment["text"].(string) + aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})") + attachment["text"] = aText + list[i] = attachment + } + } + post.AddProp(key, list) + } + } else if key != "override_icon_url" && key != "override_username" && key != "from_webhook" { post.AddProp(key, val) } } diff --git a/doc/developer/API.md b/doc/developer/API.md index 1be3669ab..1da1a475b 100644 --- a/doc/developer/API.md +++ b/doc/developer/API.md @@ -40,7 +40,7 @@ If you're building a deep integration with Mattermost, for example a mobile nati If no driver is available for the programming language of your choice, you can view the [Golang Driver](https://github.com/mattermost/platform/blob/master/model/client.go) source code to understand how it exercises the Web Service API. You can also learn more by reviewing open source projects that use the Web Service API, like [matterircd](https://github.com/42wim/matterircd). -There are a wide range of [installation guides](www.mattermost.org/installation/) for setting up your own Mattermost server on which to develop and test your integrations. +There are a wide range of [installation guides](http://www.mattermost.org/installation/) for setting up your own Mattermost server on which to develop and test your integrations. diff --git a/doc/install/Configuration-Settings.md b/doc/install/Configuration-Settings.md index 44730d40f..66fda15e0 100644 --- a/doc/install/Configuration-Settings.md +++ b/doc/install/Configuration-Settings.md @@ -268,19 +268,19 @@ Settings to configure account and team creation using GitLab OAuth. “true”: Allow team creation and account signup using GitLab OAuth. To configure, input the **Secret** and **Id** credentials. ```"Secret": ""``` -Obtain this value by logging into your GitLab account. Go to Profile Settings -> Applications -> New Application, enter a Name, then enter Redirect URLs "https://<your-mattermost-url>/login/gitlab/complete" (example: https://example.com:8065/login/gitlab/complete) and "https://<your-mattermost-url>/signup/gitlab/complete". +Obtain this value by logging into your GitLab account. Go to Profile Settings -> Applications -> New Application, enter a Name, then enter Redirect URLs `https://<your-mattermost-url>/login/gitlab/complete` (example: `https://example.com:8065/login/gitlab/complete`) and `https://<your-mattermost-url>/signup/gitlab/complete`. ```"Id": ""``` -Obtain this value by logging into your GitLab account. Go to Profile Settings -> Applications -> New Application, enter a Name, then enter Redirect URLs "https://<your-mattermost-url>/login/gitlab/complete" (example: https://example.com:8065/login/gitlab/complete) and "https://<your-mattermost-url>/signup/gitlab/complete". +Obtain this value by logging into your GitLab account. Go to Profile Settings -> Applications -> New Application, enter a Name, then enter Redirect URLs `https://<your-mattermost-url>/login/gitlab/complete` (example: `https://example.com:8065/login/gitlab/complete`) and `https://<your-mattermost-url>/signup/gitlab/complete`. ```"AuthEndpoint": ""``` -Enter https://<your-gitlab-url>/oauth/authorize (example: https://example.com:3000/oauth/authorize). Use HTTP or HTTPS depending on how your server is configured. +Enter `https://<your-gitlab-url>/oauth/authorize` (example: `https://example.com:3000/oauth/authorize`). Use HTTP or HTTPS depending on how your server is configured. ```"TokenEndpoint": ""``` -Enter https://<your-gitlab-url>/oauth/authorize (example: https://example.com:3000/oauth/token). Use HTTP or HTTPS depending on how your server is configured. +Enter `https://<your-gitlab-url>/oauth/authorize` (example: `https://example.com:3000/oauth/token`). Use HTTP or HTTPS depending on how your server is configured. ```"UserApiEndpoint": ""``` -Enter https://<your-gitlab-url>/oauth/authorize (example: https://example.com:3000/api/v3/user). Use HTTP or HTTPS depending on how your server is configured. +Enter `https://<your-gitlab-url>/oauth/authorize` (example: `https://example.com:3000/api/v3/user`). Use HTTP or HTTPS depending on how your server is configured. ## Config.json Settings Not in System Console diff --git a/doc/process/overview.md b/doc/process/overview.md index b34908782..a1201a8d6 100644 --- a/doc/process/overview.md +++ b/doc/process/overview.md @@ -40,7 +40,7 @@ A system primarily used by Mattermost for reporting bugs with clear statements o See [Filing Issues](http://www.mattermost.org/filing-issues/) for details on how to file issues for Mattermost in GitHub. -For feature ideas, troubleshooting, or general questions, we ask your help to use the appropriate [Community System](https://github.com/mattermost/platform/blob/master/doc/process/overview.md#community-systems). +Please consider using more mainstream processes for [filing feature ideas to be upvoted](https://github.com/mattermost/platform/blob/master/doc/process/overview.md#feature-idea-forum), to ask [troubleshooting questions](https://github.com/mattermost/platform/blob/master/doc/process/overview.md#troubleshooting-forum), or [general questions](https://github.com/mattermost/platform/blob/master/doc/process/overview.md#general-forum). ### GitHub Pull Requests diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 08c4a48ea..d5a46721e 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -40,7 +40,6 @@ export default class ChannelHeader extends React.Component { const state = this.getStateFromStores(); state.showEditChannelPurposeModal = false; - state.showInviteModal = false; state.showMembersModal = false; this.state = state; } @@ -201,13 +200,13 @@ export default class ChannelHeader extends React.Component { key='add_members' role='presentation' > - <a + <ToggleModalButton role='menuitem' - href='#' - onClick={() => this.setState({showInviteModal: true})} + dialogType={ChannelInviteModal} + dialogProps={{channel}} > {'Add Members'} - </a> + </ToggleModalButton> </li> ); @@ -402,13 +401,10 @@ export default class ChannelHeader extends React.Component { onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})} channel={channel} /> - <ChannelInviteModal - show={this.state.showInviteModal} - onModalDismissed={() => this.setState({showInviteModal: false})} - /> <ChannelMembersModal show={this.state.showMembersModal} onModalDismissed={() => this.setState({showMembersModal: false})} + channel={channel} /> </div> ); diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx index 0518ccb86..56e2e53f9 100644 --- a/web/react/components/channel_invite_modal.jsx +++ b/web/react/components/channel_invite_modal.jsx @@ -53,15 +53,8 @@ export default class ChannelInviteModal extends React.Component { return a.username.localeCompare(b.username); }); - var channelName = ''; - if (ChannelStore.getCurrent()) { - channelName = ChannelStore.getCurrent().display_name; - } - return { nonmembers, - memberIds, - channelName, loading }; } @@ -94,28 +87,14 @@ export default class ChannelInviteModal extends React.Component { } } handleInvite(userId) { - // Make sure the user isn't already a member of the channel - if (this.state.memberIds.indexOf(userId) > -1) { - return; - } - var data = {}; data.user_id = userId; - Client.addChannelMember(ChannelStore.getCurrentId(), data, + Client.addChannelMember( + this.props.channel.id, + data, () => { - var nonmembers = this.state.nonmembers; - var memberIds = this.state.memberIds; - - for (var i = 0; i < nonmembers.length; i++) { - if (userId === nonmembers[i].id) { - nonmembers[i].invited = true; - memberIds.push(userId); - break; - } - } - - this.setState({inviteError: null, memberIds, nonmembers}); + this.setState({inviteError: null}); AsyncClient.getChannelExtraInfo(); }, (err) => { @@ -157,10 +136,10 @@ export default class ChannelInviteModal extends React.Component { <Modal dialogClassName='more-modal' show={this.props.show} - onHide={this.props.onModalDismissed} + onHide={this.props.onHide} > <Modal.Header closeButton={true}> - <Modal.Title>{'Add New Members to '}<span className='name'>{this.state.channelName}</span></Modal.Title> + <Modal.Title>{'Add New Members to '}<span className='name'>{this.props.channel.display_nam}</span></Modal.Title> </Modal.Header> <Modal.Body ref='modalBody' @@ -173,7 +152,7 @@ export default class ChannelInviteModal extends React.Component { <button type='button' className='btn btn-default' - onClick={this.props.onModalDismissed} + onClick={this.props.onHide} > {'Close'} </button> @@ -185,5 +164,6 @@ export default class ChannelInviteModal extends React.Component { ChannelInviteModal.propTypes = { show: React.PropTypes.bool.isRequired, - onModalDismissed: React.PropTypes.func.isRequired + onHide: React.PropTypes.func.isRequired, + channel: React.PropTypes.object.isRequired }; diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx index f07fc166a..d1b9df988 100644 --- a/web/react/components/channel_members_modal.jsx +++ b/web/react/components/channel_members_modal.jsx @@ -69,16 +69,9 @@ export default class ChannelMembersModal extends React.Component { memberList.sort(compareByUsername); nonmemberList.sort(compareByUsername); - const channel = ChannelStore.getCurrent(); - let channelName = ''; - if (channel) { - channelName = channel.display_name; - } - return { nonmemberList, - memberList, - channelName + memberList }; } onShow() { @@ -169,7 +162,7 @@ export default class ChannelMembersModal extends React.Component { onHide={this.props.onModalDismissed} > <Modal.Header closeButton={true}> - <Modal.Title><span className='name'>{this.state.channelName}</span>{' Members'}</Modal.Title> + <Modal.Title><span className='name'>{this.props.channel.display_name}</span>{' Members'}</Modal.Title> <a className='btn btn-md btn-primary' href='#' @@ -205,7 +198,8 @@ export default class ChannelMembersModal extends React.Component { </Modal> <ChannelInviteModal show={this.state.showInviteModal} - onModalDismissed={() => this.setState({showInviteModal: false})} + onHide={() => this.setState({showInviteModal: false})} + channel={this.props.channel} /> </div> ); @@ -218,5 +212,6 @@ ChannelMembersModal.defaultProps = { ChannelMembersModal.propTypes = { show: React.PropTypes.bool.isRequired, - onModalDismissed: React.PropTypes.func.isRequired + onModalDismissed: React.PropTypes.func.isRequired, + channel: React.PropTypes.object.isRequired }; diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx index f5d5ab28b..f7f77f48a 100644 --- a/web/react/components/member_list_item.jsx +++ b/web/react/components/member_list_item.jsx @@ -31,9 +31,7 @@ export default class MemberListItem extends React.Component { var timestamp = UserStore.getCurrentUser().update_at; var invite; - if (member.invited && this.props.handleInvite) { - invite = <span className='member-role'>Added</span>; - } else if (this.props.handleInvite) { + if (this.props.handleInvite) { invite = ( <a onClick={this.handleInvite} diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index 6c3bfc7db..3bdc9efac 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -44,7 +44,6 @@ export default class Navbar extends React.Component { state.showEditChannelPurposeModal = false; state.showEditChannelHeaderModal = false; state.showMembersModal = false; - state.showInviteModal = false; this.state = state; } getStateFromStores() { @@ -171,13 +170,13 @@ export default class Navbar extends React.Component { if (!isDirect && !ChannelStore.isDefault(channel)) { addMembersOption = ( <li role='presentation'> - <a + <ToggleModalButton role='menuitem' - href='#' - onClick={() => this.setState({showInviteModal: true})} + dialogType={ChannelInviteModal} + dialogProps={{channel}} > {'Add Members'} - </a> + </ToggleModalButton> </li> ); @@ -475,10 +474,7 @@ export default class Navbar extends React.Component { <ChannelMembersModal show={this.state.showMembersModal} onModalDismissed={() => this.setState({showMembersModal: false})} - /> - <ChannelInviteModal - show={this.state.showInviteModal} - onModalDismissed={() => this.setState({showInviteModal: false})} + channel={{channel}} /> </div> ); diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 6d6694fec..631bd1872 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -3,7 +3,6 @@ import PostsView from './posts_view.jsx'; import LoadingScreen from './loading_screen.jsx'; -import ChannelInviteModal from './channel_invite_modal.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import PostStore from '../stores/post_store.jsx'; @@ -13,7 +12,7 @@ import * as EventHelpers from '../dispatcher/event_helpers.jsx'; import Constants from '../utils/constants.jsx'; -import {createChannelIntroMessage} from '../utils/channel_intro_mssages.jsx'; +import {createChannelIntroMessage} from '../utils/channel_intro_messages.jsx'; export default class PostsViewContainer extends React.Component { constructor() { @@ -177,7 +176,7 @@ export default class PostsViewContainer extends React.Component { loadMorePostsBottomClicked={() => {}} showMoreMessagesTop={!this.state.atTop[this.state.currentChannelIndex]} showMoreMessagesBottom={false} - introText={channel ? createChannelIntroMessage(channel, () => this.setState({showInviteModal: true})) : null} + introText={channel ? createChannelIntroMessage(channel) : null} messageSeparatorTime={this.state.currentLastViewed} /> ); @@ -194,10 +193,6 @@ export default class PostsViewContainer extends React.Component { return ( <div id='post-list'> {postListCtls} - <ChannelInviteModal - show={this.state.showInviteModal} - onModalDismissed={() => this.setState({showInviteModal: false})} - /> </div> ); } diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx index d111094e7..2edcd8b37 100644 --- a/web/react/components/rhs_thread.jsx +++ b/web/react/components/rhs_thread.jsx @@ -101,6 +101,15 @@ export default class RhsThread extends React.Component { } if (currentPosts.posts[currentPosts.order[0]].channel_id === currentSelected.posts[currentSelected.order[0]].channel_id) { + for (var key in currentSelected.posts) { + if (currentSelected.posts.hasOwnProperty(key)) { + var post = currentSelected.posts[key]; + if (post.pending_post_id) { + Reflect.deleteProperty(currentSelected.posts, key); + } + } + } + for (var postId in currentPosts.posts) { if (currentPosts.posts.hasOwnProperty(postId)) { currentSelected.posts[postId] = currentPosts.posts[postId]; diff --git a/web/react/components/suggestion/search_suggestion_list.jsx b/web/react/components/suggestion/search_suggestion_list.jsx index 542d28ddd..3378a33a0 100644 --- a/web/react/components/suggestion/search_suggestion_list.jsx +++ b/web/react/components/suggestion/search_suggestion_list.jsx @@ -35,7 +35,7 @@ export default class SearchSuggestionList extends SuggestionList { } render() { - if (this.state.items.length === 0 || !this.props.show) { + if (this.state.items.length === 0) { return null; } diff --git a/web/react/components/suggestion/suggestion_box.jsx b/web/react/components/suggestion/suggestion_box.jsx index 4ca461e82..4cfb38f8e 100644 --- a/web/react/components/suggestion/suggestion_box.jsx +++ b/web/react/components/suggestion/suggestion_box.jsx @@ -13,7 +13,6 @@ export default class SuggestionBox extends React.Component { super(props); this.handleDocumentClick = this.handleDocumentClick.bind(this); - this.handleFocus = this.handleFocus.bind(this); this.handleChange = this.handleChange.bind(this); this.handleCompleteWord = this.handleCompleteWord.bind(this); @@ -21,10 +20,6 @@ export default class SuggestionBox extends React.Component { this.handlePretextChanged = this.handlePretextChanged.bind(this); this.suggestionId = Utils.generateId(); - - this.state = { - focused: false - }; } componentDidMount() { @@ -49,27 +44,11 @@ export default class SuggestionBox extends React.Component { } handleDocumentClick(e) { - if (!this.state.focused) { - return; - } - const container = $(ReactDOM.findDOMNode(this)); if (!(container.is(e.target) || container.has(e.target).length > 0)) { // we can't just use blur for this because it fires and hides the children before // their click handlers can be called - this.setState({ - focused: false - }); - } - } - - handleFocus() { - this.setState({ - focused: true - }); - - if (this.props.onFocus) { - this.props.onFocus(); + EventHelpers.emitClearSuggestions(this.suggestionId); } } @@ -134,7 +113,6 @@ export default class SuggestionBox extends React.Component { render() { const newProps = Object.assign({}, this.props, { - onFocus: this.handleFocus, onChange: this.handleChange, onKeyDown: this.handleKeyDown }); @@ -162,10 +140,7 @@ export default class SuggestionBox extends React.Component { return ( <div> {textbox} - <SuggestionListComponent - suggestionId={this.suggestionId} - show={this.state.focused} - /> + <SuggestionListComponent suggestionId={this.suggestionId} /> </div> ); } @@ -184,6 +159,5 @@ SuggestionBox.propTypes = { // explicitly name any input event handlers we override and need to manually call onChange: React.PropTypes.func, - onKeyDown: React.PropTypes.func, - onFocus: React.PropTypes.func + onKeyDown: React.PropTypes.func }; diff --git a/web/react/components/suggestion/suggestion_list.jsx b/web/react/components/suggestion/suggestion_list.jsx index 87021fd94..e3ccd0f08 100644 --- a/web/react/components/suggestion/suggestion_list.jsx +++ b/web/react/components/suggestion/suggestion_list.jsx @@ -82,7 +82,7 @@ export default class SuggestionList extends React.Component { } render() { - if (this.state.items.length === 0 || !this.props.show) { + if (this.state.items.length === 0) { return null; } @@ -121,6 +121,5 @@ export default class SuggestionList extends React.Component { } SuggestionList.propTypes = { - suggestionId: React.PropTypes.string.isRequired, - show: React.PropTypes.bool.isRequired + suggestionId: React.PropTypes.string.isRequired }; diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx index dc3865c68..c464258de 100644 --- a/web/react/components/user_settings/user_settings_display.jsx +++ b/web/react/components/user_settings/user_settings_display.jsx @@ -241,7 +241,7 @@ export default class UserSettingsDisplay extends React.Component { const inputs = [ <div key='userDisplayNameOptions'> <div - className='input-group theme-group dropdown' + className='dropdown' > <select className='form-control' @@ -251,9 +251,6 @@ export default class UserSettingsDisplay extends React.Component { > {options} </select> - <span className={'input-group-addon ' + Constants.FONTS[this.state.selectedFont]}> - {this.state.selectedFont} - </span> </div> <div><br/>{'Select the font displayed in the Mattermost user interface.'}</div> </div> @@ -312,12 +309,12 @@ export default class UserSettingsDisplay extends React.Component { <div className='user-settings'> <h3 className='tab-header'>{'Display Settings'}</h3> <div className='divider-dark first'/> + {fontSection} + <div className='divider-dark'/> {clockSection} <div className='divider-dark'/> {nameFormatSection} <div className='divider-dark'/> - {fontSection} - <div className='divider-dark'/> </div> </div> ); diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx index 31be51c5e..306c59e8b 100644 --- a/web/react/dispatcher/event_helpers.jsx +++ b/web/react/dispatcher/event_helpers.jsx @@ -141,3 +141,10 @@ export function emitCompleteWordSuggestion(suggestionId, term = '') { term }); } + +export function emitClearSuggestions(suggestionId) { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS, + id: suggestionId + }); +} diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index 2e3a26cff..ff6ae45ea 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -1,6 +1,8 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import {generateId} from '../utils/utils.jsx'; + function getPrefix() { if (global.window.mm_user) { return global.window.mm_user.id + '_'; @@ -26,6 +28,7 @@ class BrowserStoreClass { this.clearAll = this.clearAll.bind(this); this.checkedLocalStorageSupported = ''; this.signalLogout = this.signalLogout.bind(this); + this.isSignallingLogout = this.isSignallingLogout.bind(this); var currentVersion = sessionStorage.getItem('storage_version'); if (currentVersion !== global.window.mm_config.Version) { @@ -113,11 +116,19 @@ class BrowserStoreClass { signalLogout() { if (this.isLocalStorageSupported()) { - localStorage.setItem('__logout__', 'yes'); + // PLT-1285 store an identifier in session storage so we can catch if the logout came from this tab on IE11 + const logoutId = generateId(); + + sessionStorage.setItem('__logout__', logoutId); + localStorage.setItem('__logout__', logoutId); localStorage.removeItem('__logout__'); } } + isSignallingLogout(logoutId) { + return logoutId === sessionStorage.getItem('__logout__'); + } + /** * Preforms the given action on each item that has the given prefix * Signature for action is action(key, value) @@ -151,7 +162,14 @@ class BrowserStoreClass { } clear() { + // don't clear the logout id so IE11 can tell which tab sent a logout request + const logoutId = sessionStorage.getItem('__logout__'); + sessionStorage.clear(); + + if (logoutId) { + sessionStorage.setItem('__logout__', logoutId); + } } clearAll() { @@ -185,3 +203,4 @@ class BrowserStoreClass { var BrowserStore = new BrowserStoreClass(); export default BrowserStore; +window.BrowserStore = BrowserStore; diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index 5dec86951..0bfde77b4 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -167,18 +167,7 @@ class ChannelStoreClass extends EventEmitter { this.emitChange(); } getCurrentExtraInfo() { - var currentId = this.getCurrentId(); - var extra = null; - - if (currentId) { - extra = this.pGetExtraInfos()[currentId]; - } - - if (extra == null) { - extra = {members: []}; - } - - return extra; + return this.getExtraInfo(this.getCurrentId()); } getExtraInfo(channelId) { var extra = null; @@ -187,7 +176,10 @@ class ChannelStoreClass extends EventEmitter { extra = this.pGetExtraInfos()[channelId]; } - if (extra == null) { + if (extra) { + // create a defensive copy + extra = JSON.parse(JSON.stringify(extra)); + } else { extra = {members: []}; } diff --git a/web/react/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx index 182f5810f..2250ec234 100644 --- a/web/react/stores/suggestion_store.jsx +++ b/web/react/stores/suggestion_store.jsx @@ -244,6 +244,11 @@ class SuggestionStore extends EventEmitter { this.emitSuggestionsChanged(id); } break; + case ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS: + this.clearSuggestions(id); + this.clearSelection(id); + this.emitSuggestionsChanged(id); + break; case ActionTypes.SUGGESTION_SELECT_NEXT: this.selectNext(id); this.emitSuggestionsChanged(id); diff --git a/web/react/utils/channel_intro_mssages.jsx b/web/react/utils/channel_intro_messages.jsx index 6f83778c9..9685f94b0 100644 --- a/web/react/utils/channel_intro_mssages.jsx +++ b/web/react/utils/channel_intro_messages.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import * as Utils from './utils.jsx'; +import ChannelInviteModal from '../components/channel_invite_modal.jsx'; import EditChannelHeaderModal from '../components/edit_channel_header_modal.jsx'; import ToggleModalButton from '../components/toggle_modal_button.jsx'; import UserProfile from '../components/user_profile.jsx'; @@ -10,15 +11,15 @@ import Constants from '../utils/constants.jsx'; import TeamStore from '../stores/team_store.jsx'; import * as EventHelpers from '../dispatcher/event_helpers.jsx'; -export function createChannelIntroMessage(channel, showInviteModal) { +export function createChannelIntroMessage(channel) { if (channel.type === 'D') { return createDMIntroMessage(channel); } else if (ChannelStore.isDefault(channel)) { return createDefaultIntroMessage(channel); } else if (channel.name === Constants.OFFTOPIC_CHANNEL) { - return createOffTopicIntroMessage(channel, showInviteModal); + return createOffTopicIntroMessage(channel); } else if (channel.type === 'O' || channel.type === 'P') { - return createStandardIntroMessage(channel, showInviteModal); + return createStandardIntroMessage(channel); } } @@ -62,7 +63,7 @@ export function createDMIntroMessage(channel) { ); } -export function createOffTopicIntroMessage(channel, showInviteModal) { +export function createOffTopicIntroMessage(channel) { return ( <div className='channel-intro'> <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4> @@ -71,13 +72,7 @@ export function createOffTopicIntroMessage(channel, showInviteModal) { <br/> </p> {createSetHeaderButton(channel)} - <a - href='#' - className='intro-links' - onClick={showInviteModal} - > - <i className='fa fa-user-plus'></i>{'Invite others to this channel'} - </a> + {createInviteChannelMemberButton(channel, 'channel')} </div> ); } @@ -122,7 +117,7 @@ export function createDefaultIntroMessage(channel) { ); } -export function createStandardIntroMessage(channel, showInviteModal) { +export function createStandardIntroMessage(channel) { var uiName = channel.display_name; var creatorName = ''; @@ -162,17 +157,23 @@ export function createStandardIntroMessage(channel, showInviteModal) { <br/> </p> {createSetHeaderButton(channel)} - <a - className='intro-links' - href='#' - onClick={showInviteModal} - > - <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType} - </a> + {createInviteChannelMemberButton(channel, uiType)} </div> ); } +function createInviteChannelMemberButton(channel, uiType) { + return ( + <ToggleModalButton + className='intro-links' + dialogType={ChannelInviteModal} + dialogProps={{channel}} + > + <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType} + </ToggleModalButton> + ); +} + function createSetHeaderButton(channel) { return ( <ToggleModalButton diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 8164095b9..b641e966b 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -55,6 +55,7 @@ export default { SUGGESTION_PRETEXT_CHANGED: null, SUGGESTION_RECEIVED_SUGGESTIONS: null, + SUGGESTION_CLEAR_SUGGESTIONS: null, SUGGESTION_COMPLETE_WORD: null, SUGGESTION_SELECT_NEXT: null, SUGGESTION_SELECT_PREVIOUS: null diff --git a/web/templates/head.html b/web/templates/head.html index 709edbaec..be4ed2b25 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -58,7 +58,13 @@ $(function () { $(window).bind('storage', function (e) { - if (e.originalEvent.key === '__logout__') { + // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out + if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { + // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) + if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) { + return; + } + console.log('detected logout from a different tab'); window.location.href = '/' + window.mm_team.name; } |