summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md39
-rw-r--r--api/channel.go7
-rw-r--r--api/channel_test.go4
-rw-r--r--doc/README.md21
-rw-r--r--doc/developer/API.md2
-rw-r--r--doc/developer/tests/test-link-preview.md23
-rw-r--r--doc/developer/tests/test-links.md16
-rw-r--r--doc/developer/tests/test-markdown.md14
-rw-r--r--doc/developer/tests/test-mentions.md13
-rw-r--r--doc/help/README.md12
-rw-r--r--doc/help/Sign-in.md19
-rw-r--r--doc/help/Slack-Import.md29
-rw-r--r--doc/help/Team-Settings.md70
-rw-r--r--doc/help/Team-Statistics.md24
-rw-r--r--doc/help/system-console/Team-Statistics.md24
-rw-r--r--doc/process/documentation-guidelines.md21
-rw-r--r--docker/1.2/Dockerfile2
-rw-r--r--model/channel_extra.go5
-rw-r--r--store/sql_channel_store.go20
-rw-r--r--store/sql_channel_store_test.go8
-rw-r--r--store/store.go1
-rw-r--r--web/react/components/access_history_modal.jsx2
-rw-r--r--web/react/components/activity_log_modal.jsx2
-rw-r--r--web/react/components/channel_header.jsx8
-rw-r--r--web/react/components/channel_invite_modal.jsx2
-rw-r--r--web/react/components/channel_members_modal.jsx2
-rw-r--r--web/react/components/channel_notifications.jsx2
-rw-r--r--web/react/components/delete_post_modal.jsx2
-rw-r--r--web/react/components/file_attachment.jsx2
-rw-r--r--web/react/components/invite_member_modal.jsx44
-rw-r--r--web/react/components/more_channels.jsx2
-rw-r--r--web/react/components/navbar.jsx4
-rw-r--r--web/react/components/navbar_dropdown.jsx2
-rw-r--r--web/react/components/notify_counts.jsx2
-rw-r--r--web/react/components/popover_list_members.jsx4
-rw-r--r--web/react/components/post.jsx2
-rw-r--r--web/react/components/post_body.jsx24
-rw-r--r--web/react/components/posts_view.jsx10
-rw-r--r--web/react/components/posts_view_container.jsx14
-rw-r--r--web/react/components/rhs_comment.jsx2
-rw-r--r--web/react/components/rhs_root_post.jsx2
-rw-r--r--web/react/components/rhs_thread.jsx4
-rw-r--r--web/react/components/search_bar.jsx2
-rw-r--r--web/react/components/search_results.jsx2
-rw-r--r--web/react/components/sidebar.jsx11
-rw-r--r--web/react/components/sidebar_right.jsx4
-rw-r--r--web/react/components/team_members.jsx2
-rw-r--r--web/react/components/team_settings.jsx2
-rw-r--r--web/react/components/textbox.jsx7
-rw-r--r--web/react/components/user_profile.jsx4
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx1
-rw-r--r--web/react/components/user_settings/user_settings.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx12
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx20
-rw-r--r--web/react/stores/user_store.jsx8
-rw-r--r--web/react/utils/channel_intro_mssages.jsx19
-rw-r--r--web/react/utils/utils.jsx94
-rw-r--r--web/sass-files/sass/partials/_files.scss1
-rw-r--r--web/sass-files/sass/partials/_navbar.scss6
-rw-r--r--web/sass-files/sass/partials/_post.scss38
-rw-r--r--web/sass-files/sass/partials/_post_right.scss16
-rw-r--r--web/sass-files/sass/partials/_responsive.scss15
-rw-r--r--web/sass-files/sass/partials/_settings.scss4
-rw-r--r--web/web.go4
64 files changed, 572 insertions, 214 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a21f118b9..3af4a6ae6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,32 @@
# Mattermost Changelog
-## UNDER DEVELOPMENT Release v1.2.0
+## Release v1.2.0
-The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the product's `master` branch to note key changes committed to master and are on their way to the next stable release. When a stable release is pushed the "UNDER DEVELOPMENT" heading is removed from the final changelog of the release.
-
-- **Release candidate anticipated:** 2015-11-10
- **Final release anticipated:** 2015-11-16
+### Release Highlights
+
+#### Outgoing webhooks
+
+- 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
+
+#### Search Scope Modifiers
+
+- Adding search term `in:[channel_url_name]` now limits searches within a specific channel
+- Adding search term `from:[username]` now limits searches to messages from a specific user
+
+#### Syntax Highlighting
+
+- Syntax highlight for code blocks now available for `Diff, Apache, Makefile, HTTP, JSON, Markdown, Java, CSS, nginx, ObjectiveC, Python, XML, Perl, Bash, PHP, Coffee, C, SQL, Go, Ruby, Java, and ini`
+
+#### Usability Improvements
+
+- Added tutorial to teach new users how to use Mattermost
+- Various performance improvements to support teams with hundreds of users
+- Direct Messages "More" menu now lets you search for users by username and real name
+
### Improvements
Onboarding
@@ -18,7 +38,7 @@ Messaging and Notifications
- Users can now search for teammates to add to **Direct Message** list via **More** menu
- Users can now personalize Direct Messages list by removing users listed
- Link previews - Adding URL with .gif file adds image below message
-- Added new browser tab alerts to indicate unread messages and mentions
+- Added new browser tab alerts to indicate unread messages and mentions
Search
@@ -34,8 +54,8 @@ Integrations
User Interface
- Member list in Channel display now scrollable, and includes Message button to message channel members directly
-- Added ability to edit previous message by hitting UP arrow
-- Syntax highlighting added for code blocks
+- Added ability to edit previous message by hitting UP arrow
+- Syntax highlighting added for code blocks
- Languages include `Diff, Apache, Makefile, HTTP, JSON, Markdown, Java, CSS, nginx, ObjectiveC, Python, XML, Perl, Bash, PHP, Coffee, C, SQL, Go, Ruby, Java, and ini`.
- Use by adding the name of the language on the first link of the code block, for example: ```python
- Syntax color theme can be defined under **Account Settings** > **Appearance Settings** > **Custom Theme**
@@ -65,7 +85,7 @@ System Console
- 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
@@ -75,7 +95,7 @@ System Console
Multiple settings were added to [`config.json`](./config/config.json). These options can be modified in the System Console, or manually updated in the existing config.json file. This is a list of changes and their new default values in a fresh install:
- Under `TeamSettings` in `config.json`:
- - Added: `"RestrictTeamNames": true` to control whether team names are restricted
+ - 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": true` to turn on outgoing webhooks
@@ -99,6 +119,7 @@ The following is for informational purposes only, no action needed. Mattermost a
#### Known Issues
+- When navigating to a page with new messages as well as message containing inline images added via markdown, the channel may move up and down while loading the inline images
- Microsoft Edge does not yet support drag and drop
- After upgrading to v1.2 existing users will see the newly added tutorial tips upon login (this is a special case for v1.2 and will not happen in future upgrades)
- Channel list becomes reordered when there are lowercase channel names in a Postgres database
diff --git a/api/channel.go b/api/channel.go
index 44be1cf97..75ca9680d 100644
--- a/api/channel.go
+++ b/api/channel.go
@@ -707,6 +707,7 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) {
scm := Srv.Store.Channel().GetMember(id, c.Session.UserId)
ecm := Srv.Store.Channel().GetExtraMembers(id, 20)
+ ccm := Srv.Store.Channel().GetMemberCount(id)
if cmresult := <-scm; cmresult.Err != nil {
c.Err = cmresult.Err
@@ -714,9 +715,13 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) {
} else if ecmresult := <-ecm; ecmresult.Err != nil {
c.Err = ecmresult.Err
return
+ } else if ccmresult := <-ccm; ccmresult.Err != nil {
+ c.Err = ccmresult.Err
+ return
} else {
member := cmresult.Data.(model.ChannelMember)
extraMembers := ecmresult.Data.([]model.ExtraMember)
+ memberCount := ccmresult.Data.(int64)
if !c.HasPermissionsToTeam(channel.TeamId, "getChannelExtraInfo") {
return
@@ -732,7 +737,7 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- data := model.ChannelExtra{Id: channel.Id, Members: extraMembers}
+ data := model.ChannelExtra{Id: channel.Id, Members: extraMembers, MemberCount: memberCount}
w.Header().Set(model.HEADER_ETAG_SERVER, extraEtag)
w.Header().Set("Expires", "-1")
w.Write([]byte(data.ToJson()))
diff --git a/api/channel_test.go b/api/channel_test.go
index a41f63b1b..faed387dd 100644
--- a/api/channel_test.go
+++ b/api/channel_test.go
@@ -677,6 +677,10 @@ func TestGetChannelExtraInfo(t *testing.T) {
data := rget.Data.(*model.ChannelExtra)
if data.Id != channel1.Id {
t.Fatal("couldnt't get extra info")
+ } else if len(data.Members) != 1 {
+ t.Fatal("got incorrect members")
+ } else if data.MemberCount != 1 {
+ t.Fatal("got incorrect member count")
}
//
diff --git a/doc/README.md b/doc/README.md
index 7ed20fba6..d062dee65 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -53,11 +53,20 @@ Procedures for upgrading the Mattermost server
_Note: End user help documentation is a new feature being completed for the v1.2 release. The materials below are work in progress._
-- User Interface
- - [Manage Members](help/Manage-Members.md)
- - Team Settings
- - [Slack Import](help/Slack-Import.md)
+- Getting Started
+ - [Sign-in](help/Sign-in.md)
-- Messaging
- - [Mattermost Markdown Formatting](usage/Markdown.md)
+- User Interface
+ - Main Menu
+ - [Team Settings ](https://github.com/mattermost/platform/blob/help-docs-update/doc/help/Team-Settings.md)
+ - [General Settings](https://github.com/mattermost/platform/blob/help-docs-update/doc/help/Team-Settings.md#general)
+ - [Slack Import](https://github.com/mattermost/platform/blob/help-docs-update/doc/help/Team-Settings.md#import-from-slack-beta)
+ - [Manage Members](help/Manage-Members.md)
+ - Messaging
+ - [Mattermost Markdown Formatting](usage/Markdown.md)
+ - [Search](help/Search.md)
+
+- System Console
+ - Team
+ - [Team Statistics](help/system-console/Team-Statistics.md)
diff --git a/doc/developer/API.md b/doc/developer/API.md
index 4c4b2f04e..e5e5db2ba 100644
--- a/doc/developer/API.md
+++ b/doc/developer/API.md
@@ -12,7 +12,7 @@ Incoming webhooks allow external applications to post messages into Mattermost c
In addition to supporting Slack's incoming webhook formatting, Mattermost webhooks offer full support of industry-standard markdown formatting, including headings, tables and in-line images.
-### [Outgoing Webhooks (in Mattermost v1.2)](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Outgoing-Webhooks.md)
+### [Outgoing Webhooks](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Outgoing-Webhooks.md)
Outgoing webhooks allow external applications to receive webhook events from events happening within Mattermost channels and private groups via JSON payloads via HTTP POST requests sent to incoming webhook URLs defined by your applications.
diff --git a/doc/developer/tests/test-link-preview.md b/doc/developer/tests/test-link-preview.md
new file mode 100644
index 000000000..4061bda35
--- /dev/null
+++ b/doc/developer/tests/test-link-preview.md
@@ -0,0 +1,23 @@
+# Link Preview Tests
+
+Link previews should embed previews of the contents of a hyperlink from a message or comment in the center channel directly below the message or comment.
+
+Post location variation:
+
+1. Post as message in center channel with RHS closed (link preview should render under message)
+2. Post as message in center channel with RHS open (link preview should render under message)
+3. Post as comment in RHS (link preview should not render)
+4. View comment in center channel with RHS closed (link preview should render under message)
+5. View comment in center channel with RHS open (link preview should render under message)
+6. Search for post in RHS with link
+
+Post the following links one per message in the above variations:
+
+Twitter Link Preview:
+https://twitter.com/mattermosthq/status/664928489078820865
+
+Vine:
+https://vine.co/v/eDeVgbFrt9L
+
+Wikipedia
+https://en.wikipedia.org/wiki/Princess_Bubblegum
diff --git a/doc/developer/tests/test-links.md b/doc/developer/tests/test-links.md
new file mode 100644
index 000000000..62b729b30
--- /dev/null
+++ b/doc/developer/tests/test-links.md
@@ -0,0 +1,16 @@
+
+# Link Testing
+
+Links in Mattermosts should render as specified below. Paste the below text into Mattermost to test text processing.
+
+```
+These strings should auto-link:
+
+http://wikipedia.com
+https://wikipedia.com
+www.wikipedia.com
+
+These strings should not auto-link:
+
+Readme.md
+```
diff --git a/doc/developer/tests/test-markdown.md b/doc/developer/tests/test-markdown.md
new file mode 100644
index 000000000..8a24aad86
--- /dev/null
+++ b/doc/developer/tests/test-markdown.md
@@ -0,0 +1,14 @@
+# Markdown tests
+
+Paste the following tests into Mattermost to test markdown support.
+
+```
+# This should render as Heading 1 font size
+## This should render as Heading 2 font size
+### This should render as Heading 3 font size
+#### This should render as Heading 4 font size
+##### This should render as Heading 5 font size
+###### This should render as Heading 6 font size
+~~This should show strikethrough formatting~~
+**This should be bold**
+```
diff --git a/doc/developer/tests/test-mentions.md b/doc/developer/tests/test-mentions.md
new file mode 100644
index 000000000..99a47e337
--- /dev/null
+++ b/doc/developer/tests/test-mentions.md
@@ -0,0 +1,13 @@
+# Mentions Testing
+
+To test the following mention functional:
+
+1. Add a user 'alice' to the system
+2. Paste the below text to test if mentions is properly highlighting
+
+
+```
+To run this test, if a user named @alice doesn't yet exist, create one.
+
+I saw @alice--and I said "Hi @alice!" then "What's up @alice?" and then @alice, was totally @alice; she just "@alice"'d me and walked on by. That's @alice...
+```
diff --git a/doc/help/README.md b/doc/help/README.md
deleted file mode 100644
index 9271d64dd..000000000
--- a/doc/help/README.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Help
-
-The help section of the Mattermost documentation is intended for use by end users learning about the Mattermost concepts, usage, terminology and user interface.
-
-_Note: Help documentation is a work-in-progress. Community contributions highly welcome. Please see [guidelines for contributing](https://forum.mattermost.org/t/help-improve-mattermost-documentation/194)._
-
-## Team Site Main Menu
-
-You can access the **Team Site Main Menu** by clicking on the three vertical dots at the top of the left sidebar in a team site. Here we describe the various options available from the menu:
-
-- [Manage Members](Manage-Members.md)
-
diff --git a/doc/help/Sign-in.md b/doc/help/Sign-in.md
new file mode 100644
index 000000000..728a7d42c
--- /dev/null
+++ b/doc/help/Sign-in.md
@@ -0,0 +1,19 @@
+# Sign-in
+
+You can sign-in to your team from the web address of `https://domain.com/teamname`.
+
+There are several options for signing in depending on how your System Administrator has configured your server.
+
+#### Email address and password sign-in
+
+If available, you can sign in using the combination of email address and password used to create your account.
+
+If you have forgotten your password, you should be able to reset it from the "I forgot my password" option on the sign-in screen, or contact your System Administrator if you need help resetting your password.
+
+#### GitLab Single-Sign-On (SSO) option
+
+If available, you can sign in using your GitLab account using a one-click sign-in option. GitLab SSO lets you create teams, create accounts on teams, and sign-in to teams using one username, email address and password that works across everything on the server.
+
+#### Switching Teams
+
+You can switch among teams you've recently signed into using the main menu in any team site on the server. By default, devices remember which teams you have signed into for 30 days, and this duration is configurable by the System Administrator.
diff --git a/doc/help/Slack-Import.md b/doc/help/Slack-Import.md
deleted file mode 100644
index f834d5177..000000000
--- a/doc/help/Slack-Import.md
+++ /dev/null
@@ -1,29 +0,0 @@
-### Slack Import
-
-*Note: As a proprietary SaaS service, Slack is able to change its export format quickly and without notice. If you encounter issues not mentioned in the documentation below, please alert the product team by [filing an issue](https://github.com/mattermost/platform/issues).*
-
-#### Usage
-
-The Slack Import feature in Mattermost is in "Beta" and focus is on supporting migration of teams of less than 100 registered users. To use:
-
-1. Generate a Slack "Export" file from **Slack > Team Settings > Import/Export Data > Export > Start Export**
-
-2. In Mattermost go to **Team Settings > Import > Import from Slack**. _Team Owner_ or _Team Administrator_ role is required to access this menu option.
-
-3. Click **Select file** to upload Slack export file and click **Import**.
-
-4. Emails and usernames from Slack are used to create new Mattermost accounts
-
-5. Slack users can activate their new Mattermost accounts by using Mattermost's Password Reset screen with their email addresses from Slack to set new passwords for their Mattermost accounts
-
-6. Once logged in, the Mattermost users will have access to previous Slack messages in the public channels imported from Slack.
-
-**It is highly recommended that you test Slack import before applying it to an instance intended for production.** If you use Docker, you can spin up a test instance in one line (`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`). If you don't use Docker, there are [step-by-step instructions](../install/Docker-Single-Container.md) to install Mattermost in preview mode in less than 5 minutes.
-
-#### Notes:
-
-- Newly added markdown suppport in Slack's Posts 2.0 feature announced on September 28, 2015 is not yet supported.
-- Slack does not export files or images your team has stored in Slack's database. Mattermost will provide links to the location of your assets in Slack's web UI.
-- Slack does not export any content from private groups or direct messages that your team has stored in Slack's database.
-- In Beta, Slack accounts with username or email address collisions with existing Mattermost accounts will not import and mentions do not resolve as Mattermost usernames (still shows Slack ID). No pre-check or roll-back is currently offered.
-
diff --git a/doc/help/Team-Settings.md b/doc/help/Team-Settings.md
new file mode 100644
index 000000000..7e6cf5dd5
--- /dev/null
+++ b/doc/help/Team-Settings.md
@@ -0,0 +1,70 @@
+## Team Settings
+
+The Team Settings menu offers Team Administrators, Team Owners and System Administrators to adjust settings applying to a specific team.
+
+The following settings are found in a Team Site from the **Three-Dot** menu at the top of the left sidebar under **Team Settings**.
+
+### General
+
+General settings under the **Team Settings** > **General** configure how a team is displayed to users.
+
+#### Team Name
+
+Your **Team Name** is displayed on the sign-in page, and in the top of the left-hand sidebar for your team.
+
+#### Allow anyone to sign-up from login page
+
+Setting this option to **Yes** a link to the account creation page is included on the sign-in page of this team.
+
+Team Administrators would set this to **Yes** when they:
+ 1. Operate on a closed network and want to make sign-up easy.
+ 2. Operate on the open internet with sign-up restricted to specific domains, and want to allow easy sign-up from users with email addresses. Note: System Administrators can restrict sign-up to specific domains via the System Console.
+ 3. Operate on the open internet and want to allow anyone to sign-up.
+
+Team Administrators would set this to **No** when they:
+ 1. Operate on the open internet and want a small, private team that is email-invite-only
+
+#### Include this team in the Team Directory
+
+Setting this option to **Yes** includes the Team Name on the Home Page and a link to this team's sign-in page.
+
+Team Administrators would set this to **Yes** when they:
+ 1. Operate on a closed network and want to make it easy to discover their team from the Home Page of the Mattermost server.
+ 2. Operate on the open internet with sign-up restricted to specific domains, and want to allow easy sign-up from users with email addresses. Note: System Administrators can restrict sign-up to specific domains via the System Console.
+ 3. Operate on the open internet and want to allow anyone to sign-up to their team from the Home Page of the Mattermost server.
+
+Team Administrators would set this to **No** when they:
+ 1. Operate on the open internet and want a small, private team that is email-invite-only
+
+#### Invite Code
+
+When allowing anyone to sign-up from the login page, the **Invite Code** is used as part of the sign-up process. Clicking **Re-Generate** will invalidate the previous invitations and invitation URLs.
+
+### Import
+
+#### Import from Slack (Beta)
+
+*Note: As a proprietary SaaS service, Slack is able to change its export format quickly and without notice. If you encounter issues not mentioned in the documentation below, please alert the product team by [filing an issue](https://github.com/mattermost/platform/issues).*
+
+The Slack Import feature in Mattermost is in "Beta" and focus is on supporting migration of teams of less than 100 registered users. To use:
+
+1. Generate a Slack "Export" file from **Slack > Team Settings > Import/Export Data > Export > Start Export**
+
+2. In Mattermost go to **Team Settings > Import > Import from Slack**. _Team Owner_ or _Team Administrator_ role is required to access this menu option.
+
+3. Click **Select file** to upload Slack export file and click **Import**.
+
+4. Emails and usernames from Slack are used to create new Mattermost accounts
+
+5. Slack users can activate their new Mattermost accounts by using Mattermost's Password Reset screen with their email addresses from Slack to set new passwords for their Mattermost accounts
+
+6. Once logged in, the Mattermost users will have access to previous Slack messages in the public channels imported from Slack.
+
+**It is highly recommended that you test Slack import before applying it to an instance intended for production.** If you use Docker, you can spin up a test instance in one line (`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`). If you don't use Docker, there are [step-by-step instructions](../install/Docker-Single-Container.md) to install Mattermost in preview mode in less than 5 minutes.
+
+#### Notes:
+
+- Newly added markdown suppport in Slack's Posts 2.0 feature announced on September 28, 2015 is not yet supported.
+- Slack does not export files or images your team has stored in Slack's database. Mattermost will provide links to the location of your assets in Slack's web UI.
+- Slack does not export any content from private groups or direct messages that your team has stored in Slack's database.
+- In Beta, Slack accounts with username or email address collisions with existing Mattermost accounts will not import and mentions do not resolve as Mattermost usernames (still shows Slack ID). No pre-check or roll-back is currently offered.
diff --git a/doc/help/Team-Statistics.md b/doc/help/Team-Statistics.md
deleted file mode 100644
index 05d63794b..000000000
--- a/doc/help/Team-Statistics.md
+++ /dev/null
@@ -1,24 +0,0 @@
-## Team Statistics
-___
-Statistics on users, posts and channels are tracked for each team and viewable in the System Console. System Administrators can access statistics for your Mattermost teams by clicking the **three-dot menu**, then click **System Console**. Under the *Teams* section on the left side you’ll see a list of the teams that belong to your domain. Click **Statistics** under the team of interest to open the stats page. Here is some helpful terminology:
-
-**Total Users**
-The total number of accounts created, regardless of if the accounts are active or inactive.
-
-**Total Posts**
-The total number of posts made by your team, including deleted posts or those made by incoming and outgoing webhook integrations.
-
-**Public Groups**
-The number of public channels created by your team, including channels that may have been archived.
-
-**Private Group**
-The number of private groups created by your team, including groups that may have been archived.
-
-**Active Users With Posts**
-Users who logged in and made a post on a certain day.
-
-**Recently Active Users**
-Users that have logged in and had recent browser activity in Mattermost.
-
-**Newly Created Users**
-Users that have recently completed the signup process to create a Mattermost account on the team.
diff --git a/doc/help/system-console/Team-Statistics.md b/doc/help/system-console/Team-Statistics.md
new file mode 100644
index 000000000..eef7b8346
--- /dev/null
+++ b/doc/help/system-console/Team-Statistics.md
@@ -0,0 +1,24 @@
+# Team Statistics
+
+Statistics on users, posts and channels are tracked for each team are viewable under **System Console** > **Teams** > **Statistics**.
+
+## Total Users
+The total number of accounts created, including both active and inactive accounts.
+
+## Total Posts
+The total number of posts made in a team, including deleted posts and posts made using automation.
+
+## Public Groups
+The number of public channels created by your team, including channels that may have been archived.
+
+## Private Group
+The number of private groups created by your team, including groups that may have been archived.
+
+## Active Users With Posts
+Users who logged in and made a post on a certain day.
+
+## Recently Active Users
+Users that have logged in and had recent browser activity in Mattermost.
+
+## Newly Created Users
+Users that have recently completed the sign-up process to create a Mattermost account on the team.
diff --git a/doc/process/documentation-guidelines.md b/doc/process/documentation-guidelines.md
index f186da6bd..e75bb3169 100644
--- a/doc/process/documentation-guidelines.md
+++ b/doc/process/documentation-guidelines.md
@@ -41,7 +41,7 @@ This procedure works on Linux servers running Python 2.6 and higher.
### Use headings
-Headings in markdown provide anchors that can be used to easily reference sub-sections of long pieces of documentation. This is preferrable to just numbering sections without headings.
+Headings in markdown provide anchors that can be used to easily reference sub-sections of long pieces of documentation. This is preferable to just numbering sections without headings.
##### Correct:
@@ -77,7 +77,24 @@ H3, H4, H5 headings should be "Sentence case" and can be any length.
These headers are smaller and used to summarize sections. H3 can be considered either a large or small heading.
-These conventions are new, so there's flexibility around them, when you're not sure, consider the convention here as default.
+These conventions are new, so there's flexibility around them, when you're not sure, consider the convention here as default.
+
+### Sub-section headings should end with a colon
+
+For readability and clear layout, end a sub-section heading with a colon
+
+##### Correct:
+
+----
+Service Based:
+- [AWS Elastic Beanstalk Setup](https://github.com/mattermost/platform/blob/master/doc/install/Amazon-Elastic-Beanstalk.md)
+----
+##### Incorrect:
+
+----
+Optional
+- [Community Guide for Production Debian Setup](https://github.com/mattermost/platform/blob/master/doc/install/Production-Debian.md)
+----
### One instruction per line
diff --git a/docker/1.2/Dockerfile b/docker/1.2/Dockerfile
index 6a8d69196..0e4e3e242 100644
--- a/docker/1.2/Dockerfile
+++ b/docker/1.2/Dockerfile
@@ -34,7 +34,7 @@ VOLUME /var/lib/mysql
WORKDIR /mattermost
# Copy over files
-ADD https://github.com/mattermost/platform/releases/download/v1.2.0-rc1/mattermost.tar.gz /
+ADD https://github.com/mattermost/platform/releases/download/v1.2.0-rc2/mattermost.tar.gz /
RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz
ADD config_docker.json /
ADD docker-entry.sh /
diff --git a/model/channel_extra.go b/model/channel_extra.go
index c6f0ca192..55da588af 100644
--- a/model/channel_extra.go
+++ b/model/channel_extra.go
@@ -23,8 +23,9 @@ func (o *ExtraMember) Sanitize(options map[string]bool) {
}
type ChannelExtra struct {
- Id string `json:"id"`
- Members []ExtraMember `json:"members"`
+ Id string `json:"id"`
+ Members []ExtraMember `json:"members"`
+ MemberCount int64 `json:"member_count"`
}
func (o *ChannelExtra) ToJson() string {
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index fc4e19442..a9f99bd67 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -542,6 +542,26 @@ func (s SqlChannelStore) GetMember(channelId string, userId string) StoreChannel
return storeChannel
}
+func (s SqlChannelStore) GetMemberCount(channelId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ count, err := s.GetReplica().SelectInt("SELECT count(*) FROM ChannelMembers WHERE ChannelId = :ChannelId", map[string]interface{}{"ChannelId": channelId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.GetMemberCount", "We couldn't get the channel member count", "channel_id="+channelId+", "+err.Error())
+ } else {
+ result.Data = count
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlChannelStore) GetExtraMembers(channelId string, limit int) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go
index f6a0fb713..8662fcbd3 100644
--- a/store/sql_channel_store_test.go
+++ b/store/sql_channel_store_test.go
@@ -339,15 +339,15 @@ func TestChannelMemberStore(t *testing.T) {
t.Fatal("Member update time incorrect")
}
- members := (<-store.Channel().GetMembers(o1.ChannelId)).Data.([]model.ChannelMember)
- if len(members) != 2 {
+ count := (<-store.Channel().GetMemberCount(o1.ChannelId)).Data.(int64)
+ if count != 2 {
t.Fatal("should have saved 2 members")
}
Must(store.Channel().RemoveMember(o2.ChannelId, o2.UserId))
- members = (<-store.Channel().GetMembers(o1.ChannelId)).Data.([]model.ChannelMember)
- if len(members) != 1 {
+ count = (<-store.Channel().GetMemberCount(o1.ChannelId)).Data.(int64)
+ if count != 1 {
t.Fatal("should have removed 1 member")
}
diff --git a/store/store.go b/store/store.go
index ce4d90883..13b59b582 100644
--- a/store/store.go
+++ b/store/store.go
@@ -70,6 +70,7 @@ type ChannelStore interface {
UpdateMember(member *model.ChannelMember) StoreChannel
GetMembers(channelId string) StoreChannel
GetMember(channelId string, userId string) StoreChannel
+ GetMemberCount(channelId string) StoreChannel
RemoveMember(channelId string, userId string) StoreChannel
GetExtraMembers(channelId string, limit int) StoreChannel
CheckPermissionsTo(teamId string, channelId string, userId string) StoreChannel
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index 27959ec7e..ab5686720 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -54,7 +54,7 @@ export default class AccessHistoryModal extends React.Component {
}
onAuditChange() {
var newState = this.getStateFromStoresForAudits();
- if (!Utils.areStatesEqual(newState.audits, this.state.audits)) {
+ if (!Utils.areObjectsEqual(newState.audits, this.state.audits)) {
this.setState(newState);
}
}
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index ef3077470..af423a601 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -73,7 +73,7 @@ export default class ActivityLogModal extends React.Component {
}
onListenerChange() {
const newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(newState.sessions, this.state.sessions)) {
+ if (!Utils.areObjectsEqual(newState.sessions, this.state.sessions)) {
this.setState(newState);
}
}
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 895dc5fe4..a8d4ec100 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -39,11 +39,14 @@ export default class ChannelHeader extends React.Component {
this.state = state;
}
getStateFromStores() {
+ const extraInfo = ChannelStore.getCurrentExtraInfo();
+
return {
channel: ChannelStore.getCurrent(),
memberChannel: ChannelStore.getCurrentMember(),
memberTeam: UserStore.getCurrentUser(),
- users: ChannelStore.getCurrentExtraInfo().members,
+ users: extraInfo.members,
+ userCount: extraInfo.member_count,
searchVisible: SearchStore.getSearchResults() !== null
};
}
@@ -63,7 +66,7 @@ export default class ChannelHeader extends React.Component {
}
onListenerChange() {
const newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
$('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}});
@@ -373,6 +376,7 @@ export default class ChannelHeader extends React.Component {
<th>
<PopoverListMembers
members={this.state.users}
+ memberCount={this.state.userCount}
channelId={channel.id}
/>
</th>
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 7c1032321..47bc50971 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -78,7 +78,7 @@ export default class ChannelInviteModal extends React.Component {
}
onListenerChange() {
var newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(this.state, newState)) {
+ if (!Utils.areObjectsEqual(this.state, newState)) {
this.setState(newState);
}
}
diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx
index 2fa7ae8ff..5cf3511f4 100644
--- a/web/react/components/channel_members_modal.jsx
+++ b/web/react/components/channel_members_modal.jsx
@@ -91,7 +91,7 @@ export default class ChannelMembersModal extends React.Component {
}
onChange() {
const newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(this.state, newState)) {
+ if (!Utils.areObjectsEqual(this.state, newState)) {
this.setState(newState);
}
}
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx
index 43700bf36..f57fc12c5 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications.jsx
@@ -69,7 +69,7 @@ export default class ChannelNotifications extends React.Component {
newState.notifyLevel = notifyLevel;
newState.markUnreadLevel = markUnreadLevel;
- if (!Utils.areStatesEqual(this.state, newState)) {
+ if (!Utils.areObjectsEqual(this.state, newState)) {
this.setState(newState);
}
}
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index 3a3dabce5..f3bead1c2 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -81,7 +81,7 @@ export default class DeletePostModal extends React.Component {
}
onListenerChange() {
var newList = PostStore.getSelectedPost();
- if (!Utils.areStatesEqual(this.state.selectedList, newList)) {
+ if (!Utils.areObjectsEqual(this.state.selectedList, newList)) {
this.setState({selectedList: newList});
}
}
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index e707e32f5..d6a30abf9 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -67,7 +67,7 @@ export default class FileAttachment extends React.Component {
this.canSetState = false;
}
shouldComponentUpdate(nextProps, nextState) {
- if (!utils.areStatesEqual(nextProps, this.props)) {
+ if (!utils.areObjectsEqual(nextProps, this.props)) {
return true;
}
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index c09477a69..3f6ad3358 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -31,7 +31,8 @@ export default class InviteMemberModal extends React.Component {
firstNameErrors: {},
lastNameErrors: {},
emailEnabled: global.window.mm_config.SendEmailNotifications === 'true',
- showConfirmModal: false
+ showConfirmModal: false,
+ isSendingEmails: false
};
}
@@ -89,10 +90,13 @@ export default class InviteMemberModal extends React.Component {
var data = {};
data.invites = invites;
+ this.setState({isSendingEmails: true});
+
Client.inviteMembers(
data,
() => {
this.handleHide(false);
+ this.setState({isSendingEmails: false});
},
(err) => {
if (err.message === 'This person is already on your team') {
@@ -101,6 +105,8 @@ export default class InviteMemberModal extends React.Component {
} else {
this.setState({serverError: err.message});
}
+
+ this.setState({isSendingEmails: false});
}
);
}
@@ -289,11 +295,6 @@ export default class InviteMemberModal extends React.Component {
var content = null;
var sendButton = null;
- var sendButtonLabel = 'Send Invitation';
- if (this.state.inviteIds.length > 1) {
- sendButtonLabel = 'Send Invitations';
- }
-
if (this.state.emailEnabled) {
content = (
<div>
@@ -309,14 +310,25 @@ export default class InviteMemberModal extends React.Component {
</div>
);
- sendButton =
- (
- <button
- onClick={this.handleSubmit}
- type='button'
- className='btn btn-primary'
- >{sendButtonLabel}</button>
+ var sendButtonLabel = 'Send Invitation';
+ if (this.state.isSendingEmails) {
+ sendButtonLabel = (
+ <span><i className='fa fa-spinner fa-spin' />{' Sending'}</span>
);
+ } else if (this.state.inviteIds.length > 1) {
+ sendButtonLabel = 'Send Invitations';
+ }
+
+ sendButton = (
+ <button
+ onClick={this.handleSubmit}
+ type='button'
+ className='btn btn-primary'
+ disabled={this.state.isSendingEmails}
+ >
+ {sendButtonLabel}
+ </button>
+ );
} else {
var teamInviteLink = null;
if (currentUser && TeamStore.getCurrent().type === 'O') {
@@ -351,12 +363,13 @@ export default class InviteMemberModal extends React.Component {
return (
<div>
<Modal
- className='modal-invite-member'
+ dialogClassName='modal-invite-member'
show={this.state.show}
onHide={this.handleHide.bind(this, true)}
enforceFocus={!this.state.showConfirmModal}
+ backdrop={this.state.isSendingEmails ? 'static' : true}
>
- <Modal.Header closeButton={true}>
+ <Modal.Header closeButton={!this.state.isSendingEmails}>
<Modal.Title>{'Invite New Member'}</Modal.Title>
</Modal.Header>
<Modal.Body ref='modalBody'>
@@ -370,6 +383,7 @@ export default class InviteMemberModal extends React.Component {
type='button'
className='btn btn-default'
onClick={this.handleHide.bind(this, true)}
+ disabled={this.state.isSendingEmails}
>
{'Cancel'}
</button>
diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx
index c4f831c2e..8a6dd84a4 100644
--- a/web/react/components/more_channels.jsx
+++ b/web/react/components/more_channels.jsx
@@ -46,7 +46,7 @@ export default class MoreChannels extends React.Component {
}
onListenerChange() {
var newState = getStateFromStores();
- if (!utils.areStatesEqual(newState.channels, this.state.channels)) {
+ if (!utils.areObjectsEqual(newState.channels, this.state.channels)) {
this.setState(newState);
}
}
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index ff53816c7..af29f219e 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -140,7 +140,9 @@ export default class Navbar extends React.Component {
role='menuitem'
href='#'
onClick={() => this.setState({showEditChannelPurposeModal: true})}
- />
+ >
+ {'Set Channel Purpose...'}
+ </a>
</li>
);
}
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 0b755f377..cf9db055d 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -70,7 +70,7 @@ export default class NavbarDropdown extends React.Component {
}
onListenerChange() {
var newState = getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
diff --git a/web/react/components/notify_counts.jsx b/web/react/components/notify_counts.jsx
index 54b9e4289..0a4f60989 100644
--- a/web/react/components/notify_counts.jsx
+++ b/web/react/components/notify_counts.jsx
@@ -39,7 +39,7 @@ export default class NotifyCounts extends React.Component {
}
onListenerChange() {
var newState = getCountsStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
+ if (!utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index f3c0fa0b4..bd6b6d3bd 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -69,7 +69,6 @@ export default class PopoverListMembers extends React.Component {
render() {
let popoverHtml = [];
- let count = 0;
let countText = '-';
const members = this.props.members;
const teamMembers = UserStore.getProfilesUsernameMap();
@@ -147,10 +146,10 @@ export default class PopoverListMembers extends React.Component {
</div>
</div>
);
- count++;
}
});
+ const count = this.props.memberCount;
if (count > 20) {
countText = '20+';
} else if (count > 0) {
@@ -195,5 +194,6 @@ export default class PopoverListMembers extends React.Component {
PopoverListMembers.propTypes = {
members: React.PropTypes.array.isRequired,
+ memberCount: React.PropTypes.number.isRequired,
channelId: React.PropTypes.string.isRequired
};
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index c3c5b3e0b..2b9586345 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -77,7 +77,7 @@ export default class Post extends React.Component {
this.forceUpdate();
}
shouldComponentUpdate(nextProps) {
- if (!utils.areStatesEqual(nextProps.post, this.props.post)) {
+ if (!utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true;
}
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index c57c4490b..617b4b36c 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -18,6 +18,7 @@ export default class PostBody extends React.Component {
this.receivedYoutubeData = false;
this.isGifLoading = false;
+ this.handleUserChange = this.handleUserChange.bind(this);
this.parseEmojis = this.parseEmojis.bind(this);
this.createEmbed = this.createEmbed.bind(this);
this.createGifEmbed = this.createGifEmbed.bind(this);
@@ -25,7 +26,14 @@ export default class PostBody extends React.Component {
this.createYoutubeEmbed = this.createYoutubeEmbed.bind(this);
const linkData = Utils.extractLinks(this.props.post.message);
- this.state = {links: linkData.links, message: linkData.text, post: this.props.post};
+ const profiles = UserStore.getProfiles();
+
+ this.state = {
+ links: linkData.links,
+ message: linkData.text,
+ post: this.props.post,
+ hasUserProfiles: profiles && Object.keys(profiles).length > 1
+ };
}
getAllChildNodes(nodeIn) {
@@ -55,12 +63,26 @@ export default class PostBody extends React.Component {
componentDidMount() {
this.parseEmojis();
+
+ UserStore.addChangeListener(this.handleUserChange);
}
componentDidUpdate() {
this.parseEmojis();
}
+ componentWillUnmount() {
+ UserStore.removeChangeListener(this.handleUserChange);
+ }
+
+ handleUserChange() {
+ if (!this.state.hasProfiles) {
+ const profiles = UserStore.getProfiles();
+
+ this.setState({hasProfiles: profiles && Object.keys(profiles).length > 1});
+ }
+ }
+
componentWillReceiveProps(nextProps) {
const linkData = Utils.extractLinks(nextProps.post.message);
if (this.props.post.filenames.length === 0 && this.state.links && this.state.links.length > 0) {
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index b782268fa..087ca1df2 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -104,11 +104,13 @@ export default class PostsView extends React.Component {
// check if it's the last comment in a consecutive string of comments on the same post
// it is the last comment if it is last post in the channel or the next post has a different root post
- var isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
+ const isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
- var postCtl = (
+ const keyPrefix = post.id ? post.id : i;
+
+ const postCtl = (
<Post
- key={post.id + 'postKey'}
+ key={keyPrefix + 'postKey'}
ref={post.id}
sameUser={sameUser}
sameRoot={sameRoot}
@@ -240,7 +242,7 @@ export default class PostsView extends React.Component {
if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) {
return true;
}
- if (!Utils.areStatesEqual(this.props.postList, nextProps.postList)) {
+ if (!Utils.areObjectsEqual(this.props.postList, nextProps.postList)) {
return true;
}
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
index 5037a86cd..2cb56cd47 100644
--- a/web/react/components/posts_view_container.jsx
+++ b/web/react/components/posts_view_container.jsx
@@ -3,6 +3,7 @@
const PostsView = require('./posts_view.jsx');
const LoadingScreen = require('./loading_screen.jsx');
+const ChannelInviteModal = require('./channel_invite_modal.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
const PostStore = require('../stores/post_store.jsx');
@@ -50,6 +51,7 @@ export default class PostsViewContainer extends React.Component {
});
}
+ state.showInviteModal = false;
this.state = state;
}
componentDidMount() {
@@ -223,7 +225,7 @@ export default class PostsViewContainer extends React.Component {
}
}
shouldComponentUpdate(nextProps, nextState) {
- if (Utils.areStatesEqual(this.state, nextState)) {
+ if (Utils.areObjectsEqual(this.state, nextState)) {
return false;
}
@@ -248,7 +250,7 @@ export default class PostsViewContainer extends React.Component {
postViewScrolled={this.handlePostsViewScroll}
loadMorePostsTopClicked={this.loadMorePostsTop}
numPostsToDisplay={this.state.numPostsToDisplay}
- introText={channel ? createChannelIntroMessage(channel) : null}
+ introText={channel ? createChannelIntroMessage(channel, () => this.setState({showInviteModal: true})) : null}
messageSeparatorTime={this.state.currentLastViewed}
/>
);
@@ -263,7 +265,13 @@ export default class PostsViewContainer extends React.Component {
}
return (
- <div id='post-list'>{postListCtls}</div>
+ <div id='post-list'>
+ {postListCtls}
+ <ChannelInviteModal
+ show={this.state.showInviteModal}
+ onModalDismissed={() => this.setState({showInviteModal: false})}
+ />
+ </div>
);
}
}
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index 8c6324c72..58cc1cac7 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -61,7 +61,7 @@ export default class RhsComment extends React.Component {
this.parseEmojis();
}
shouldComponentUpdate(nextProps) {
- if (!Utils.areStatesEqual(nextProps.post, this.props.post)) {
+ if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true;
}
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index e3b023841..69de5d523 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -26,7 +26,7 @@ export default class RhsRootPost extends React.Component {
this.parseEmojis();
}
shouldComponentUpdate(nextProps) {
- if (!utils.areStatesEqual(nextProps.post, this.props.post)) {
+ if (!utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true;
}
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index fe57bed28..7c11de7cf 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -82,7 +82,7 @@ export default class RhsThread extends React.Component {
}
onChange() {
var newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
@@ -112,7 +112,7 @@ export default class RhsThread extends React.Component {
}
var newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx
index 90865475b..0f749f2cf 100644
--- a/web/react/components/search_bar.jsx
+++ b/web/react/components/search_bar.jsx
@@ -46,7 +46,7 @@ export default class SearchBar extends React.Component {
onListenerChange(doSearch, isMentionSearch) {
if (this.mounted) {
var newState = this.getSearchTermStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
+ if (!utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
if (doSearch) {
diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx
index b56a7b006..2f0068908 100644
--- a/web/react/components/search_results.jsx
+++ b/web/react/components/search_results.jsx
@@ -55,7 +55,7 @@ export default class SearchResults extends React.Component {
onChange() {
if (this.mounted) {
var newState = getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index f5ce5c10e..542f433f3 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -106,6 +106,8 @@ export default class Sidebar extends React.Component {
const currentChannelId = ChannelStore.getCurrentId();
const channels = Object.assign([], ChannelStore.getAll());
+ channels.sort((a, b) => a.display_name.localeCompare(b.display_name));
+
const publicChannels = channels.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
const privateChannels = channels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
const directChannels = channels.filter((channel) => channel.type === Constants.DM_CHANNEL);
@@ -142,7 +144,7 @@ export default class Sidebar extends React.Component {
}
}
- const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList().length - visibleDirectChannels.length;
+ const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList(true).length - visibleDirectChannels.length;
visibleDirectChannels.sort(this.sortChannelsByDisplayName);
@@ -173,7 +175,7 @@ export default class Sidebar extends React.Component {
window.addEventListener('resize', this.handleResize);
}
shouldComponentUpdate(nextProps, nextState) {
- if (!Utils.areStatesEqual(nextState, this.state)) {
+ if (!Utils.areObjectsEqual(nextState, this.state)) {
return true;
}
return false;
@@ -205,10 +207,7 @@ export default class Sidebar extends React.Component {
}
}
onChange() {
- var newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
- this.setState(newState);
- }
+ this.setState(this.getStateFromStores());
}
updateTitle() {
const channel = ChannelStore.getCurrent();
diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx
index e2ef60959..ab558ad0f 100644
--- a/web/react/components/sidebar_right.jsx
+++ b/web/react/components/sidebar_right.jsx
@@ -66,13 +66,13 @@ export default class SidebarRight extends React.Component {
onSelectedChange(fromSearch) {
var newState = getStateFromStores(fromSearch);
newState.from_search = fromSearch;
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
onSearchChange() {
var newState = getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
diff --git a/web/react/components/team_members.jsx b/web/react/components/team_members.jsx
index ac1ebf52d..afe7f46ec 100644
--- a/web/react/components/team_members.jsx
+++ b/web/react/components/team_members.jsx
@@ -59,7 +59,7 @@ export default class TeamMembers extends React.Component {
onChange() {
var newState = getStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
+ if (!utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx
index 09674f1ef..862f3c528 100644
--- a/web/react/components/team_settings.jsx
+++ b/web/react/components/team_settings.jsx
@@ -23,7 +23,7 @@ export default class TeamSettings extends React.Component {
}
onChange() {
var team = TeamStore.getCurrent();
- if (!Utils.areStatesEqual(this.state.team, team)) {
+ if (!Utils.areObjectsEqual(this.state.team, team)) {
this.setState({team});
}
}
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index 82f830038..e6530b941 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -243,7 +243,6 @@ export default class Textbox extends React.Component {
const lht = parseInt($(e).css('lineHeight'), 10);
const lines = e.scrollHeight / lht;
- const previewLinkHeightMod = 20;
let mod = 15;
if (lines < 2.5 || this.props.messageText === '') {
@@ -252,17 +251,17 @@ export default class Textbox extends React.Component {
if (e.scrollHeight - mod < 167) {
$(e).css({height: 'auto', 'overflow-y': 'hidden'}).height(e.scrollHeight - mod);
- $(w).css({height: 'auto'}).height(e.scrollHeight + 2 + previewLinkHeightMod);
+ $(w).css({height: 'auto'}).height(e.scrollHeight + 2);
$(w).closest('.post-body__cell').removeClass('scroll');
if (this.state.preview) {
$(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'auto'}).height(e.scrollHeight - mod);
}
} else {
$(e).css({height: 'auto', 'overflow-y': 'scroll'}).height(167 - mod);
- $(w).css({height: 'auto'}).height(167 + previewLinkHeightMod);
+ $(w).css({height: 'auto'}).height(163);
$(w).closest('.post-body__cell').addClass('scroll');
if (this.state.preview) {
- $(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'scroll'}).height(167 - mod);
+ $(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'scroll'}).height(163);
}
}
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index eb0a8f0ca..a2523ef68 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -29,7 +29,7 @@ export default class UserProfile extends React.Component {
return {profile: {id: '0', username: '...'}};
}
- return {profile: profile};
+ return {profile};
}
componentDidMount() {
UserStore.addChangeListener(this.onChange);
@@ -43,7 +43,7 @@ export default class UserProfile extends React.Component {
onChange(userId) {
if (!userId || userId === this.props.userId) {
var newState = this.getStateFromStores(this.props.userId);
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx
index 895d0c500..3dbed72c3 100644
--- a/web/react/components/user_settings/custom_theme_chooser.jsx
+++ b/web/react/components/user_settings/custom_theme_chooser.jsx
@@ -129,7 +129,6 @@ export default class CustomThemeChooser extends React.Component {
{'Copy and paste to share theme colors:'}
</label>
<input
- readOnly='true'
type='text'
className='form-control'
value={colors}
diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx
index e089ce973..40825ba93 100644
--- a/web/react/components/user_settings/user_settings.jsx
+++ b/web/react/components/user_settings/user_settings.jsx
@@ -36,7 +36,7 @@ export default class UserSettings extends React.Component {
onListenerChange() {
var user = UserStore.getCurrentUser();
- if (!utils.areStatesEqual(this.state.user, user)) {
+ if (!utils.areObjectsEqual(this.state.user, user)) {
this.setState({user});
}
}
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index d73b5f476..029a1af5e 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -1,13 +1,15 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../../stores/user_store.jsx');
-var Client = require('../../utils/client.jsx');
-var Utils = require('../../utils/utils.jsx');
-
const CustomThemeChooser = require('./custom_theme_chooser.jsx');
const PremadeThemeChooser = require('./premade_theme_chooser.jsx');
+
+const UserStore = require('../../stores/user_store.jsx');
+
const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx');
+const Client = require('../../utils/client.jsx');
+const Utils = require('../../utils/utils.jsx');
+
const Constants = require('../../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
@@ -66,7 +68,7 @@ export default class UserSettingsAppearance extends React.Component {
onChange() {
const newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(this.state, newState)) {
+ if (!Utils.areObjectsEqual(this.state, newState)) {
this.setState(newState);
}
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index c6f47804f..c958bf5bc 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -1,16 +1,18 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../../stores/user_store.jsx');
-var SettingItemMin = require('../setting_item_min.jsx');
-var SettingItemMax = require('../setting_item_max.jsx');
-var client = require('../../utils/client.jsx');
-var AsyncClient = require('../../utils/async_client.jsx');
-var utils = require('../../utils/utils.jsx');
+const SettingItemMin = require('../setting_item_min.jsx');
+const SettingItemMax = require('../setting_item_max.jsx');
+
+const UserStore = require('../../stores/user_store.jsx');
+
+const Client = require('../../utils/client.jsx');
+const AsyncClient = require('../../utils/async_client.jsx');
+const Utils = require('../../utils/utils.jsx');
function getNotificationsStateFromStores() {
var user = UserStore.getCurrentUser();
- var soundNeeded = !utils.isBrowserFirefox();
+ var soundNeeded = !Utils.isBrowserFirefox();
var sound = 'true';
if (user.notify_props && user.notify_props.desktop_sound) {
@@ -116,7 +118,7 @@ export default class NotificationsTab extends React.Component {
data.all = this.state.allKey.toString();
data.channel = this.state.channelKey.toString();
- client.updateUserNotifyProps(data,
+ Client.updateUserNotifyProps(data,
function success() {
this.props.updateSection('');
AsyncClient.getMe();
@@ -138,7 +140,7 @@ export default class NotificationsTab extends React.Component {
}
onListenerChange() {
var newState = getNotificationsStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx
index 6b7d671fc..40b64b34b 100644
--- a/web/react/stores/user_store.jsx
+++ b/web/react/stores/user_store.jsx
@@ -164,6 +164,10 @@ class UserStoreClass extends EventEmitter {
}
getProfile(userId) {
+ if (userId === this.getCurrentId()) {
+ return this.getCurrentUser();
+ }
+
return this.getProfiles()[userId];
}
@@ -193,13 +197,13 @@ class UserStoreClass extends EventEmitter {
return BrowserStore.getItem('profiles', {});
}
- getActiveOnlyProfiles() {
+ getActiveOnlyProfiles(skipCurrent) {
const active = {};
const profiles = this.getProfiles();
const currentId = this.getCurrentId();
for (var key in profiles) {
- if (profiles[key].delete_at === 0 && profiles[key].id !== currentId) {
+ if (!(profiles[key].id === currentId && skipCurrent) && profiles[key].delete_at === 0) {
active[key] = profiles[key];
}
}
diff --git a/web/react/utils/channel_intro_mssages.jsx b/web/react/utils/channel_intro_mssages.jsx
index 161c79761..f27e23a82 100644
--- a/web/react/utils/channel_intro_mssages.jsx
+++ b/web/react/utils/channel_intro_mssages.jsx
@@ -9,15 +9,15 @@ const ChannelStore = require('../stores/channel_store.jsx');
const Constants = require('../utils/constants.jsx');
const TeamStore = require('../stores/team_store.jsx');
-export function createChannelIntroMessage(channel) {
+export function createChannelIntroMessage(channel, showInviteModal) {
if (channel.type === 'D') {
return createDMIntroMessage(channel);
} else if (ChannelStore.isDefault(channel)) {
return createDefaultIntroMessage(channel);
} else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
- return createOffTopicIntroMessage(channel);
+ return createOffTopicIntroMessage(channel, showInviteModal);
} else if (channel.type === 'O' || channel.type === 'P') {
- return createStandardIntroMessage(channel);
+ return createStandardIntroMessage(channel, showInviteModal);
}
}
@@ -71,7 +71,7 @@ export function createDMIntroMessage(channel) {
);
}
-export function createOffTopicIntroMessage(channel) {
+export function createOffTopicIntroMessage(channel, showInviteModal) {
return (
<div className='channel-intro'>
<h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
@@ -91,10 +91,8 @@ export function createOffTopicIntroMessage(channel) {
<i className='fa fa-pencil'></i>{'Set a header'}
</a>
<a
- className='intro-links'
href='#'
- data-toggle='modal'
- data-target='#channel_invite'
+ onClick={showInviteModal}
>
<i className='fa fa-user-plus'></i>{'Invite others to this channel'}
</a>
@@ -155,7 +153,7 @@ export function createDefaultIntroMessage(channel) {
);
}
-export function createStandardIntroMessage(channel) {
+export function createStandardIntroMessage(channel, showInviteModal) {
var uiName = channel.display_name;
var creatorName = '';
@@ -206,14 +204,11 @@ export function createStandardIntroMessage(channel) {
<i className='fa fa-pencil'></i>{'Set a header'}
</a>
<a
- className='intro-links'
href='#'
- data-toggle='modal'
- data-target='#channel_invite'
+ onClick={showInviteModal}
>
<i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType}
</a>
-
</div>
);
}
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 38f91b35f..6f3924829 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -311,8 +311,98 @@ export function escapeRegExp(string) {
return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
}
-export function areStatesEqual(state1, state2) {
- return JSON.stringify(state1) === JSON.stringify(state2);
+// Taken from http://stackoverflow.com/questions/1068834/object-comparison-in-javascript and modified slightly
+export function areObjectsEqual(x, y) {
+ let p;
+ const leftChain = [];
+ const rightChain = [];
+
+ // Remember that NaN === NaN returns false
+ // and isNaN(undefined) returns true
+ if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
+ return true;
+ }
+
+ // Compare primitives and functions.
+ // Check if both arguments link to the same object.
+ // Especially useful on step when comparing prototypes
+ if (x === y) {
+ return true;
+ }
+
+ // Works in case when functions are created in constructor.
+ // Comparing dates is a common scenario. Another built-ins?
+ // We can even handle functions passed across iframes
+ if ((typeof x === 'function' && typeof y === 'function') ||
+ (x instanceof Date && y instanceof Date) ||
+ (x instanceof RegExp && y instanceof RegExp) ||
+ (x instanceof String && y instanceof String) ||
+ (x instanceof Number && y instanceof Number)) {
+ return x.toString() === y.toString();
+ }
+
+ // At last checking prototypes as good a we can
+ if (!(x instanceof Object && y instanceof Object)) {
+ return false;
+ }
+
+ if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) {
+ return false;
+ }
+
+ if (x.constructor !== y.constructor) {
+ return false;
+ }
+
+ if (x.prototype !== y.prototype) {
+ return false;
+ }
+
+ // Check for infinitive linking loops
+ if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) {
+ return false;
+ }
+
+ // Quick checking of one object beeing a subset of another.
+ for (p in y) {
+ if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
+ return false;
+ } else if (typeof y[p] !== typeof x[p]) {
+ return false;
+ }
+ }
+
+ for (p in x) {
+ if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
+ return false;
+ } else if (typeof y[p] !== typeof x[p]) {
+ return false;
+ }
+
+ switch (typeof (x[p])) {
+ case 'object':
+ case 'function':
+
+ leftChain.push(x);
+ rightChain.push(y);
+
+ if (!areObjectsEqual(x[p], y[p])) {
+ return false;
+ }
+
+ leftChain.pop();
+ rightChain.pop();
+ break;
+
+ default:
+ if (x[p] !== y[p]) {
+ return false;
+ }
+ break;
+ }
+ }
+
+ return true;
}
export function replaceHtmlEntities(text) {
diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss
index d3ab3b9f8..49fb8e847 100644
--- a/web/sass-files/sass/partials/_files.scss
+++ b/web/sass-files/sass/partials/_files.scss
@@ -1,5 +1,6 @@
.preview-container {
position: relative;
+ margin-top: 10px;
width: 100%;
max-height: 110px;
height: 110px;
diff --git a/web/sass-files/sass/partials/_navbar.scss b/web/sass-files/sass/partials/_navbar.scss
index 2e78a8728..c06feffcf 100644
--- a/web/sass-files/sass/partials/_navbar.scss
+++ b/web/sass-files/sass/partials/_navbar.scss
@@ -96,9 +96,9 @@
}
.badge-notify {
- background:red;
+ background: red;
position: absolute;
- right: -5px;
- top: -5px;
+ left: 4px;
+ top: 3px;
z-index: 100;
}
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 3e2d6f045..33748052d 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -46,21 +46,22 @@ body.ios {
.textarea-wrapper {
position:relative;
- min-height:57px;
- .textbox-preview-area {
- position: absolute;
- z-index: 2;
- top: 0;
- left: 0;
- box-shadow: none;
- }
- .textbox-preview-link {
- position: absolute;
- z-index: 3;
- bottom: 0;
- right: 10px;
- cursor: pointer;
- }
+ min-height: 36px;
+ .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;
+ font-size: 13px;
+ cursor: pointer;
+ }
}
.date-separator, .new-separator {
@@ -338,9 +339,9 @@ body.ios {
}
}
.msg-typing {
- min-height: 20px;
- line-height: 18px;
- display: inline-block;
+ min-height: 25px;
+ line-height: 25px;
+ display: block;
font-size: 13px;
@include opacity(0.7);
}
@@ -658,6 +659,7 @@ body.ios {
float: left;
width: 80%;
padding-right: 5px;
+ overflow-x: auto;
&.attachment__body--no_thumb {
width: 100%;
}
diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss
index c1d291073..ba41d3b95 100644
--- a/web/sass-files/sass/partials/_post_right.scss
+++ b/web/sass-files/sass/partials/_post_right.scss
@@ -13,6 +13,7 @@
&.post--root {
padding: 1em 1em 0;
margin: 0 0 1em;
+ width: 100%;
hr {
border-color: #DDD;
margin: 1em 0 0 0;
@@ -21,9 +22,10 @@
}
.post-create__container {
+ width: 100%;
margin-top: 10px;
.textarea-wrapper {
- min-height: 120px;
+ min-height: 100px;
}
.custom-textarea {
min-height: 100px;
@@ -31,10 +33,18 @@
.msg-typing {
@include opacity(0.7);
float: left;
- padding-top: 17px;
+ margin-top: 3px;
+ font-size: 13px;
+ line-height: 20px;
+ min-width: 1px;
+ display: block;
+ height: 20px;
+ max-width: 200px;
+ @include clearfix;
}
.post-create-footer {
- padding-top: 10px;
+ width: 100%;
+ padding-top: 5px;
}
.post-right-comments-upload-in-progress {
padding: 6px 0;
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 339412b45..cb140dce6 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -507,8 +507,16 @@
form {
padding: 0;
}
+ .post-create-footer {
+ .msg-typing {
+ margin-left: 45px;
+ width: 55%;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
.post-create-body {
- padding-bottom: 10px;
display: table;
width: 100%;
.post-body__cell {
@@ -532,11 +540,10 @@
display: table-cell;
}
}
- .post-create-footer .msg-typing {
- display: none;
- }
}
.preview-container {
+ padding: 0px 10px;
+ margin-top: 20px;
.preview-div {
margin-top: 0;
}
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index b304450bc..0d75a42df 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -64,6 +64,10 @@
}
}
}
+ .profile-img {
+ width: 128px;
+ height: 128px;
+ }
.settings-table {
display: table;
table-layout: fixed;
diff --git a/web/web.go b/web/web.go
index 1cae604ae..ffc791cb7 100644
--- a/web/web.go
+++ b/web/web.go
@@ -132,7 +132,7 @@ func watchAndParseTemplates() {
}
}
-var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7"
+var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7;Safari/8"
func CheckBrowserCompatability(c *api.Context, r *http.Request) bool {
ua := user_agent.New(r.UserAgent())
@@ -143,7 +143,7 @@ func CheckBrowserCompatability(c *api.Context, r *http.Request) bool {
version := strings.Split(browser, "/")
if strings.HasPrefix(bname, version[0]) && strings.HasPrefix(bversion, version[1]) {
- c.Err = model.NewAppError("CheckBrowserCompatability", "Your current browser is not supported, please upgrade to one of the following browsers: Google Chrome 21 or higher, Internet Explorer 11 or higher, FireFox 14 or higher, Safari 8 or higher", "")
+ c.Err = model.NewAppError("CheckBrowserCompatability", "Your current browser is not supported, please upgrade to one of the following browsers: Google Chrome 21 or higher, Internet Explorer 11 or higher, FireFox 14 or higher, Safari 9 or higher", "")
return false
}
}