summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore7
-rw-r--r--LICENSE.txt34
-rw-r--r--api/post.go19
-rw-r--r--doc/developer/API.md2
-rw-r--r--doc/install/Configuration-Settings.md10
-rw-r--r--doc/process/overview.md2
-rw-r--r--web/react/components/channel_header.jsx14
-rw-r--r--web/react/components/channel_invite_modal.jsx38
-rw-r--r--web/react/components/channel_members_modal.jsx17
-rw-r--r--web/react/components/member_list_item.jsx4
-rw-r--r--web/react/components/navbar.jsx14
-rw-r--r--web/react/components/posts_view_container.jsx9
-rw-r--r--web/react/components/suggestion/search_suggestion_list.jsx2
-rw-r--r--web/react/components/suggestion/suggestion_box.jsx32
-rw-r--r--web/react/components/suggestion/suggestion_list.jsx5
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx9
-rw-r--r--web/react/dispatcher/event_helpers.jsx7
-rw-r--r--web/react/stores/browser_store.jsx21
-rw-r--r--web/react/stores/channel_store.jsx18
-rw-r--r--web/react/stores/suggestion_store.jsx5
-rw-r--r--web/react/utils/channel_intro_messages.jsx (renamed from web/react/utils/channel_intro_mssages.jsx)39
-rw-r--r--web/react/utils/constants.jsx1
-rw-r--r--web/templates/head.html8
23 files changed, 141 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/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 f792c610f..3deddd754 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;
}