summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md22
-rw-r--r--api/channel.go8
-rw-r--r--api/channel_test.go3
-rw-r--r--api/post.go5
-rw-r--r--api/templates/signup_team_subject.html2
-rw-r--r--doc/install/Administration.md30
-rw-r--r--doc/install/Production-Ubuntu.md2
-rw-r--r--doc/install/Upgrade-Guide.md10
-rw-r--r--web/react/components/access_history_modal.jsx10
-rw-r--r--web/react/components/activity_log_modal.jsx8
-rw-r--r--web/react/components/channel_header.jsx77
-rw-r--r--web/react/components/channel_info_modal.jsx111
-rw-r--r--web/react/components/channel_notifications_modal.jsx (renamed from web/react/components/channel_notifications.jsx)108
-rw-r--r--web/react/components/create_comment.jsx3
-rw-r--r--web/react/components/create_post.jsx3
-rw-r--r--web/react/components/delete_channel_modal.jsx116
-rw-r--r--web/react/components/delete_post_modal.jsx155
-rw-r--r--web/react/components/docs.jsx41
-rw-r--r--web/react/components/edit_post_modal.jsx3
-rw-r--r--web/react/components/login.jsx16
-rw-r--r--web/react/components/navbar.jsx80
-rw-r--r--web/react/components/post_info.jsx14
-rw-r--r--web/react/components/posts_view.jsx17
-rw-r--r--web/react/components/rename_channel_modal.jsx51
-rw-r--r--web/react/components/rhs_comment.jsx8
-rw-r--r--web/react/components/rhs_root_post.jsx12
-rw-r--r--web/react/components/rhs_thread.jsx2
-rw-r--r--web/react/components/search_results.jsx2
-rw-r--r--web/react/components/sidebar_right_menu.jsx18
-rw-r--r--web/react/components/textbox.jsx15
-rw-r--r--web/react/components/toggle_modal_button.jsx62
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx21
-rw-r--r--web/react/components/user_settings/user_settings_security.jsx48
-rw-r--r--web/react/pages/channel.jsx18
-rw-r--r--web/react/pages/docs.jsx16
-rw-r--r--web/react/stores/browser_store.jsx2
-rw-r--r--web/react/stores/modal_store.jsx8
-rw-r--r--web/react/stores/post_store.jsx19
-rw-r--r--web/react/utils/async_client.jsx2
-rw-r--r--web/react/utils/channel_intro_mssages.jsx1
-rw-r--r--web/react/utils/constants.jsx3
-rw-r--r--web/react/utils/utils.jsx15
-rw-r--r--web/sass-files/sass/partials/_content.scss1
-rw-r--r--web/sass-files/sass/partials/_files.scss8
-rw-r--r--web/sass-files/sass/partials/_forms.scss4
-rw-r--r--web/sass-files/sass/partials/_headers.scss31
-rw-r--r--web/sass-files/sass/partials/_post.scss68
-rw-r--r--web/sass-files/sass/partials/_post_right.scss17
-rw-r--r--web/sass-files/sass/partials/_responsive.scss152
-rw-r--r--web/sass-files/sass/partials/_search.scss11
-rw-r--r--web/sass-files/sass/partials/_sidebar--left.scss2
-rw-r--r--web/sass-files/sass/partials/_sidebar--menu.scss5
-rw-r--r--web/sass-files/sass/partials/_sidebar--right.scss26
l---------web/static/help/Messaging.md1
-rw-r--r--web/templates/docs.html24
-rw-r--r--web/web.go11
56 files changed, 882 insertions, 646 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e423557d..702d96c7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,14 @@
# Mattermost Changelog
-## Release v1.2.0
+## Release v1.2.1
-- **Final release anticipated:** 2015-11-16
+- **Released:** 2015-11-16
+
+### Security Notice
+
+Mattermost v1.2.1 is a bug fix release addressing a security issue in v1.2.0 affecting a newly introduced outgoing webhooks feature. Specifically, in v1.2.0 there was a check missing from outgoing webhooks, so a team member creating outgoing webhooks could in theory find a way to listen to messages in private channels containing popular words like "a", "the", "at", etc. For added security, Mattermost v1.2.1 now installs with incoming and outgoing webhooks disabled by default.
+
+To limit the impact of this security issue, Mattermost v1.2.0 has been removed from the source repo. It is recommended that anyone who's installed v1.2.0 upgrade to v1.2.1 via [the procedure described in the Mattermost Upgrade Guide](https://github.com/mattermost/platform/blob/master/doc/install/Upgrade-Guide.md).
### Release Highlights
@@ -10,7 +16,7 @@
- Mattermost users can now interact with external applications using [outgoing webhooks](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Outgoing-Webhooks.md)
- An [application template](https://github.com/mattermost/mattermost-integration-giphy) demonstrating user queries sent to the Giphy search engine via Mattermost webhooks now available
-- A community application, [Matterbrige](https://github.com/42wim/matterbridge?files=1), shows how to use webhooks to connect Mattermost with IRC
+- A community application, [Matterbridge](https://github.com/42wim/matterbridge?files=1), shows how to use webhooks to connect Mattermost with IRC
#### Search Scope Modifiers
@@ -79,13 +85,14 @@ System Console
- New statistics page
- Configurable option to create an account directly from team page
-#### Bug Fixes
+#### Bug Fixes
- Various fixes to theme colors
- Fixed issue with the centre channel scroll position jumping when right hand side was opened and closed
- Added support for simultaneous login to different teams in different browser tabs
- Incoming webhooks no longer disrupted when channel is deleted
- You can now paste a Mattermost incoming webhook URL into the same field designed for a Slack URL and integrations will work
+
### Compatibility
- IE 11 new minimum version for IE, since IE 10 share fell below 5% on desktop
@@ -98,7 +105,8 @@ Multiple settings were added to [`config.json`](./config/config.json). These opt
- Added: `"RestrictTeamNames": true` to control whether team names can contain reserved words like www, admin, support, test, etc.
- Added: `"EnableTeamListing": false` to control whether teams can be listed on the root page of the site
- Under `ServiceSettings` in `config.json`
- - Added: `EnableOutgoingWebhooks": false` to control whether outgoing webhooks are enabled
+ - Added: `"EnableOutgoingWebhooks": false` to control whether outgoing webhooks are enabled
+ - Changed: `"EnableIncomingWebhooks": true` to `"EnableIncomingWebhooks": false` to turn incoming webhooks off by default, to increase security of default install. Documentation updated to enable webhooks before use.
#### Database Changes from v1.1 to v1.2
@@ -153,6 +161,10 @@ Many thanks to our external contributors. In no particular order:
- [yuvipanda](https://github.com/yuvipanda)
- [toyorg](https://github.com/toyorg)
+## Release v1.2.0 (Redacted Release)
+
+- **Final release:** 2015-11-16 (**Note:** This release was removed from public availability and replaced by v1.2.1 owing to a security issue with the new outgoing webhooks feature. See v1.2.1 Release Notes for details).
+
## Release v1.1.1 (Bug Fix Release)
Released 2015-10-20
diff --git a/api/channel.go b/api/channel.go
index 75ca9680d..99640e71a 100644
--- a/api/channel.go
+++ b/api/channel.go
@@ -205,9 +205,11 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
}
if oldChannel.Name == model.DEFAULT_CHANNEL {
- c.Err = model.NewAppError("updateChannel", "Cannot update the default channel "+model.DEFAULT_CHANNEL, "")
- c.Err.StatusCode = http.StatusForbidden
- return
+ if (len(channel.Name) > 0 && channel.Name != oldChannel.Name) || (len(channel.Type) > 0 && channel.Type != oldChannel.Type) {
+ c.Err = model.NewAppError("updateChannel", "Tried to perform an invalid update of the default channel "+model.DEFAULT_CHANNEL, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
}
oldChannel.Header = channel.Header
diff --git a/api/channel_test.go b/api/channel_test.go
index faed387dd..e7e1f4eb0 100644
--- a/api/channel_test.go
+++ b/api/channel_test.go
@@ -215,8 +215,9 @@ func TestUpdateChannel(t *testing.T) {
for _, c := range data.Channels {
if c.Name == model.DEFAULT_CHANNEL {
c.Header = "new header"
+ c.Name = "pseudo-square"
if _, err := Client.UpdateChannel(c); err == nil {
- t.Fatal("should have errored on updating default channel")
+ t.Fatal("should have errored on updating default channel name")
}
break
}
diff --git a/api/post.go b/api/post.go
index ef70e1336..0860fd299 100644
--- a/api/post.go
+++ b/api/post.go
@@ -544,11 +544,10 @@ func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team,
alreadySeen := make(map[string]string)
for _, session := range sessions {
- if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" {
-
+ if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" && strings.HasPrefix(session.DeviceId, "apple:") {
alreadySeen[session.DeviceId] = session.DeviceId
- utils.SendAppleNotifyAndForget(session.DeviceId, subjectPage.Render(), 1)
+ utils.SendAppleNotifyAndForget(strings.TrimPrefix(session.DeviceId, "apple:"), subjectPage.Render(), 1)
}
}
}
diff --git a/api/templates/signup_team_subject.html b/api/templates/signup_team_subject.html
index 236b288fa..4fc5b3d72 100644
--- a/api/templates/signup_team_subject.html
+++ b/api/templates/signup_team_subject.html
@@ -1 +1 @@
-{{define "signup_team_subject"}}Invitation to {{ .ClientCfg.SiteName }}{{end}} \ No newline at end of file
+{{define "signup_team_subject"}}{{ .ClientCfg.SiteName }} Team Setup{{end}} \ No newline at end of file
diff --git a/doc/install/Administration.md b/doc/install/Administration.md
index 76ec78abd..15bd07778 100644
--- a/doc/install/Administration.md
+++ b/doc/install/Administration.md
@@ -33,11 +33,35 @@ For help and support around your GitLab Mattermost deployment please see:
- [GitLab Mattermost discussion forum](https://forum.mattermost.org/c/general/gitlab)
- [GitLab Mattermost issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-mattermost/issues)
-### Setting up realtime notifications from GitLab to Mattermost
+### Connecting Mattermost to integrations with incoming webhooks
-To set up standard notification from GitLab to Mattermost [follow these steps](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Incoming-Webhooks.md#connecting-mattermost-to-gitlab-using-slack-ui).
+#### Connecting Mattermost to GitLab for Slack-equivalent functionality.
-To set up a set of fully customizable realtime notifications from GitLab to Mattermost you can run the [GitLab Integration Service for Mattermost](https://github.com/mattermost/mattermost-integration-gitlab).
+Mattermost is designed to be _Slack-compatible, not Slack-limited_ and supports integration via the Slack UI in GitLab, as well as fully customizable integrations.
+
+To enable this:
+
+1. In Mattermost, from a team site where you have System Administration privileges, from the main menu go to **System Console** > **Serice Settings** > **Enable Incoming Webhooks** and select **true** then click **Save**
+
+2. Follow the step-by-step example of [connecting Mattermost incoming webhooks to GitLab's Slack webhooks UI](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Incoming-Webhooks.md#connecting-mattermost-to-gitlab-using-slack-ui).
+
+#### Connecting Mattermost to GitLab for functionality exceeding Slack integration.
+
+To enable this:
+
+1. In Mattermost, from a team site where you have System Administration privileges, from the main menu go to **System Console** > **Serice Settings** > **Enable Incoming Webhooks** and select **true** then click **Save**
+
+2. Set up the [GitLab Integration Service for Mattermost](https://github.com/mattermost/mattermost-integration-gitlab).
+
+### Connecting Mattermost to integrations with outgoing webhooks
+
+Mattermost offers Slack-compatible outgoing webhooks, that can connect to applications created by the Mattermost community, such as [Hubot](https://www.npmjs.com/package/hubot-mattermost) and [IRC](https://github.com/42wim/matterbridge) support.
+
+To enable this:
+
+1. In Mattermost, from a team site where you have System Administration privileges, from the main menu go to **System Console** > **Serice Settings** > **Enable Outgoing Webhooks** and select **true** then click **Save**
+
+2. Select a [Mattermost community application](http://www.mattermost.org/community-applications/) using outgoing webhooks--or adapt a Slack application using the same outgoing webhook standard--and follow the setup instructions provided.
### Upgrading GitLab Mattermost manually
diff --git a/doc/install/Production-Ubuntu.md b/doc/install/Production-Ubuntu.md
index 098707ada..482c2a0ba 100644
--- a/doc/install/Production-Ubuntu.md
+++ b/doc/install/Production-Ubuntu.md
@@ -38,7 +38,7 @@
## Set up Mattermost Server
1. For the purposes of this guide we will assume this server has an IP address of 10.10.10.2
1. Download the latest Mattermost Server by typing:
- * ``` wget https://github.com/mattermost/platform/releases/download/v1.1.0/mattermost.tar.gz```
+ * ``` wget https://github.com/mattermost/platform/releases/download/v1.2.1/mattermost.tar.gz```
1. Unzip the Mattermost Server by typing:
* ``` tar -xvzf mattermost.tar.gz```
1. For the sake of making this guide simple we located the files at `/home/ubuntu/mattermost`. In the future we will give guidance for storing under `/opt`.
diff --git a/doc/install/Upgrade-Guide.md b/doc/install/Upgrade-Guide.md
index 7f4eeaeb9..e01bdb9a0 100644
--- a/doc/install/Upgrade-Guide.md
+++ b/doc/install/Upgrade-Guide.md
@@ -4,6 +4,8 @@
Each release of Mattermost contains logic to upgrade it from the previously major build version. For example, version 1.2 upgrades the database and configuration data schema for a Mattermost version 1.1 server. The following procedure outlines how to upgrade Mattermost to the next major release version.
+If you're upgrading across multiple major releases, from 1.0.x to 1.2.x for example, please run the following procedure once for each incremental upgrade, in sequential order.
+
1. Download the **next major build release** of your server
1. Determine the current version of your Mattermost server
1. Go to any team site, opening the main menu at the top right of the left-hand sidebar and selecting **About Mattermost**
@@ -11,7 +13,7 @@ Each release of Mattermost contains logic to upgrade it from the previously majo
1. For example, if your current version is 1.1.0, you want to select version 1.2.0.
1. In some cases there will be **minor build releases**, such as 1.2.1 and 1.2.2. The minor build number indicates a bug fix or security issue release. Testing on minor build versions is less extensive than on major build versions and it is recommended that you use the minor build only if you need the specific additions included.
3. Review Release Notes
- 1. Check the release notes for the version of Mattermost you are able to install, and note any setting changes in the **Compatibility** section that apply to your deployment
+ 1. Check the release notes for the version of Mattermost you are able to install, and note any setting changes in the **Compatibility** section that apply to your deployment (Release notes across versions are available from the [CHANGELOG](https://github.com/mattermost/platform/blob/master/CHANGELOG.md)).
4. Download the `mattermost.tar.gz` file with the correct version for your upgrade
1. You can use `wget` to retrieve a specific version. For example, to download v1.1.0 run `wget https://github.com/mattermost/platform/releases/download/v1.1.0/mattermost.tar.gz`
2. Stop the Mattermost Server
@@ -26,9 +28,9 @@ Each release of Mattermost contains logic to upgrade it from the previously majo
5. Restore the state of your server by copying the backed up version of `config.json` in place of the default `config.json`
6. Start your server and address any setting changes relevant in the latest version of Mattermost
1. Run `sudo start mattermost`
- 2. The server will upgrade your database schema to be compatibile with the new release, as well as upgrade your `config.json` file to the latest format, using default values for new settings added
- 3. Go to the System Console to update any settings that have been added or modified based on the **Compatibility** documentation in the release notes
-7. Test the system is working by going to the URL of an existing team
+ 2. Go to the **System Console** to update any settings that have been added or modified based on the **Compatibility** section in the release notes of the version you are installing (Release notes across versions are available from the [CHANGELOG](https://github.com/mattermost/platform/blob/master/CHANGELOG.md)).
+ 1. Opening the System Console and saving a change will upgrade your `config.json` schema to the latest version using default values for new settings added
+7. Test the system is working by going to the URL of an existing team. You may need to refresh your Mattermost browser page in order to get the latest updates from the upgrade
### Upgrading from Mattermost Beta (Version 0.7)
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index ab5686720..65b80dfb7 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -14,8 +14,8 @@ export default class AccessHistoryModal extends React.Component {
this.onAuditChange = this.onAuditChange.bind(this);
this.handleMoreInfo = this.handleMoreInfo.bind(this);
- this.onHide = this.onHide.bind(this);
this.onShow = this.onShow.bind(this);
+ this.onHide = this.onHide.bind(this);
this.formatAuditInfo = this.formatAuditInfo.bind(this);
this.handleRevokedSession = this.handleRevokedSession.bind(this);
@@ -39,10 +39,14 @@ export default class AccessHistoryModal extends React.Component {
}
onHide() {
this.setState({moreInfo: []});
- this.props.onModalDismissed();
+ this.props.onHide();
}
componentDidMount() {
UserStore.addAuditsChangeListener(this.onAuditChange);
+
+ if (this.props.show) {
+ this.onShow();
+ }
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
@@ -406,5 +410,5 @@ export default class AccessHistoryModal extends React.Component {
AccessHistoryModal.propTypes = {
show: React.PropTypes.bool.isRequired,
- onModalDismissed: React.PropTypes.func.isRequired
+ onHide: React.PropTypes.func.isRequired
};
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index af423a601..5824ce7e2 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -58,10 +58,14 @@ export default class ActivityLogModal extends React.Component {
}
onHide() {
this.setState({moreInfo: []});
- this.props.onModalDismissed();
+ this.props.onHide();
}
componentDidMount() {
UserStore.addSessionsChangeListener(this.onListenerChange);
+
+ if (this.props.show) {
+ this.onShow();
+ }
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
@@ -178,5 +182,5 @@ export default class ActivityLogModal extends React.Component {
ActivityLogModal.propTypes = {
show: React.PropTypes.bool.isRequired,
- onModalDismissed: React.PropTypes.func.isRequired
+ onHide: React.PropTypes.func.isRequired
};
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index a8d4ec100..ffe7cbb5d 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -5,8 +5,12 @@ const NavbarSearchBox = require('./search_bar.jsx');
const MessageWrapper = require('./message_wrapper.jsx');
const PopoverListMembers = require('./popover_list_members.jsx');
const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
+const ChannelInfoModal = require('./channel_info_modal.jsx');
const ChannelInviteModal = require('./channel_invite_modal.jsx');
const ChannelMembersModal = require('./channel_members_modal.jsx');
+const ChannelNotificationsModal = require('./channel_notifications_modal.jsx');
+const DeleteChannelModal = require('./delete_channel_modal.jsx');
+const ToggleModalButton = require('./toggle_modal_button.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
@@ -180,15 +184,13 @@ export default class ChannelHeader extends React.Component {
key='view_info'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- data-toggle='modal'
- data-target='#channel_info'
- data-channelid={channel.id}
- href='#'
+ dialogType={ChannelInfoModal}
+ dialogProps={{channel}}
>
{'View Info'}
- </a>
+ </ToggleModalButton>
</li>
);
@@ -263,58 +265,55 @@ export default class ChannelHeader extends React.Component {
key='notification_preferences'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#channel_notifications'
- data-title={channel.display_name}
- data-channelid={channel.id}
+ dialogType={ChannelNotificationsModal}
+ dialogProps={{channel}}
>
{'Notification Preferences'}
- </a>
+ </ToggleModalButton>
</li>
);
- if (!ChannelStore.isDefault(channel)) {
- if (isAdmin) {
- dropdownContents.push(
- <li
- key='rename_channel'
- role='presentation'
+ if (isAdmin) {
+ dropdownContents.push(
+ <li
+ key='rename_channel'
+ role='presentation'
+ >
+ <a
+ role='menuitem'
+ href='#'
+ data-toggle='modal'
+ data-target='#rename_channel'
+ data-display={channel.display_name}
+ data-name={channel.name}
+ data-channelid={channel.id}
>
- <a
- role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#rename_channel'
- data-display={channel.display_name}
- data-name={channel.name}
- data-channelid={channel.id}
- >
- {'Rename '}{channelTerm}{'...'}
- </a>
- </li>
- );
+ {'Rename '}{channelTerm}{'...'}
+ </a>
+ </li>
+ );
+
+ if (!ChannelStore.isDefault(channel)) {
dropdownContents.push(
<li
key='delete_channel'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#delete_channel'
- data-title={channel.display_name}
- data-channelid={channel.id}
+ dialogType={DeleteChannelModal}
+ dialogProps={{channel}}
>
{'Delete '}{channelTerm}{'...'}
- </a>
+ </ToggleModalButton>
</li>
);
}
+ }
+ if (!ChannelStore.isDefault(channel)) {
dropdownContents.push(
<li
key='leave_channel'
diff --git a/web/react/components/channel_info_modal.jsx b/web/react/components/channel_info_modal.jsx
index bccd8d0b9..18e125de3 100644
--- a/web/react/components/channel_info_modal.jsx
+++ b/web/react/components/channel_info_modal.jsx
@@ -1,88 +1,57 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var ChannelStore = require('../stores/channel_store.jsx');
-
-export default class CommandList extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- channel_id: ChannelStore.getCurrentId()
- };
- }
-
- componentDidMount() {
- var self = this;
- if (this.refs.modal) {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) {
- var button = e.relatedTarget;
- self.setState({channel_id: $(button).attr('data-channelid')});
- });
- }
- }
+const Modal = ReactBootstrap.Modal;
+export default class ChannelInfoModal extends React.Component {
render() {
- var channel = ChannelStore.get(this.state.channel_id);
-
+ let channel = this.props.channel;
if (!channel) {
- channel = {};
- channel.display_name = 'No Channel Found';
- channel.name = 'No Channel Found';
- channel.id = 'No Channel Found';
+ channel = {
+ display_name: 'No Channel Found',
+ name: 'No Channel Found',
+ id: 'No Channel Found'
+ };
}
return (
- <div
- className='modal fade'
- ref='modal'
- id='channel_info'
- tabIndex='-1'
- role='dialog'
- aria-hidden='true'
+ <Modal
+ show={this.props.show}
+ onHide={this.props.onHide}
>
- <div className='modal-dialog'>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- id='myModalLabel'
- >
- <span className='name'>{channel.display_name}</span>
- </h4>
- </div>
- <div className='modal-body'>
- <div className='row form-group'>
- <div className='col-sm-3 info__label'>Channel Name: </div>
+ <Modal.Header closeButtton={true}>
+ {channel.display_name}
+ </Modal.Header>
+ <Modal.Body ref='modalBody'>
+ <div className='row form-group'>
+ <div className='col-sm-3 info__label'>{'Channel Name:'}</div>
<div className='col-sm-9'>{channel.display_name}</div>
- </div>
- <div className='row form-group'>
- <div className='col-sm-3 info__label'>Channel Handle:</div>
+ </div>
+ <div className='row form-group'>
+ <div className='col-sm-3 info__label'>{'Channel Handle:'}</div>
<div className='col-sm-9'>{channel.name}</div>
- </div>
- <div className='row'>
- <div className='col-sm-3 info__label'>Channel ID:</div>
- <div className='col-sm-9'>{channel.id}</div>
- </div>
</div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >Close</button>
+ <div className='row'>
+ <div className='col-sm-3 info__label'>{'Channel ID:'}</div>
+ <div className='col-sm-9'>{channel.id}</div>
</div>
- </div>
- </div>
- </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.props.onHide}
+ >
+ {'Close'}
+ </button>
+ </Modal.Footer>
+ </Modal>
);
}
}
+
+ChannelInfoModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
+};
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications_modal.jsx
index f57fc12c5..c8bd1c2dc 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications_modal.jsx
@@ -1,15 +1,15 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+var Modal = ReactBootstrap.Modal;
var SettingItemMin = require('./setting_item_min.jsx');
var SettingItemMax = require('./setting_item_max.jsx');
-var Utils = require('../utils/utils.jsx');
var Client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx');
var ChannelStore = require('../stores/channel_store.jsx');
-export default class ChannelNotifications extends React.Component {
+export default class ChannelNotificationsModal extends React.Component {
constructor(props) {
super(props);
@@ -23,35 +23,17 @@ export default class ChannelNotifications extends React.Component {
this.handleSubmitMarkUnreadLevel = this.handleSubmitMarkUnreadLevel.bind(this);
this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
- this.onShow = this.onShow.bind(this);
+ const member = ChannelStore.getMember(props.channel.id);
this.state = {
- notifyLevel: '',
- markUnreadLevel: '',
- title: '',
+ notifyLevel: member.notify_props.desktop,
+ markUnreadLevel: member.notify_props.mark_unread,
channelId: '',
activeSection: ''
};
}
- onShow(e) {
- var button = e.relatedTarget;
- var channelId = button.getAttribute('data-channelid');
-
- const member = ChannelStore.getMember(channelId);
- var notifyLevel = member.notify_props.desktop;
- var markUnreadLevel = member.notify_props.mark_unread;
-
- this.setState({
- notifyLevel,
- markUnreadLevel,
- title: button.getAttribute('data-title'),
- channelId
- });
- }
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
-
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
@@ -62,15 +44,12 @@ export default class ChannelNotifications extends React.Component {
}
const member = ChannelStore.getMember(this.state.channelId);
- var notifyLevel = member.notify_props.desktop;
- var markUnreadLevel = member.notify_props.mark_unread;
- var newState = this.state;
- newState.notifyLevel = notifyLevel;
- newState.markUnreadLevel = markUnreadLevel;
-
- if (!Utils.areObjectsEqual(this.state, newState)) {
- this.setState(newState);
+ if (member.notify_props.desktop !== this.state.notifyLevel || member.notify_props.mark_unread !== this.state.mark_unread) {
+ this.setState({
+ notifyLevel: member.notify_props.desktop,
+ markUnreadLevel: member.notify_props.mark_unread
+ });
}
}
updateSection(section) {
@@ -104,7 +83,6 @@ export default class ChannelNotifications extends React.Component {
}
handleUpdateNotifyLevel(notifyLevel) {
this.setState({notifyLevel});
- ReactDOM.findDOMNode(this.refs.modal).focus();
}
createNotifyLevelSection(serverError) {
var handleUpdateSection;
@@ -262,7 +240,6 @@ export default class ChannelNotifications extends React.Component {
handleUpdateMarkUnreadLevel(markUnreadLevel) {
this.setState({markUnreadLevel});
- ReactDOM.findDOMNode(this.refs.modal).focus();
}
createMarkUnreadLevelSection(serverError) {
@@ -347,48 +324,39 @@ export default class ChannelNotifications extends React.Component {
}
return (
- <div
- className='modal fade'
- id='channel_notifications'
- ref='modal'
- tabIndex='-1'
- role='dialog'
- aria-hidden='true'
+ <Modal
+ show={this.props.show}
+ dialogClassName='settings-modal'
+ onHide={this.props.onHide}
>
- <div className='modal-dialog settings-modal'>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
+ <Modal.Header closeButton={true}>
+ {'Notification Preferences for '}<span className='name'>{this.props.channel.display_name}</span>
+ </Modal.Header>
+ <Modal.Body>
+ <div className='settings-table'>
+ <div className='settings-content'>
+ <div
+ ref='wrapper'
+ className='user-settings'
>
- <span aria-hidden='true'>&times;</span>
- <span className='sr-only'>{'Close'}</span>
- </button>
- <h4 className='modal-title'>Notification Preferences for <span className='name'>{this.state.title}</span></h4>
- </div>
- <div className='modal-body'>
- <div className='settings-table'>
- <div className='settings-content'>
- <div
- ref='wrapper'
- className='user-settings'
- >
- <br/>
- <div className='divider-dark first'/>
- {this.createNotifyLevelSection(serverError)}
- <div className='divider-light'/>
- {this.createMarkUnreadLevelSection(serverError)}
- <div className='divider-dark'/>
- </div>
+ <br/>
+ <div className='divider-dark first'/>
+ {this.createNotifyLevelSection(serverError)}
+ <div className='divider-light'/>
+ {this.createMarkUnreadLevelSection(serverError)}
+ <div className='divider-dark'/>
</div>
- </div>
- {serverError}
</div>
</div>
- </div>
- </div>
+ {serverError}
+ </Modal.Body>
+ </Modal>
);
}
}
+
+ChannelNotificationsModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
+};
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 058594165..22a659ed5 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -194,7 +194,8 @@ export default class CreateComment extends React.Component {
title: 'Comment',
message: lastPost.message,
postId: lastPost.id,
- channelId: lastPost.channel_id
+ channelId: lastPost.channel_id,
+ comments: PostStore.getCommentCount(lastPost)
});
}
}
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 5a69c9bfb..6f25ef608 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -372,7 +372,8 @@ export default class CreatePost extends React.Component {
title: type,
message: lastPost.message,
postId: lastPost.id,
- channelId: lastPost.channel_id
+ channelId: lastPost.channel_id,
+ comments: PostStore.getCommentCount(lastPost)
});
}
}
diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx
index b7d633b38..271f21c3a 100644
--- a/web/react/components/delete_channel_modal.jsx
+++ b/web/react/components/delete_channel_modal.jsx
@@ -1,102 +1,72 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const Client = require('../utils/client.jsx');
const AsyncClient = require('../utils/async_client.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-var TeamStore = require('../stores/team_store.jsx');
+const Client = require('../utils/client.jsx');
+const Modal = ReactBootstrap.Modal;
+const TeamStore = require('../stores/team_store.jsx');
+const Utils = require('../utils/utils.jsx');
export default class DeleteChannelModal extends React.Component {
constructor(props) {
super(props);
this.handleDelete = this.handleDelete.bind(this);
- this.onShow = this.onShow.bind(this);
-
- this.state = {
- title: '',
- channelId: ''
- };
}
+
handleDelete() {
- if (this.state.channelId.length !== 26) {
+ if (this.props.channel.id.length !== 26) {
return;
}
- Client.deleteChannel(this.state.channelId,
- function handleDeleteSuccess() {
+ Client.deleteChannel(
+ this.props.channel.id,
+ () => {
AsyncClient.getChannels(true);
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
},
- function handleDeleteError(err) {
+ (err) => {
AsyncClient.dispatchError(err, 'handleDelete');
}
);
}
- onShow(e) {
- var button = $(e.relatedTarget);
- this.setState({
- title: button.attr('data-title'),
- channelId: button.attr('data-channelid')
- });
- }
- componentDidMount() {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- }
+
render() {
- const channel = ChannelStore.getCurrent();
- let channelType = 'channel';
- if (channel && channel.type === 'P') {
- channelType = 'private group';
- }
+ const channelTerm = Utils.getChannelTerm(this.props.channel.type).toLowerCase();
return (
- <div
- className='modal fade'
- ref='modal'
- id='delete_channel'
- role='dialog'
- tabIndex='-1'
- aria-hidden='true'
+ <Modal
+ show={this.props.show}
+ onHide={this.props.onHide}
>
- <div className='modal-dialog'>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4 className='modal-title'>Confirm DELETE Channel</h4>
- </div>
- <div className='modal-body'>
- <p>
- Are you sure you wish to delete the {this.state.title} {channelType}?
- </p>
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- Cancel
- </button>
- <button
- type='button'
- className='btn btn-danger'
- data-dismiss='modal'
- onClick={this.handleDelete}
- >
- Delete
- </button>
- </div>
- </div>
- </div>
- </div>
+ <Modal.Header closeButton={true}>{'Confirm DELETE Channel'}</Modal.Header>
+ <Modal.Body>
+ {`Are you sure you wish to delete the ${this.props.channel.display_name} ${channelTerm}?`}
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.props.onHide}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ type='button'
+ className='btn btn-danger'
+ data-dismiss='modal'
+ onClick={this.handleDelete}
+ >
+ {'Delete'}
+ </button>
+ </Modal.Footer>
+ </Modal>
);
}
}
+
+DeleteChannelModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
+};
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index f3bead1c2..e0489856f 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -3,7 +3,8 @@
var Client = require('../utils/client.jsx');
var PostStore = require('../stores/post_store.jsx');
-var BrowserStore = require('../stores/browser_store.jsx');
+var ModalStore = require('../stores/modal_store.jsx');
+var Modal = ReactBootstrap.Modal;
var Utils = require('../utils/utils.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
@@ -15,18 +16,40 @@ export default class DeletePostModal extends React.Component {
super(props);
this.handleDelete = this.handleDelete.bind(this);
+ this.handleToggle = this.handleToggle.bind(this);
+ this.handleHide = this.handleHide.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
- this.onShow = this.onShow.bind(this);
- this.state = {title: '', postId: '', channelId: '', selectedList: PostStore.getSelectedPost(), comments: 0};
+ this.selectedList = null;
+
+ this.state = {
+ show: true,
+ post: null,
+ commentCount: 0,
+ error: ''
+ };
+ }
+
+ componentDidMount() {
+ ModalStore.addModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle);
+ PostStore.addSelectedPostChangeListener(this.onListenerChange);
+ }
+
+ componentWillUnmount() {
+ PostStore.removeSelectedPostChangeListener(this.onListenerChange);
+ ModalStore.removeModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle);
}
+
handleDelete() {
- Client.deletePost(this.state.channelId, this.state.postId,
- function deleteSuccess() {
- var selectedList = this.state.selectedList;
+ Client.deletePost(
+ this.state.post.channel_id,
+ this.state.post.id,
+ () => {
+ var selectedList = this.selectedList;
+
if (selectedList && selectedList.order && selectedList.order.length > 0) {
var selectedPost = selectedList.posts[selectedList.order[0]];
- if ((selectedPost.id === this.state.postId && this.state.title === 'Post') || selectedPost.root_id === this.state.postId) {
+ if ((selectedPost.id === this.state.post.id && !this.state.root_id) || selectedPost.root_id === this.state.post.id) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_SEARCH,
results: null
@@ -36,7 +59,7 @@ export default class DeletePostModal extends React.Component {
type: ActionTypes.RECIEVED_POST_SELECTED,
results: null
});
- } else if (selectedPost.id === this.state.postId && this.state.title === 'Comment') {
+ } else if (selectedPost.id === this.state.post.id && this.state.root_id) {
if (selectedPost.root_id && selectedPost.root_id.length > 0 && selectedList.posts[selectedPost.root_id]) {
selectedList.order = [selectedPost.root_id];
delete selectedList.posts[selectedPost.id];
@@ -53,98 +76,96 @@ export default class DeletePostModal extends React.Component {
}
}
}
- PostStore.removePost(this.state.postId, this.state.channelId);
- AsyncClient.getPosts(this.state.channelId);
- }.bind(this),
- function deleteFailed(err) {
+
+ PostStore.removePost(this.state.post.id, this.state.post.channel_id);
+ AsyncClient.getPosts(this.state.post.channel_id);
+ },
+ (err) => {
AsyncClient.dispatchError(err, 'deletePost');
}
);
+
+ this.handleHide();
}
- onShow(e) {
- var newState = {};
- if (BrowserStore.getItem('edit_state_transfer')) {
- newState = BrowserStore.getItem('edit_state_transfer');
- BrowserStore.removeItem('edit_state_transfer');
- } else {
- var button = e.relatedTarget;
- newState = {title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), postId: $(button).attr('data-postid'), comments: $(button).attr('data-comments')};
- }
- this.setState(newState);
- }
- componentDidMount() {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- PostStore.addSelectedPostChangeListener(this.onListenerChange);
+
+ handleToggle(value, args) {
+ this.setState({
+ show: value,
+ post: args.post,
+ commentCount: args.commentCount,
+ error: ''
+ });
}
- componentWillUnmount() {
- PostStore.removeSelectedPostChangeListener(this.onListenerChange);
+
+ handleHide() {
+ this.setState({show: false});
}
+
onListenerChange() {
var newList = PostStore.getSelectedPost();
- if (!Utils.areObjectsEqual(this.state.selectedList, newList)) {
- this.setState({selectedList: newList});
+ if (!Utils.areObjectsEqual(this.selectedList, newList)) {
+ this.selectedList = newList;
}
}
+
render() {
+ if (!this.state.post) {
+ return null;
+ }
+
var error = null;
if (this.state.error) {
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
}
var commentWarning = '';
- if (this.state.comments > 0) {
- commentWarning = 'This post has ' + this.state.comments + ' comment(s) on it.';
+ if (this.state.commentCount > 0) {
+ commentWarning = 'This post has ' + this.state.commentCount + ' comment(s) on it.';
}
+ const postTerm = Utils.getPostTerm(this.state.post);
+
return (
- <div
- className='modal fade'
- id='delete_post'
- ref='modal'
- role='dialog'
- tabIndex='-1'
- aria-hidden='true'
+ <Modal
+ show={this.state.show}
+ onHide={this.handleHide}
>
- <div className='modal-dialog modal-push-down'>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4 className='modal-title'>Confirm {this.state.title} Delete</h4>
- </div>
- <div className='modal-body'>
- Are you sure you want to delete the {this.state.title.toLowerCase()}?
- <br/>
- <br/>
+ <Modal.Header closeButton={true}>
+ {`Confirm ${postTerm} Delete`}
+ </Modal.Header>
+ <Modal.Body>
+ {`Are you sure you want to delete this ${postTerm.toLowerCase()}?`}
+ <br />
+ <br />
{commentWarning}
- </div>
- {error}
- <div className='modal-footer'>
+ {error}
+ </Modal.Body>
+ <Modal.Footer>
<button
type='button'
className='btn btn-default'
- data-dismiss='modal'
+ onClick={this.handleHide}
>
- Cancel
+ {'Cancel'}
</button>
<button
type='button'
className='btn btn-danger'
- data-dismiss='modal'
onClick={this.handleDelete}
>
- Delete
+ {'Delete'}
</button>
- </div>
- </div>
- </div>
- </div>
+ </Modal.Footer>
+ </Modal>
);
}
+
+ static show(post, commentCount) {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_DELETE_POST_MODAL,
+ value: true,
+ post,
+ commentCount: commentCount || 0
+ });
+ }
}
diff --git a/web/react/components/docs.jsx b/web/react/components/docs.jsx
new file mode 100644
index 000000000..68baa6dad
--- /dev/null
+++ b/web/react/components/docs.jsx
@@ -0,0 +1,41 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const TextFormatting = require('../utils/text_formatting.jsx');
+const UserStore = require('../stores/user_store.jsx');
+
+export default class Docs extends React.Component {
+ constructor(props) {
+ super(props);
+ UserStore.setCurrentUser(global.window.mm_user || {});
+
+ this.state = {text: ''};
+ const errorState = {text: '## 404'};
+
+ if (props.site) {
+ $.get('/static/help/' + props.site + '.md').then((response) => {
+ this.setState({text: response});
+ }, () => {
+ this.setState(errorState);
+ });
+ } else {
+ this.setState(errorState);
+ }
+ }
+
+ render() {
+ return (
+ <div
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.text)}}
+ >
+ </div>
+ );
+ }
+}
+
+Docs.defaultProps = {
+ site: ''
+};
+Docs.propTypes = {
+ site: React.PropTypes.string
+};
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index ef32baa7d..c75da75c9 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -3,6 +3,7 @@
var Client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
+var DeletePostModal = require('./delete_post_modal.jsx');
var Textbox = require('./textbox.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
var PostStore = require('../stores/post_store.jsx');
@@ -34,7 +35,7 @@ export default class EditPostModal extends React.Component {
delete tempState.editText;
BrowserStore.setItem('edit_state_transfer', tempState);
$('#edit_post').modal('hide');
- $('#delete_post').modal('show');
+ DeletePostModal.show(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
return;
}
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index 423ba9067..7f8820d9f 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -201,14 +201,12 @@ export default class Login extends React.Component {
if (global.window.mm_config.EnableTeamCreation === 'true') {
teamSignUp = (
<div className='margin--extra'>
- <span>{'Want to create your own team? '}
- <a
- href='/'
- className='signup-team-login'
- >
- {'Create one now'}
- </a>
- </span>
+ <a
+ href='/'
+ className='signup-team-login'
+ >
+ {'Create a new team'}
+ </a>
</div>
);
}
@@ -227,7 +225,7 @@ export default class Login extends React.Component {
{emailSignup}
{userSignUp}
<div className='form-group margin--extra form-group--small'>
- <span><a href='/find_team'>{'Find other teams'}</a></span>
+ <span><a href='/find_team'>{'Find your other teams'}</a></span>
</div>
{forgotPassword}
{teamSignUp}
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index af29f219e..1fcfabccd 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -5,7 +5,11 @@ const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
const MessageWrapper = require('./message_wrapper.jsx');
const NotifyCounts = require('./notify_counts.jsx');
const ChannelMembersModal = require('./channel_members_modal.jsx');
+const ChannelInfoModal = require('./channel_info_modal.jsx');
const ChannelInviteModal = require('./channel_invite_modal.jsx');
+const ChannelNotificationsModal = require('./channel_notifications_modal.jsx');
+const DeleteChannelModal = require('./delete_channel_modal.jsx');
+const ToggleModalButton = require('./toggle_modal_button.jsx');
const UserStore = require('../stores/user_store.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
@@ -104,15 +108,13 @@ export default class Navbar extends React.Component {
if (channel) {
var viewInfoOption = (
<li role='presentation'>
- <a
+ <ToggleModalButton
role='menuitem'
- data-toggle='modal'
- data-target='#channel_info'
- data-channelid={channel.id}
- href='#'
+ dialogType={ChannelInfoModal}
+ dialogProps={{channel}}
>
{'View Info'}
- </a>
+ </ToggleModalButton>
</li>
);
@@ -178,18 +180,32 @@ export default class Navbar extends React.Component {
var manageMembersOption;
var renameChannelOption;
var deleteChannelOption;
- if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) {
- manageMembersOption = (
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- onClick={() => this.setState({showMembersModal: true})}
- >
- {'Manage Members'}
- </a>
- </li>
- );
+ if (!isDirect && isAdmin) {
+ if (!ChannelStore.isDefault(channel)) {
+ manageMembersOption = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={() => this.setState({showMembersModal: true})}
+ >
+ {'Manage Members'}
+ </a>
+ </li>
+ );
+
+ deleteChannelOption = (
+ <li role='presentation'>
+ <ToggleModalButton
+ role='menuitem'
+ dialogType={DeleteChannelModal}
+ dialogProps={{channel}}
+ >
+ {'Delete Channel...'}
+ </ToggleModalButton>
+ </li>
+ );
+ }
renameChannelOption = (
<li role='presentation'>
@@ -206,37 +222,19 @@ export default class Navbar extends React.Component {
</a>
</li>
);
-
- deleteChannelOption = (
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#delete_channel'
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- {'Delete Channel...'}
- </a>
- </li>
- );
}
var notificationPreferenceOption;
if (!isDirect) {
notificationPreferenceOption = (
<li role='presentation'>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#channel_notifications'
- data-title={channel.display_name}
- data-channelid={channel.id}
+ dialogType={ChannelNotificationsModal}
+ dialogProps={{channel}}
>
- {'Notification Preferences'}
- </a>
+ {'Notification Preferences'}
+ </ToggleModalButton>
</li>
);
}
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index a01d842e5..fffa5b19a 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+var DeletePostModal = require('./delete_post_modal.jsx');
var UserStore = require('../stores/user_store.jsx');
var utils = require('../utils/utils.jsx');
var TimeSince = require('./time_since.jsx');
@@ -50,7 +51,7 @@ export default class PostInfo extends React.Component {
data-channelid={post.channel_id}
data-comments={dataComments}
>
- Edit
+ {'Edit'}
</a>
</li>
);
@@ -65,14 +66,9 @@ export default class PostInfo extends React.Component {
<a
href='#'
role='menuitem'
- data-toggle='modal'
- data-target='#delete_post'
- data-title={type}
- data-postid={post.id}
- data-channelid={post.channel_id}
- data-comments={dataComments}
+ onClick={() => DeletePostModal.show(post, dataComments)}
>
- Delete
+ {'Delete'}
</a>
</li>
);
@@ -89,7 +85,7 @@ export default class PostInfo extends React.Component {
href='#'
onClick={this.props.handleCommentClick}
>
- Reply
+ {'Reply'}
</a>
</li>
);
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index 087ca1df2..ec8223203 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -83,9 +83,14 @@ export default class PostsView extends React.Component {
let hideProfilePic = false;
if (prevPost) {
- sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5;
+ const postIsComment = Utils.isComment(post);
+ const prevPostIsComment = Utils.isComment(prevPost);
+ const postFromWebhook = Boolean(post.props && post.props.from_webhook);
+ const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook);
- sameRoot = Utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
+ sameUser = prevPost.user_id === post.user_id && postFromWebhook === prevPostFromWebhook &&
+ post.create_at - prevPost.create_at <= 1000 * 60 * 5;
+ sameRoot = (postIsComment && (prevPost.id === post.root_id || prevPost.root_id === post.root_id)) || (!postIsComment && !prevPostIsComment && sameUser);
// hide the profile pic if:
// the previous post was made by the same user as the current post,
@@ -94,10 +99,10 @@ export default class PostsView extends React.Component {
// the current post is not from a webhook
// and the previous post is not from a webhook
if ((prevPost.user_id === post.user_id) &&
- !Utils.isComment(prevPost) &&
- !Utils.isComment(post) &&
- (!post.props || !post.props.from_webhook) &&
- (!prevPost.props || !prevPost.props.from_webhook)) {
+ !prevPostIsComment &&
+ !postIsComment &&
+ !postFromWebhook &&
+ !prevPostFromWebhook) {
hideProfilePic = true;
}
}
diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx
index 9fb3af035..f47009cce 100644
--- a/web/react/components/rename_channel_modal.jsx
+++ b/web/react/components/rename_channel_modal.jsx
@@ -5,6 +5,7 @@ const Utils = require('../utils/utils.jsx');
const Client = require('../utils/client.jsx');
const AsyncClient = require('../utils/async_client.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
+const Constants = require('../utils/constants.jsx');
export default class RenameChannelModal extends React.Component {
constructor(props) {
@@ -36,10 +37,10 @@ export default class RenameChannelModal extends React.Component {
return;
}
- let channel = ChannelStore.get(this.state.channelId);
+ const channel = ChannelStore.get(this.state.channelId);
const oldName = channel.name;
const oldDisplayName = channel.displayName;
- let state = {serverError: ''};
+ const state = {serverError: ''};
channel.display_name = this.state.displayName.trim();
if (!channel.display_name) {
@@ -60,7 +61,7 @@ export default class RenameChannelModal extends React.Component {
state.nameError = 'This field must be less than 22 characters';
state.invalid = true;
} else {
- let cleanedName = Utils.cleanUpUrlable(channel.name);
+ const cleanedName = Utils.cleanUpUrlable(channel.name);
if (cleanedName === channel.name) {
state.nameError = '';
} else {
@@ -76,7 +77,7 @@ export default class RenameChannelModal extends React.Component {
}
Client.updateChannel(channel,
- function handleUpdateSuccess() {
+ () => {
$(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
AsyncClient.getChannel(channel.id);
@@ -84,12 +85,12 @@ export default class RenameChannelModal extends React.Component {
ReactDOM.findDOMNode(this.refs.displayName).value = '';
ReactDOM.findDOMNode(this.refs.channelName).value = '';
- }.bind(this),
- function handleUpdateError(err) {
+ },
+ (err) => {
state.serverError = err.message;
state.invalid = true;
this.setState(state);
- }.bind(this)
+ }
);
}
onNameChange() {
@@ -99,10 +100,12 @@ export default class RenameChannelModal extends React.Component {
this.setState({displayName: ReactDOM.findDOMNode(this.refs.displayName).value});
}
displayNameKeyUp() {
- const displayName = ReactDOM.findDOMNode(this.refs.displayName).value.trim();
- const channelName = Utils.cleanUpUrlable(displayName);
- ReactDOM.findDOMNode(this.refs.channelName).value = channelName;
- this.setState({channelName: channelName});
+ if (this.state.channelName !== Constants.DEFAULT_CHANNEL) {
+ const displayName = ReactDOM.findDOMNode(this.refs.displayName).value.trim();
+ const channelName = Utils.cleanUpUrlable(displayName);
+ ReactDOM.findDOMNode(this.refs.channelName).value = channelName;
+ this.setState({channelName: channelName});
+ }
}
handleClose() {
this.setState({
@@ -150,6 +153,15 @@ export default class RenameChannelModal extends React.Component {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
+ let handleInputLabel = 'Handle';
+ let handleInputClass = 'form-control';
+ let readOnlyHandleInput = false;
+ if (this.state.channelName === Constants.DEFAULT_CHANNEL) {
+ handleInputLabel += ' - Cannot be changed for the default channel';
+ handleInputClass += ' disabled-input';
+ readOnlyHandleInput = true;
+ }
+
return (
<div
className='modal fade'
@@ -167,15 +179,15 @@ export default class RenameChannelModal extends React.Component {
className='close'
data-dismiss='modal'
>
- <span aria-hidden='true'>&times;</span>
- <span className='sr-only'>Close</span>
+ <span aria-hidden='true'>{'×'}</span>
+ <span className='sr-only'>{'Close'}</span>
</button>
- <h4 className='modal-title'>Rename Channel</h4>
+ <h4 className='modal-title'>{'Rename Channel'}</h4>
</div>
<form role='form'>
<div className='modal-body'>
<div className={displayNameClass}>
- <label className='control-label'>Display Name</label>
+ <label className='control-label'>{'Display Name'}</label>
<input
onKeyUp={this.displayNameKeyUp}
onChange={this.onDisplayNameChange}
@@ -190,15 +202,16 @@ export default class RenameChannelModal extends React.Component {
{displayNameError}
</div>
<div className={nameClass}>
- <label className='control-label'>Handle</label>
+ <label className='control-label'>{handleInputLabel}</label>
<input
onChange={this.onNameChange}
type='text'
- className='form-control'
+ className={handleInputClass}
ref='channelName'
placeholder='lowercase alphanumeric&#39;s only'
value={this.state.channelName}
maxLength='64'
+ readOnly={readOnlyHandleInput}
/>
{nameError}
</div>
@@ -210,14 +223,14 @@ export default class RenameChannelModal extends React.Component {
className='btn btn-default'
data-dismiss='modal'
>
- Cancel
+ {'Cancel'}
</button>
<button
onClick={this.handleSubmit}
type='submit'
className='btn btn-primary'
>
- Save
+ {'Save'}
</button>
</div>
</form>
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index 58cc1cac7..c16f9ff0e 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -8,6 +8,7 @@ var UserStore = require('../stores/user_store.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
+var DeletePostModal = require('./delete_post_modal.jsx');
var FileAttachmentList = require('./file_attachment_list.jsx');
var Client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
@@ -114,12 +115,7 @@ export default class RhsComment extends React.Component {
<a
href='#'
role='menuitem'
- data-toggle='modal'
- data-target='#delete_post'
- data-title='Comment'
- data-postid={post.id}
- data-channelid={post.channel_id}
- data-comments={0}
+ onClick={() => DeletePostModal.show(post, 0)}
>
{'Delete'}
</a>
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 69de5d523..84fdc014a 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -6,6 +6,7 @@ var UserProfile = require('./user_profile.jsx');
var UserStore = require('../stores/user_store.jsx');
var TextFormatting = require('../utils/text_formatting.jsx');
var utils = require('../utils/utils.jsx');
+var DeletePostModal = require('./delete_post_modal.jsx');
var FileAttachmentList = require('./file_attachment_list.jsx');
var twemoji = require('twemoji');
var Constants = require('../utils/constants.jsx');
@@ -86,21 +87,16 @@ export default class RhsRootPost extends React.Component {
data-postid={post.id}
data-channelid={post.channel_id}
>
- Edit
+ {'Edit'}
</a>
</li>
<li role='presentation'>
<a
href='#'
role='menuitem'
- data-toggle='modal'
- data-target='#delete_post'
- data-title={type}
- data-postid={post.id}
- data-channelid={post.channel_id}
- data-comments={this.props.commentCount}
+ onClick={() => DeletePostModal.show(post, this.props.commentCount)}
>
- Delete
+ {'Delete'}
</a>
</li>
</ul>
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index 7c11de7cf..cc062c538 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -117,8 +117,6 @@ export default class RhsThread extends React.Component {
}
}
resize() {
- var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100;
- $('.post-right__scroll').css('height', height + 'px');
$('.post-right__scroll').scrollTop(100000);
if (this.state.windowWidth > 768) {
$('.post-right__scroll').perfectScrollbar();
diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx
index 2f0068908..f4d8647db 100644
--- a/web/react/components/search_results.jsx
+++ b/web/react/components/search_results.jsx
@@ -62,8 +62,6 @@ export default class SearchResults extends React.Component {
}
resize() {
- var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100;
- $('#search-items-container').css('height', height + 'px');
$('#search-items-container').scrollTop(0);
if (this.state.windowWidth > 768) {
$('#search-items-container').perfectScrollbar();
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index 2135e3ef3..6a428e884 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -48,7 +48,7 @@ export default class SidebarRightMenu extends React.Component {
href='#'
onClick={InviteMemberModal.show}
>
- <i className='glyphicon glyphicon-user'></i>Invite New Member
+ <i className='fa fa-user'></i>Invite New Member
</a>
</li>
);
@@ -61,7 +61,7 @@ export default class SidebarRightMenu extends React.Component {
data-target='#get_link'
data-title='Team Invite'
data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id}
- ><i className='glyphicon glyphicon-link'></i>Get Team Invite Link</a>
+ ><i className='fa fa-link'></i>Get Team Invite Link</a>
</li>
);
}
@@ -74,7 +74,7 @@ export default class SidebarRightMenu extends React.Component {
href='#'
data-toggle='modal'
data-target='#team_settings'
- ><i className='glyphicon glyphicon-globe'></i>Team Settings</a>
+ ><i className='fa fa-globe'></i>Team Settings</a>
</li>
);
manageLink = (
@@ -84,7 +84,7 @@ export default class SidebarRightMenu extends React.Component {
data-toggle='modal'
data-target='#team_members'
>
- <i className='glyphicon glyphicon-wrench'></i>Manage Members</a>
+ <i className='fa fa-users'></i>Manage Members</a>
</li>
);
}
@@ -95,7 +95,7 @@ export default class SidebarRightMenu extends React.Component {
<a
href={'/admin_console?' + utils.getSessionIndex()}
>
- <i className='glyphicon glyphicon-wrench'></i>System Console</a>
+ <i className='fa fa-wrench'></i>System Console</a>
</li>
);
}
@@ -125,7 +125,7 @@ export default class SidebarRightMenu extends React.Component {
href='#'
onClick={() => this.setState({showUserSettingsModal: true})}
>
- <i className='glyphicon glyphicon-cog'></i>Account Settings
+ <i className='fa fa-cog'></i>Account Settings
</a>
</li>
{teamSettingsLink}
@@ -137,18 +137,18 @@ export default class SidebarRightMenu extends React.Component {
<a
href='#'
onClick={this.handleLogoutClick}
- ><i className='glyphicon glyphicon-log-out'></i>Logout</a></li>
+ ><i className='fa fa-sign-out'></i>Logout</a></li>
<li className='divider'></li>
<li>
<a
target='_blank'
href='/static/help/configure_links.html'
- ><i className='glyphicon glyphicon-question-sign'></i>Help</a></li>
+ ><i className='fa fa-question'></i>Help</a></li>
<li>
<a
target='_blank'
href='/static/help/configure_links.html'
- ><i className='glyphicon glyphicon-earphone'></i>Report a Problem</a></li>
+ ><i className='fa fa-phone'></i>Report a Problem</a></li>
</ul>
</div>
<UserSettingsModal
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index e6530b941..1a5269baa 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -295,6 +295,13 @@ export default class Textbox extends React.Component {
this.resize();
}
+ showHelp(e) {
+ e.preventDefault();
+ e.target.blur();
+
+ global.window.open('/docs/Messaging');
+ }
+
render() {
const previewLinkVisible = this.props.messageText.length > 0;
@@ -336,11 +343,17 @@ export default class Textbox extends React.Component {
>
</div>
<a
+ onClick={this.showHelp}
+ className='textbox-help-link'
+ >
+ {'Help'}
+ </a>
+ <a
style={{visibility: previewLinkVisible ? 'visible' : 'hidden'}}
onClick={this.showPreview}
className='textbox-preview-link'
>
- {this.state.preview ? 'Edit message' : 'Preview'}
+ {this.state.preview ? 'Edit' : 'Preview'}
</a>
</div>
);
diff --git a/web/react/components/toggle_modal_button.jsx b/web/react/components/toggle_modal_button.jsx
new file mode 100644
index 000000000..51c8d1f20
--- /dev/null
+++ b/web/react/components/toggle_modal_button.jsx
@@ -0,0 +1,62 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+export default class ModalToggleButton extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.show = this.show.bind(this);
+ this.hide = this.hide.bind(this);
+
+ this.state = {
+ show: false
+ };
+ }
+
+ show() {
+ this.setState({show: true});
+ }
+
+ hide() {
+ this.setState({show: false});
+ }
+
+ render() {
+ const {children, dialogType, dialogProps, ...props} = this.props;
+
+ // this assumes that all modals will have a show property and an onHide event
+ const dialog = React.createElement(this.props.dialogType, Object.assign({}, dialogProps, {
+ show: this.state.show,
+ onHide: () => {
+ this.hide();
+
+ if (dialogProps.onHide) {
+ dialogProps.onHide();
+ }
+ }
+ }));
+
+ // nesting the dialog in the anchor tag looks like it shouldn't work, but it does due to how react-bootstrap
+ // renders modals at the top level of the DOM instead of where you specify in the virtual DOM
+ return (
+ <a
+ {...props}
+ href='#'
+ onClick={this.show}
+ >
+ {children}
+ {dialog}
+ </a>
+ );
+ }
+}
+
+ModalToggleButton.propTypes = {
+ children: React.PropTypes.node.isRequired,
+ dialogType: React.PropTypes.func.isRequired,
+ dialogProps: React.PropTypes.object
+};
+
+ModalToggleButton.defaultProps = {
+ dialogProps: {}
+};
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index 4dcf32cb9..776201295 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -10,6 +10,7 @@ export default class UserSettingsModal extends React.Component {
constructor(props) {
super(props);
+ this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
this.handleHidden = this.handleHidden.bind(this);
this.handleCollapse = this.handleCollapse.bind(this);
@@ -33,12 +34,22 @@ export default class UserSettingsModal extends React.Component {
this.requireConfirm = false;
}
+ componentDidMount() {
+ if (this.props.show) {
+ this.handleShow();
+ }
+ }
+
componentDidUpdate(prevProps) {
- if (!prevProps.show && this.props.show) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300);
- if ($(window).width() > 768) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
- }
+ if (this.props.show && !prevProps.show) {
+ this.handleShow();
+ }
+ }
+
+ handleShow() {
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300);
+ if ($(window).width() > 768) {
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
}
diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx
index 61d13ed8b..16ace0abc 100644
--- a/web/react/components/user_settings/user_settings_security.jsx
+++ b/web/react/components/user_settings/user_settings_security.jsx
@@ -5,6 +5,7 @@ var SettingItemMin = require('../setting_item_min.jsx');
var SettingItemMax = require('../setting_item_max.jsx');
var AccessHistoryModal = require('../access_history_modal.jsx');
var ActivityLogModal = require('../activity_log_modal.jsx');
+var ToggleModalButton = require('../toggle_modal_button.jsx');
var Client = require('../../utils/client.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var Constants = require('../../utils/constants.jsx');
@@ -13,34 +14,13 @@ export default class SecurityTab extends React.Component {
constructor(props) {
super(props);
- this.showAccessHistoryModal = this.showAccessHistoryModal.bind(this);
- this.showActivityLogModal = this.showActivityLogModal.bind(this);
- this.hideModals = this.hideModals.bind(this);
this.submitPassword = this.submitPassword.bind(this);
this.updateCurrentPassword = this.updateCurrentPassword.bind(this);
this.updateNewPassword = this.updateNewPassword.bind(this);
this.updateConfirmPassword = this.updateConfirmPassword.bind(this);
this.setupInitialState = this.setupInitialState.bind(this);
- const state = this.setupInitialState();
- state.showAccessHistoryModal = false;
- state.showActivityLogModal = false;
- this.state = state;
- }
- showAccessHistoryModal() {
- this.props.setEnforceFocus(false);
- this.setState({showAccessHistoryModal: true});
- }
- showActivityLogModal() {
- this.props.setEnforceFocus(false);
- this.setState({showActivityLogModal: true});
- }
- hideModals() {
- this.props.setEnforceFocus(true);
- this.setState({
- showAccessHistoryModal: false,
- showActivityLogModal: false
- });
+ this.state = this.setupInitialState();
}
submitPassword(e) {
e.preventDefault();
@@ -258,30 +238,20 @@ export default class SecurityTab extends React.Component {
{passwordSection}
<div className='divider-dark'/>
<br></br>
- <a
+ <ToggleModalButton
className='security-links theme'
- href='#'
- onClick={this.showAccessHistoryModal}
+ dialogType={AccessHistoryModal}
>
<i className='fa fa-clock-o'></i>View Access History
- </a>
+ </ToggleModalButton>
<b> </b>
- <a
+ <ToggleModalButton
className='security-links theme'
- href='#'
- onClick={this.showActivityLogModal}
+ dialogType={ActivityLogModal}
>
- <i className='fa fa-globe'></i>View and Logout of Active Sessions
- </a>
+ <i className='fa fa-clock-o'></i>{'View and Logout of Active Sessions'}
+ </ToggleModalButton>
</div>
- <AccessHistoryModal
- show={this.state.showAccessHistoryModal}
- onModalDismissed={this.hideModals}
- />
- <ActivityLogModal
- show={this.state.showActivityLogModal}
- onModalDismissed={this.hideModals}
- />
</div>
);
}
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index 8781d52a5..9aba1a564 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -10,16 +10,13 @@ var ErrorStore = require('../stores/error_store.jsx');
var MentionList = require('../components/mention_list.jsx');
var GetLinkModal = require('../components/get_link_modal.jsx');
var EditChannelModal = require('../components/edit_channel_modal.jsx');
-var DeleteChannelModal = require('../components/delete_channel_modal.jsx');
var RenameChannelModal = require('../components/rename_channel_modal.jsx');
var EditPostModal = require('../components/edit_post_modal.jsx');
var DeletePostModal = require('../components/delete_post_modal.jsx');
var MoreChannelsModal = require('../components/more_channels.jsx');
var PostDeletedModal = require('../components/post_deleted_modal.jsx');
-var ChannelNotificationsModal = require('../components/channel_notifications.jsx');
var TeamSettingsModal = require('../components/team_settings_modal.jsx');
var TeamMembersModal = require('../components/team_members.jsx');
-var ChannelInfoModal = require('../components/channel_info_modal.jsx');
var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx');
var RegisterAppModal = require('../components/register_app_modal.jsx');
var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
@@ -103,26 +100,11 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <DeleteChannelModal />,
- document.getElementById('delete_channel_modal')
- );
-
- ReactDOM.render(
<RenameChannelModal />,
document.getElementById('rename_channel_modal')
);
ReactDOM.render(
- <ChannelNotificationsModal />,
- document.getElementById('channel_notifications_modal')
- );
-
- ReactDOM.render(
- <ChannelInfoModal />,
- document.getElementById('channel_info_modal')
- );
-
- ReactDOM.render(
<MoreChannelsModal />,
document.getElementById('more_channels_modal')
);
diff --git a/web/react/pages/docs.jsx b/web/react/pages/docs.jsx
new file mode 100644
index 000000000..ed2b6d0c9
--- /dev/null
+++ b/web/react/pages/docs.jsx
@@ -0,0 +1,16 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Docs = require('../components/docs.jsx');
+
+function setupDocumentationPage(props) {
+ ReactDOM.render(
+ <Docs
+ site={props.Site}
+ />,
+ document.getElementById('docs')
+ );
+}
+
+global.window.mm_user = global.window.mm_user || {};
+global.window.setup_documentation_page = setupDocumentationPage;
diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx
index 8e86ce32f..2e3a26cff 100644
--- a/web/react/stores/browser_store.jsx
+++ b/web/react/stores/browser_store.jsx
@@ -72,7 +72,7 @@ class BrowserStoreClass {
console.log('An error occurred while setting local storage, clearing all props'); //eslint-disable-line no-console
localStorage.clear();
sessionStorage.clear();
- window.location.href = window.location.href;
+ window.location.reload(true);
}
}
diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx
index dc65d48da..809f83a59 100644
--- a/web/react/stores/modal_store.jsx
+++ b/web/react/stores/modal_store.jsx
@@ -27,12 +27,14 @@ class ModalStoreClass extends EventEmitter {
}
handleEventPayload(payload) {
- const action = payload.action;
+ // toggle event handlers should accept a boolean show/hide value and can accept a map of arguments
+ const {type, value, ...args} = payload.action;
- switch (action.type) {
+ switch (type) {
case ActionTypes.TOGGLE_IMPORT_THEME_MODAL:
case ActionTypes.TOGGLE_INVITE_MEMBER_MODAL:
- this.emit(action.type, action.value);
+ case ActionTypes.TOGGLE_DELETE_POST_MODAL:
+ this.emit(type, value, args);
break;
}
}
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 0fe253310..a564a2435 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -40,6 +40,7 @@ class PostStoreClass extends EventEmitter {
this.storePosts = this.storePosts.bind(this);
this.pStorePosts = this.pStorePosts.bind(this);
this.getPosts = this.getPosts.bind(this);
+ this.getPost = this.getPost.bind(this);
this.storePost = this.storePost.bind(this);
this.pStorePost = this.pStorePost.bind(this);
this.removePost = this.removePost.bind(this);
@@ -68,6 +69,7 @@ class PostStoreClass extends EventEmitter {
this.storeLatestUpdate = this.storeLatestUpdate.bind(this);
this.getLatestUpdate = this.getLatestUpdate.bind(this);
this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this);
+ this.getCommentCount = this.getCommentCount.bind(this);
}
emitChange() {
this.emit(CHANGE_EVENT);
@@ -193,6 +195,9 @@ class PostStoreClass extends EventEmitter {
getPosts(channelId) {
return BrowserStore.getItem('posts_' + channelId);
}
+ getPost(channelId, postId) {
+ return this.getPosts(channelId).posts[postId];
+ }
getCurrentUsersLatestPost(channelId, rootId) {
const userId = UserStore.getCurrentId();
var postList = makePostListNonNull(this.getPosts(channelId));
@@ -402,6 +407,20 @@ class PostStoreClass extends EventEmitter {
getLatestUpdate(channelId) {
return BrowserStore.getItem('latest_post_' + channelId, 0);
}
+ getCommentCount(post) {
+ const posts = this.getPosts(post.channel_id).posts;
+
+ let commentCount = 0;
+ for (let id in posts) {
+ if (posts.hasOwnProperty(id)) {
+ if (posts[id].root_id === post.id) {
+ commentCount += 1;
+ }
+ }
+ }
+
+ return commentCount;
+ }
}
var PostStore = new PostStoreClass();
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 205c7461c..b39648bf0 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -63,7 +63,7 @@ export function getChannels(force, updateLastViewed, checkVersion) {
if (serverVersion !== BrowserStore.getLastServerVersion()) {
BrowserStore.setLastServerVersion(serverVersion);
- window.location.href = window.location.href;
+ window.location.reload(true);
console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
}
}
diff --git a/web/react/utils/channel_intro_mssages.jsx b/web/react/utils/channel_intro_mssages.jsx
index f27e23a82..11b69e28f 100644
--- a/web/react/utils/channel_intro_mssages.jsx
+++ b/web/react/utils/channel_intro_mssages.jsx
@@ -92,6 +92,7 @@ export function createOffTopicIntroMessage(channel, showInviteModal) {
</a>
<a
href='#'
+ className='intro-links'
onClick={showInviteModal}
>
<i className='fa fa-user-plus'></i>{'Invite others to this channel'}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 58ee8e2d2..6be87254f 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -40,7 +40,8 @@ module.exports = {
RECIEVED_ALL_TEAMS: null,
TOGGLE_IMPORT_THEME_MODAL: null,
- TOGGLE_INVITE_MEMBER_MODAL: null
+ TOGGLE_INVITE_MEMBER_MODAL: null,
+ TOGGLE_DELETE_POST_MODAL: null
}),
PayloadSources: keyMirror({
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 6f3924829..5af837822 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -616,7 +616,7 @@ export function applyTheme(theme) {
if (theme.centerChannelColor) {
changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name, .tip-overlay', 'color:' + theme.centerChannelColor, 1);
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
- changeCss('.channel-header__links a', 'fill:' + changeOpacity(theme.centerChannelColor, 0.7), 1);
+ changeCss('.channel-header__links a', 'fill:' + changeOpacity(theme.centerChannelColor, 0.9), 1);
changeCss('.channel-header__links a:hover, .channel-header__links a:active', 'fill:' + theme.centerChannelColor, 2);
changeCss('.mentions--top, .command-box', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3);
changeCss('.mentions--top, .command-box', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2);
@@ -644,7 +644,7 @@ export function applyTheme(theme) {
changeCss('.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2);
changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
changeCss('.input-group-addon, .search-bar__container .search__form, .form-control', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
- changeCss('.form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
+ changeCss('.form-control:focus, .channel-header__links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.channel-intro .channel-intro__content, .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
@@ -656,7 +656,7 @@ export function applyTheme(theme) {
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
- changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post:hover, .channel-header__links a:hover, .channel-header__links a:active, .modal .more-table tbody>tr:hover td, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1);
@@ -1232,3 +1232,12 @@ export function getChannelTerm(channelType) {
return channelTerm;
}
+
+export function getPostTerm(post) {
+ let postTerm = 'Post';
+ if (post.root_id) {
+ postTerm = 'Comment';
+ }
+
+ return postTerm;
+}
diff --git a/web/sass-files/sass/partials/_content.scss b/web/sass-files/sass/partials/_content.scss
index 6228cc45e..44a959a9b 100644
--- a/web/sass-files/sass/partials/_content.scss
+++ b/web/sass-files/sass/partials/_content.scss
@@ -20,7 +20,6 @@
background: #fff;
@include display-flex;
@include flex-direction(column);
- flex-direction: column;
.channel__wrap & {
padding-top: 0;
}
diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss
index 49fb8e847..168634b5e 100644
--- a/web/sass-files/sass/partials/_files.scss
+++ b/web/sass-files/sass/partials/_files.scss
@@ -1,9 +1,9 @@
.preview-container {
position: relative;
- margin-top: 10px;
+ margin-top: 25px;
width: 100%;
- max-height: 110px;
- height: 110px;
+ max-height: 100px;
+ height: 100px;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
@@ -11,7 +11,7 @@
display: inline-block;
width: 120px;
height: 100px;
- margin: 7px 0 0 5px;
+ margin: 0 0 0 5px;
vertical-align: top;
position: relative;
border: 1px solid #DDD;
diff --git a/web/sass-files/sass/partials/_forms.scss b/web/sass-files/sass/partials/_forms.scss
index 2d7b6cd26..685677ad0 100644
--- a/web/sass-files/sass/partials/_forms.scss
+++ b/web/sass-files/sass/partials/_forms.scss
@@ -43,3 +43,7 @@
margin: 10px 0 0;
color: #999;
}
+
+.disabled-input {
+ background-color: #dddddd !important;
+}
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index 67c938b8c..69bc56841 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -1,8 +1,5 @@
#channel-header {
- padding: 3px 0;
- height: 58px;
- flex: 0 0 58px;
- @include flex(0 0 50px);
+ @include flex(0 0 56px);
}
.row {
&.header {
@@ -45,9 +42,9 @@
text-overflow: ellipsis;
margin-top: 2px;
max-height: 45px;
- .markdown-inline-img {
- max-height: 45px
- }
+ .markdown-inline-img {
+ max-height: 45px
+ }
}
&.popover {
white-space: normal;
@@ -97,7 +94,7 @@
.sidebar--left, .sidebar--menu {
.team__header {
position: relative;
- padding: 10px;
+ padding: 9px 10px;
@include legacy-pie-clearfix;
&:before {
@include single-transition(all, 0.05s, linear);
@@ -125,7 +122,7 @@
.navbar-right {
font-size: 0.85em;
position: absolute;
- top: 11px;
+ top: 10px;
right: 22px;
z-index: 5;
.dropdown-toggle {
@@ -217,7 +214,7 @@
width:100%;
border-left: none;
font-size: 14px;
- line-height: 50px;
+ line-height: 56px;
#member_popover {
margin-right: 5px;
width: 45px;
@@ -296,17 +293,19 @@
.channel-header__links {
height: 32px;
- vertical-align: top;
- display: inline-block;
- width: 15px;
- margin: 9px 6px 3px 0;
- a {
+ width: 32px;
+ @include border-radius(50px);
+ border: 1px solid #ccc;
+ margin-right: 10px;
+ > a {
+ @include border-radius(50px);
height: 100%;
display: block;
+ @include single-transition(all, 0.1s, ease-in);
}
svg {
vertical-align: top;
- margin-top: 8px;
+ margin: 7px 0 0 0px;
fill: inherit;
}
}
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 45b7b7f23..fad6f5074 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -47,20 +47,25 @@ body.ios {
.textarea-wrapper {
position:relative;
- .textbox-preview-area {
- position: absolute;
- z-index: 2;
- top: 0;
- left: 0;
- box-shadow: none;
- }
- .textbox-preview-link {
- position: absolute;
- z-index: 3;
- bottom: -23px;
- right: 0;
+ .textbox-preview-area {
+ position: absolute;
+ z-index: 2;
+ top: 0;
+ left: 0;
+ box-shadow: none;
+ }
+ .textbox-preview-link, .textbox-help-link {
+ position: absolute;
+ z-index: 3;
+ bottom: -23px;
font-size: 13px;
- cursor: pointer;
+ cursor: pointer;
+ }
+ .textbox-preview-link {
+ right: 45px;
+ }
+ .textbox-help-link {
+ right: 0;
}
min-height:36px;
}
@@ -332,18 +337,19 @@ body.ios {
}
.post-create-footer {
@include clearfix;
- padding: 0;
+ padding: 3px 0 0 0;
+ font-size: 13px;
.has-error {
.control-label {
+ height: 0;
+ display: block;
font-weight: normal;
margin-bottom: 0;
}
}
.msg-typing {
min-height: 25px;
- line-height: 25px;
display: block;
- font-size: 13px;
@include opacity(0.7);
}
}
@@ -392,6 +398,15 @@ body.ios {
.post-body {
@include border-radius(0 4px 4px 0);
}
+ .post-body {
+ border-left: 4px solid #EEE;
+ width: 570px;
+ margin-left: 30px;
+ padding-left: 10px;
+ .post-link {
+ display: none;
+ }
+ }
}
}
&.same--root {
@@ -407,19 +422,15 @@ body.ios {
.post__content {
padding: 0;
}
- .post-body {
- border-left: 4px solid #EEE;
- width: 570px;
- margin-left: 30px;
- padding-left: 10px;
- .post-link {
- display: none;
+ &.same--user {
+ .post__content {
+ padding-left: 46px;
+ }
+ .post-header-post {
+ visibility: hidden;
}
}
}
- .post-create-footer {
- padding: 0;
- }
p {
margin: 0 0 1em;
line-height: 1.6em;
@@ -545,13 +556,14 @@ body.ios {
}
&.post-info {
.post-profile-time {
- color: #a8adb7;
vertical-align: top;
+ width: 150px;
max-width: 220px;
overflow: hidden;
display: block;
white-space: nowrap;
text-overflow: ellipsis;
+ margin: 0 0 0 50px;
}
}
.post-header-col {
@@ -577,7 +589,7 @@ body.ios {
}
}
.post-profile-time {
- color: #a8adb7;
+ @include opacity(0.5);
}
}
.post-comment {
diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss
index ba41d3b95..54c3bcdf8 100644
--- a/web/sass-files/sass/partials/_post_right.scss
+++ b/web/sass-files/sass/partials/_post_right.scss
@@ -1,4 +1,7 @@
.post-right__container {
+ @include display-flex;
+ @include flex-direction(column);
+ height: 100%;
.post-right-root-message {
padding: 1em 1em 0;
@@ -19,6 +22,15 @@
margin: 1em 0 0 0;
}
}
+ .post-header {
+ .post-profile-time {
+ width: 200px;
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
}
.post-create__container {
@@ -44,7 +56,7 @@
}
.post-create-footer {
width: 100%;
- padding-top: 5px;
+ padding: 5px 0 10px;
}
.post-right-comments-upload-in-progress {
padding: 6px 0;
@@ -103,8 +115,9 @@
.post-right__scroll {
position: relative;
- overflow: scroll;
+ overflow: auto;
-webkit-overflow-scrolling: touch;
+ @include flex(1 1 auto);
}
.post-right-comment-time {
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 37626e303..aad991035 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -9,25 +9,26 @@
}
}
.post {
- &.same--root {
- margin-left: 60px;
- padding-left: 10px;
- border-left: 4px solid #EEE;
- div.post-profile-img__container {
- .post-profile-img {
- display: none;
- }
- }
- .post__content {
- width: 825px;
- }
- .post-body {
- width: 736px;
- border: none;
- margin: 3px 0 0;
- }
- }
&.post--comment {
+ &.same--root {
+ margin-left: 104px;
+ padding-left: 10px;
+ border-left: 4px solid #EEE;
+ div.post-profile-img__container {
+ .post-profile-img {
+ display: none;
+ }
+ }
+ .post__content {
+ width: 825px;
+ margin-left: 0;
+ }
+ .post-body {
+ width: 736px;
+ border: none;
+ margin: 3px 0 0;
+ }
+ }
&.other--root {
.post-comment {
margin-left: 0;
@@ -105,34 +106,61 @@
}
}
.post {
- &.same--root {
- margin-left: 60px;
- padding-left: 10px;
- border-left: 4px solid #EEE;
- div.post-profile-img__container {
- .post-profile-img {
- display: none;
- }
- }
- .post__content {
- width: 825px;
- }
- .post-body {
- width: 736px;
- border: none;
- margin: 3px 0 0;
- }
- }
+ &.same--root.same--user {
+ .post-header-post {
+ visibility: hidden;
+ width: 100%;
+ position: relative;
+ top: -5px;
+ .post-header-col.post-header__name {
+ display: none;
+ }
+ }
+ .post-body {
+ top: -15px;
+ margin-bottom: -10px;
+ }
+ &:hover .post-header-post {
+ visibility: visible;
+ }
+ }
+
&.post--comment {
&.other--root {
.post-comment {
margin-left: 0;
}
}
- &.same--root {
- margin-top: 5px;
- margin-bottom: 5px;
- }
+ &.same--root {
+ margin-top: 5px;
+ margin-bottom: 5px;
+ margin-left: 104px;
+ padding-left: 10px;
+ border-left: 4px solid #EEE;
+ div.post-profile-img__container {
+ .post-profile-img {
+ display: none;
+ }
+ }
+ .post-body {
+ margin-left: 0;
+ border-left: 0;
+ }
+ &.same--user {
+ .post__content {
+ margin-left: 0;
+ padding-left: 0;
+ }
+ }
+ }
+ .post__content {
+ width: 810px;
+ }
+ .post-body {
+ width: 736px;
+ border: none;
+ margin: 3px 0 0;
+ }
}
.post__content {
width: 880px;
@@ -275,6 +303,11 @@
}
}
@media screen and (max-width: 768px) {
+ .textarea-wrapper {
+ .textbox-preview-link {
+ display: none;
+ }
+ }
.form-control {
&.min-height {
min-height: 100px;
@@ -353,9 +386,6 @@
}
}
.post {
- &.same--root {
- margin-left: 25px;
- }
&:hover {
background: none;
.post-header .post-header-col.post-header__reply {
@@ -365,7 +395,11 @@
}
}
&.post--comment {
+ &.same--root {
+ margin-left: 25px;
+ }
&.other--root {
+ margin-left: 0;
&:hover {
background: none;
}
@@ -376,6 +410,9 @@
content: '...';
}
}
+ &.same--root.same--user .post__content{
+ padding-left: 0;
+ }
}
.signup-team__container {
padding: 30px 0;
@@ -503,17 +540,22 @@
.post-create__container {
.post-right__container & {
padding: 0 1em;
+ .msg-typing {
+ display: none;
+ }
}
form {
- padding: 0;
+ padding: 0.5em 0;
}
.post-create-footer {
+ padding: 0 1em;
.msg-typing {
- margin-left: 45px;
- width: 55%;
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
+ display: none;
+ }
+ .has-error {
+ .control-label {
+ height: auto;
+ }
}
}
.post-create-body {
@@ -542,8 +584,8 @@
}
}
.preview-container {
- padding: 0px 10px;
- margin-top: 20px;
+ padding: 0px;
+ margin-top: 5px;
.preview-div {
margin-top: 0;
}
@@ -616,7 +658,7 @@
}
.search-bar__container {
padding: 0;
- height: 45px;
+ @include flex(0 0 44px);
background: $primary-color;
color: #fff;
&.focused {
@@ -745,6 +787,12 @@
.sidebar--right__close {
display: none;
}
+ .sidebar-right__body {
+ height: calc(100% - 44px);
+ }
+ }
+ .search-items-container {
+ height: calc(100% - 44px);
}
.inner__wrap {
&.move--right {
diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss
index 73b69c866..bedf35376 100644
--- a/web/sass-files/sass/partials/_search.scss
+++ b/web/sass-files/sass/partials/_search.scss
@@ -1,8 +1,9 @@
#channel-header .search-bar__container {
- padding: 8px 8px 8px 0;
+ padding: 0 8px 0 0;
}
.search-bar__container {
- padding: 12px 8px 12px 0;
+ padding: 12px 8px 0 0;
+ @include flex(0 0 56px);
}
.search__clear {
display: none;
@@ -71,8 +72,10 @@
.search-items-container {
position: relative;
- overflow: scroll;
+ overflow: auto;
-webkit-overflow-scrolling: touch;
+ @include flex(1 1 auto);
+ height: calc(100% - 56px);
}
.search-results-header {
@@ -92,9 +95,11 @@
padding: 10px 1em;
margin: 0;
cursor: pointer;
+
&:first-child {
border: none;
}
+
.search-channel__name {
font-weight: 600;
margin: 0 0 10px 0;
diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss
index ab13d1b42..eb7c1b83f 100644
--- a/web/sass-files/sass/partials/_sidebar--left.scss
+++ b/web/sass-files/sass/partials/_sidebar--left.scss
@@ -47,7 +47,7 @@
.nav-pills__container {
height: 100%;
position: relative;
- overflow: scroll;
+ overflow: auto;
-webkit-overflow-scrolling: touch;
}
diff --git a/web/sass-files/sass/partials/_sidebar--menu.scss b/web/sass-files/sass/partials/_sidebar--menu.scss
index 6f4a0cc38..e34cd72c1 100644
--- a/web/sass-files/sass/partials/_sidebar--menu.scss
+++ b/web/sass-files/sass/partials/_sidebar--menu.scss
@@ -57,8 +57,11 @@
color: inherit;
line-height: 40px;
padding: 0 15px;
- .glyphicon {
+ .fa ,.glyphicon {
width: 25px;
+ text-align: center;
+ left: -5px;
+ position: relative;
}
}
}
diff --git a/web/sass-files/sass/partials/_sidebar--right.scss b/web/sass-files/sass/partials/_sidebar--right.scss
index a4267294c..2527eef28 100644
--- a/web/sass-files/sass/partials/_sidebar--right.scss
+++ b/web/sass-files/sass/partials/_sidebar--right.scss
@@ -3,24 +3,38 @@
width: 400px;
height: 100%;
right: 0px;
- padding: 0 0 2em 0;
+ padding: 0;
background: #fff;
@include single-transition(transform, 0.5s, ease);
right: -320px;
+
&.move--left {
right: 0;
}
+
+ .sidebar--right__content {
+ height: 100%;
+ @include display-flex;
+ @include flex-direction(column);
+ }
+
.sidebar--right__back {
- color: #666;
- width: 20px;
+ color: inherit;
+ @include opacity(0.8);
+ width: 30px;
text-align: center;
- margin: 0 0 0 -6px;
- font-size: 12px;
+ margin: 0 0 0 -14px;
+ font-size: 13px;
display: inline-block;
}
.sidebar-right__body {
+ @include flex(1 1 auto);
border-left: $border-gray;
border-top: $border-gray;
+ @include display-flex;
+ @include flex-direction(column);
+ height: calc(100% - 56px);
+ @include border-radius(2px 0 0 0);
}
.post {
.post-header {
@@ -75,7 +89,7 @@
height: 44px;
padding: 0 1em;
line-height: 44px;
- background: #F5F5F5;
+ @include flex(0 0 44px);
border-bottom: $border-gray;
}
.sidebar--right__subheader {
diff --git a/web/static/help/Messaging.md b/web/static/help/Messaging.md
new file mode 120000
index 000000000..f74c0b879
--- /dev/null
+++ b/web/static/help/Messaging.md
@@ -0,0 +1 @@
+../../../doc/help/Messaging.md \ No newline at end of file
diff --git a/web/templates/docs.html b/web/templates/docs.html
new file mode 100644
index 000000000..21659e810
--- /dev/null
+++ b/web/templates/docs.html
@@ -0,0 +1,24 @@
+{{define "docs"}}
+<!DOCTYPE html>
+<html>
+{{template "head" . }}
+<body class="white">
+<div class="container-fluid">
+ <div class="inner__wrap">
+ <div class="row content">
+ <div class="col-sm-12">
+ <div id="docs"></div>
+ </div>
+ <div class="footer-push"></div>
+ </div>
+ <div class="row footer">
+ {{template "footer" . }}
+ </div>
+ </div>
+</div>
+<script>
+ window.setup_documentation_page({{ .Props }});
+</script>
+</body>
+</html>
+{{end}}
diff --git a/web/web.go b/web/web.go
index 02ceb69ba..477bd8b27 100644
--- a/web/web.go
+++ b/web/web.go
@@ -80,6 +80,8 @@ func InitWeb() {
mainrouter.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiAppHandler(incomingWebhook)).Methods("POST")
+ mainrouter.Handle("/docs/{doc:[A-Za-z0-9]+}", api.AppHandlerIndependent(docs)).Methods("GET")
+
// ----------------------------------------------------------------------------------------------
// *ANYTHING* team specific should go below this line
// ----------------------------------------------------------------------------------------------
@@ -494,6 +496,15 @@ func findTeam(c *api.Context, w http.ResponseWriter, r *http.Request) {
page.Render(c, w)
}
+func docs(c *api.Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ doc := params["doc"]
+
+ page := NewHtmlTemplatePage("docs", "Documentation")
+ page.Props["Site"] = doc
+ page.Render(c, w)
+}
+
func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) {
isResetLink := true
hash := r.URL.Query().Get("h")