summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md15
-rw-r--r--LICENSE.txt34
-rw-r--r--api/post.go19
-rw-r--r--doc/developer/API.md2
-rw-r--r--doc/developer/tests/test-markdown-lists.md17
-rw-r--r--doc/install/Configuration-Settings.md10
-rw-r--r--doc/install/Troubleshooting.md2
-rw-r--r--doc/integrations/Single-Sign-On/GitHub-Enterprise.md (renamed from doc/integrations/Single-Sign-On/Github.md)0
-rw-r--r--doc/integrations/Single-Sign-On/GitHub.md24
-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/react/utils/utils.jsx22
-rw-r--r--web/templates/head.html8
28 files changed, 200 insertions, 190 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8af1d9efb..18fd7c229 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,6 +11,7 @@ Thank you for your interest in contributing to Mattermost. This guide provides a
2. These projects are intended to be a straight forward first pull requests from new contributors
- If you don't find something appropriate for your interests, please see the full list of tickets [Accepting Pull Requests](https://mattermost.atlassian.net/issues/?filter=10101)
- Also, feel free to fix bugs you find, or items in GitHub issues that the core team has approved, but not yet added to Jira
+ - For feature ideas, please discuss on the [feature ideas forum](http://www.mattermost.org/feature-requests/) before beginning work
3. If you have any questions at all about a ticket, there are several options to ask:
1. Start a topic in the [Mattermost forum](http://forum.mattermost.org/)
@@ -30,15 +31,16 @@ git checkout -b <branch name>
1. Please review the [Mattermost Style Guide](doc/developer/Style-Guide.md) prior to making changes
- To keep code clean and well structured, Mattermost uses ESLint to check that pull requests adhere to style guidelines for React. Code will need to follow Mattermost's React style guidelines in order to pass the automated build tests when a pull request is submitted
+ To keep code clean and well structured, Mattermost uses ESLint to check that pull requests adhere to style guidelines for React. In addition all code is run through the official go formatter tool gofmt. Code will need to follow Mattermost's React style guidelines and the golang official style guide in order to pass the automated build tests when a pull request is submitted.
2. Please make sure to thoroughly test your change before submitting a pull request
+ For any changes to text processing, please run the text processing tests found in the [/tests](https://github.com/mattermost/platform/tree/master/doc/developer/tests) folder in GitHub.
+
Please review the ["Fast, Obvious, Forgiving" experience design principles](http://www.mattermost.org/design-principles/) for Mattermost and check that your feature meets the criteria. Also, for any changes to user interface or help text, please read the changes out loud, as a quick and easy way to catch any inconsitencies
3. For new server-side funcitonality, please include test cases that verify the code performs as you have intended
-
## Submitting a Pull Request
@@ -47,9 +49,14 @@ git checkout -b <branch name>
2. When you submit your pull request please make it against `master` and include the Ticket ID at the beginning of your pull request comment, followed by a colon
- For example, for a ticket ID `PLT-394` start your comment with: `PLT-394:`. See [previously closed pull requests](https://github.com/mattermost/platform/pulls?q=is%3Apr+is%3Aclosed) for examples
+ - All pull requests must have a ticket ID so the issue can be tracked and tested properly. If there is no existing ticket in Jira, please [file an issue in GitHub](http://www.mattermost.org/filing-issues/) so a Jira ticket can be created
+
+3. Please include a comment on the pull request describing the changes
+
+ For new features visible in the UI, please make sure there are enough details explaining how the feature is expected to work. This will be used when testing and writing help documentation.
-3. Once submitted, your pull request will be checked via an automated build process and will be reviewed by at least two members of the Mattermost core team, who may either accept the PR or follow-up with feedback. It would then get merged into `master` for the next release
+4. Once submitted, your pull request will be checked via an automated build process and will be reviewed by at least two members of the Mattermost core team, who may either accept the PR or follow-up with feedback. It would then get merged into `master` for the next release
1. If the build fails, check the error log to narrow down the reason
2. Sometimes one of the multiple build tests will randomly fail due to issues in Travis CI so if you see just one build failure and no clear error message it may be a random issue. Add a comment so the reviewer for your change can re-run the build for you, or close the PR and re-submit and that typically clears the issue
-4. If you've included your mailing address in Step 1, you'll be receiving a [Limited Edition Mattermost Mug](http://forum.mattermost.org/t/limited-edition-mattermost-mugs/143) as a thank you gift after your first pull request has been accepted
+5. If you've included your mailing address in Step 1, you'll be receiving a [Limited Edition Mattermost Mug](http://forum.mattermost.org/t/limited-edition-mattermost-mugs/143) as a thank you gift after your first pull request has been accepted
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/developer/tests/test-markdown-lists.md b/doc/developer/tests/test-markdown-lists.md
index 905f5b0d5..905350d31 100644
--- a/doc/developer/tests/test-markdown-lists.md
+++ b/doc/developer/tests/test-markdown-lists.md
@@ -34,7 +34,7 @@ Verify that all list types render as expected.
2. Charlie
3. Delta
1. Echo
- 1. Foxtrot
+ 2. Foxtrot
```
**Actual:**
@@ -175,3 +175,18 @@ Verify that all list types render as expected.
1. One
2. Two
+
+### Carriage Return and New Line After a List
+
+**Expected:**
+```
+1. One
+ - Two
+This text should be on a new line.
+```
+
+**Actual:**
+1. One
+ - Two
+This text should be on a new line.
+
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/install/Troubleshooting.md b/doc/install/Troubleshooting.md
index 78ab5617d..deae7717d 100644
--- a/doc/install/Troubleshooting.md
+++ b/doc/install/Troubleshooting.md
@@ -22,7 +22,7 @@ The following is a list of common error messages and solutions:
###### `x509: certificate signed by unknown authority` in server logs when attempting to sign-up
- - This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost. You
+ - This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost.
- **Solution:** Set up a load balancer like Ngnix [per production install guide](https://github.com/mattermost/platform/blob/master/doc/install/Production-Ubuntu.md#set-up-nginx-with-ssl-recommended). A ticket exists to [add support for self-signed certificates in future](x509: certificate signed by unknown authority).
###### `panic: runtime error: invalid memory address or nil pointer dereference`
diff --git a/doc/integrations/Single-Sign-On/Github.md b/doc/integrations/Single-Sign-On/GitHub-Enterprise.md
index 6f6633846..6f6633846 100644
--- a/doc/integrations/Single-Sign-On/Github.md
+++ b/doc/integrations/Single-Sign-On/GitHub-Enterprise.md
diff --git a/doc/integrations/Single-Sign-On/GitHub.md b/doc/integrations/Single-Sign-On/GitHub.md
new file mode 100644
index 000000000..56e2d1c72
--- /dev/null
+++ b/doc/integrations/Single-Sign-On/GitHub.md
@@ -0,0 +1,24 @@
+## Configuring GitHub Single-Sign-On (unofficial)
+
+Note: Because the authentication interface of GitHub is similar to that of GitLab, the GitLab SSO feature can be used to unofficially also support GitHub SSO.
+
+Follow these steps to configure Mattermost to use Github as a single-sign-on (SSO) service for team creation, account creation and sign-in using the GitLab SSO interface.
+
+1. Login to your GitHub account and go to the Applications section in Profile Settings.
+2. Add a new application called "Mattermost" with the following as Authorization callback URL:
+ * `<your-mattermost-url>` (example: http://localhost:8065)
+
+3. Submit the application and copy the given _Id_ and _Secret_ into the appropriate _GitLabSettings_ fields in config/config.json
+
+4. Also in config/config.json, set _Enable_ to `true` for the _gitlab_ section, leave _Scope_ blank and use the following for the endpoints:
+ * _AuthEndpoint_: `https://github.com/login/oauth/authorize`
+ * _TokenEndpoint_: `https://github.com/login/oauth/access_token`
+ * _UserApiEndpoint_: `https://api.github.com/user`
+
+6. (Optional) If you would like to force all users to sign-up with GitHub only,
+in the _ServiceSettings_ section of config/config.json set _DisableEmailSignUp_
+to `true`.
+
+6. Restart your Mattermost server to see the changes take effect.
+
+7. Tell the users to set their public email for GitHub at the [Public profile page](https://github.com/settings/profile). Mattermost uses the email to create account.
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/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 788d8a45c..0a52f5b37 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -695,15 +695,19 @@ export function applyTheme(theme) {
}
export function applyFont(fontName) {
- const body = document.querySelector('body');
- const keys = Object.getOwnPropertyNames(body.classList);
- keys.forEach((k) => {
- const className = body.classList[k];
- if (className && className.lastIndexOf('font') === 0) {
- body.classList.remove(className);
+ const body = $('body');
+
+ for (const key of Reflect.ownKeys(Constants.FONTS)) {
+ const className = Constants.FONTS[key];
+
+ if (fontName === key) {
+ if (!body.hasClass(className)) {
+ body.addClass(className);
+ }
+ } else {
+ body.removeClass(className);
}
- });
- body.classList.add(Constants.FONTS[fontName]);
+ }
}
export function changeCss(className, classValue, classRepeat) {
@@ -1238,4 +1242,4 @@ export function getPostTerm(post) {
export function isFeatureEnabled(feature) {
return PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, Constants.FeatureTogglePrefix + feature.label, {value: 'false'}).value === 'true';
-} \ No newline at end of file
+}
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;
}