summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Makefile20
-rw-r--r--api/post.go16
-rw-r--r--config/config.json5
-rw-r--r--doc/developer/Setup.md26
-rw-r--r--doc/developer/tests/README.md18
-rw-r--r--doc/developer/tests/test-markdown-basics.md139
-rw-r--r--doc/developer/tests/test-markdown-lists.md177
-rw-r--r--doc/developer/tests/test-markdown.md25
-rw-r--r--doc/help/Notifications.md31
-rw-r--r--doc/help/README.md15
-rw-r--r--doc/install/Configuration-Settings.md13
-rw-r--r--doc/process/overview.md2
-rw-r--r--docker/dev/config_docker.json5
-rw-r--r--docker/local/config_docker.json5
-rw-r--r--mattermost.go26
-rw-r--r--model/config.go18
-rw-r--r--model/push_notification.go45
-rw-r--r--model/push_notification_test.go19
-rw-r--r--utils/apns.go37
-rw-r--r--utils/config.go1
-rw-r--r--web/react/components/admin_console/email_settings.jsx67
-rw-r--r--web/react/components/channel_notifications_modal.jsx12
-rw-r--r--web/react/components/get_link_modal.jsx2
-rw-r--r--web/react/components/member_list_item.jsx2
-rw-r--r--web/react/components/member_list_team_item.jsx2
-rw-r--r--web/react/components/popover_list_members.jsx46
-rw-r--r--web/react/components/post.jsx7
-rw-r--r--web/react/components/posts_view.jsx67
-rw-r--r--web/react/components/posts_view_container.jsx4
-rw-r--r--web/react/components/register_app_modal.jsx192
-rw-r--r--web/react/components/sidebar.jsx6
-rw-r--r--web/react/components/user_profile.jsx6
-rw-r--r--web/react/components/user_settings/user_settings_developer.jsx12
-rw-r--r--web/react/components/view_image.jsx37
-rw-r--r--web/react/dispatcher/event_helpers.jsx7
-rw-r--r--web/react/stores/modal_store.jsx1
-rw-r--r--web/react/stores/post_store.jsx2
-rw-r--r--web/react/utils/constants.jsx4
-rw-r--r--web/react/utils/markdown.jsx10
-rw-r--r--web/react/utils/text_formatting.jsx2
-rw-r--r--web/react/utils/utils.jsx23
-rw-r--r--web/sass-files/sass/partials/_modal.scss40
-rw-r--r--web/sass-files/sass/partials/_post.scss5
-rw-r--r--web/sass-files/sass/partials/_responsive.scss15
-rw-r--r--web/sass-files/sass/partials/_search.scss1
46 files changed, 926 insertions, 291 deletions
diff --git a/.gitignore b/.gitignore
index 50cdca100..6e433df3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,8 @@ web/static/js/libs*.js
# Build Targets
.prepare
+.prepare-go
+.prepare-jsx
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
diff --git a/Makefile b/Makefile
index 87219e68c..098422052 100644
--- a/Makefile
+++ b/Makefile
@@ -33,7 +33,7 @@ dist: | build-server build-client go-test package
mv ./model/version.go.bak ./model/version.go
dist-local: | start-docker dist
-
+
dist-travis: | travis-init build-container
start-docker:
@@ -153,7 +153,7 @@ go-test:
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./utils || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./web || exit 1
-test: | start-docker go-test
+test: | start-docker .prepare-go go-test
travis-init:
@echo Setting up enviroment for travis
@@ -217,25 +217,29 @@ clean: stop-docker
rm -rf Godeps/_workspace/pkg/
rm -f mattermost.log
- rm -f .prepare
+ rm -f .prepare-go .prepare-jsx
nuke: | clean clean-docker
rm -rf data
-.prepare:
- @echo Preparation for run step
-
+.prepare-go:
+ @echo Preparation for running go code
go get $(GOFLAGS) github.com/tools/godep
+ touch $@
+
+.prepare-jsx:
+ @echo Preparation for compiling jsx code
+
cd web/react/ && npm install
cd web/react/ && npm run build-libs
touch $@
-run: start-docker .prepare
+run: start-docker .prepare-go .prepare-jsx
mkdir -p web/static/js
- @echo Starting react processor
+ @echo Starting react processo
cd web/react && npm start &
@echo Starting go web server
diff --git a/api/post.go b/api/post.go
index 88d0127d3..81cc9a1c6 100644
--- a/api/post.go
+++ b/api/post.go
@@ -536,7 +536,7 @@ func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team,
l4g.Error("Failed to send mention email successfully email=%v err=%v", profileMap[id].Email, err)
}
- if len(utils.Cfg.EmailSettings.ApplePushServer) > 0 {
+ if *utils.Cfg.EmailSettings.SendPushNotifications {
sessionChan := Srv.Store.Session().GetSessions(id)
if result := <-sessionChan; result.Err != nil {
l4g.Error("Failed to retrieve sessions in notifications id=%v, err=%v", id, result.Err)
@@ -548,7 +548,19 @@ func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team,
if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" && strings.HasPrefix(session.DeviceId, "apple:") {
alreadySeen[session.DeviceId] = session.DeviceId
- utils.SendAppleNotifyAndForget(strings.TrimPrefix(session.DeviceId, "apple:"), subjectPage.Render(), 1)
+ msg := model.PushNotification{}
+ msg.Platform = model.PUSH_NOTIFY_APPLE
+ msg.Message = subjectPage.Render()
+ msg.Badge = 1
+ msg.DeviceId = strings.TrimPrefix(session.DeviceId, "apple:")
+ msg.ServerId = utils.CfgDiagnosticId
+
+ httpClient := http.Client{}
+ request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+"/api/v1/send_push", strings.NewReader(msg.ToJson()))
+
+ if _, err := httpClient.Do(request); err != nil {
+ l4g.Error("Failed to send push notificationid=%v, err=%v", id, err)
+ }
}
}
}
diff --git a/config/config.json b/config/config.json
index 932bed8a2..999ea8a83 100644
--- a/config/config.json
+++ b/config/config.json
@@ -68,9 +68,8 @@
"ConnectionSecurity": "",
"InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
- "ApplePushServer": "",
- "ApplePushCertPublic": "",
- "ApplePushCertPrivate": ""
+ "SendPushNotifications": true,
+ "PushNotificationServer": "https://push.mattermost.com"
},
"RateLimitSettings": {
"EnableRateLimiter": true,
diff --git a/doc/developer/Setup.md b/doc/developer/Setup.md
index 882aac530..24e7d6a90 100644
--- a/doc/developer/Setup.md
+++ b/doc/developer/Setup.md
@@ -11,7 +11,10 @@ Developer Machine Setup
`docker-machine ip dev`
3. Add a line to your /etc/hosts that goes `<Docker IP> dockerhost`
4. Run `docker-machine env dev` and copy the export statements to your ~/.bash_profile
-2. Download Go 1.5.1 from http://golang.org/dl/
+2. Download Go 1.5.1 and Node.js using Homebrew
+ 1. Download Homebrew from http://brew.sh/
+ 2. `brew install go`
+ 3. `brew install node`
3. Set up your Go workspace
1. `mkdir ~/go`
2. Add the following to your ~/.bash_profile
@@ -21,20 +24,17 @@ Developer Machine Setup
If you don't increase the file handle limit you may see some weird build issues with browserify or npm.
3. Reload your bash profile
`source ~/.bash_profile`
-4. Install Node.js using Homebrew
- 1. Download Homebrew from http://brew.sh/
- 2. `brew install node`
-5. Install Compass
+4. Install Compass
1. Run `ruby -v` and check the ruby version is 1.8.7 or higher
2. `sudo gem install compass`
-6. Download Mattermost
+5. Download Mattermost
`cd ~/go`
`mkdir -p src/github.com/mattermost`
`cd src/github.com/mattermost`
`git clone https://github.com/mattermost/platform.git`
`cd platform`
-7. Run unit tests on Mattermost using `make test` to make sure the installation was successful
-8. If tests passed, you can now run Mattermost using `make run`
+6. Run unit tests on Mattermost using `make test` to make sure the installation was successful
+7. If tests passed, you can now run Mattermost using `make run`
Any issues? Please let us know on our forums at: http://forum.mattermost.org
@@ -64,13 +64,9 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org
If you don't increase the file handle limit you may see some weird build issues with browserify or npm.
3. Reload your bashrc
`source ~/.bashrc`
-6. Install Node.js
- 1. Download the newest version of the Node.js sources from https://nodejs.org/en/download/
- 2. Extract the contents of the package and cd into the extracted files
- 3. Compile and install Node.js
- `./configure`
- `make`
- `make install`
+6. Install Node.js
+ `curl -sL https://deb.nodesource.com/setup_5.x | sudo -E bash -`
+ `sudo apt-get install -y nodejs`
7. Install Ruby and Compass
`apt-get install ruby`
`apt-get install ruby-dev`
diff --git a/doc/developer/tests/README.md b/doc/developer/tests/README.md
new file mode 100644
index 000000000..a7b36f18d
--- /dev/null
+++ b/doc/developer/tests/README.md
@@ -0,0 +1,18 @@
+# Testing Text Processing
+The text processing tests located in the [doc/developer/tests folder](https://github.com/mattermost/platform/tree/master/doc/developer/tests) are designed for use with the `/loadtest url` command. This command posts the raw contents of a specified .md file in the doc/developer/test folder into Mattermost.
+
+## Turning on /loadtest
+Access the **System Console** from the Main Menu. Under *Service Settings* make sure that *Enable Testing* is set to `true`, then click **Save**. You may also change this setting from `config.json` by setting `”EnableTesting”: true`. Changing this setting requires a server restart to take effect.
+
+## Running the Tests
+In the text input box in Mattermost, type: `/loadtest url [file-name-in-testing-folder].md`. Some examples:
+
+`/loadtest url test-emoticons.md`
+`/loadtest url test-links.md`
+
+#### Notes:
+1. If a test has prerequisites, make sure your Mattermost setup meets the requirements described at the top of the test file.
+2. Some tests are over 4000 characters in length and will render across multiple posts.
+
+## Manual Testing
+It is possible to manually test specific sections of any test, instead of using the /loadtest command. Do this by clicking **Raw** in the header for the file when it’s open in GitHub, then copy and paste any section into Mattermost to post it. Manual testing only supports sections of 4000 characters or less per post.
diff --git a/doc/developer/tests/test-markdown-basics.md b/doc/developer/tests/test-markdown-basics.md
new file mode 100644
index 000000000..7a46adeb0
--- /dev/null
+++ b/doc/developer/tests/test-markdown-basics.md
@@ -0,0 +1,139 @@
+# Basic Markdown Testing
+Tests for text style, code blocks, in-line code and images, lines, block quotes, and headings.
+
+### Text Style
+
+**The following text should render as:**
+_Italics_
+*Italics*
+**Bold**
+***Bold-italics***
+**_Bold-italics_**
+~~Strikethrough~~
+
+This sentence contains **bold**, _italic_, ***bold-italic***, and ~~stikethrough~~ text.
+
+**The following should render as normal text:**
+Normal Text_
+_Normal Text
+_Normal Text*
+
+### Code Blocks
+
+```
+This text should render in a code block
+```
+
+**The following markdown should not render:**
+```
+_Italics_
+*Italics*
+**Bold**
+***Bold-italics***
+**Bold-italics_**
+~~Strikethrough~~
+:) :-) ;) ;-) :o :O :-o :-O
+:bamboo: :gift_heart: :dolls: :school_satchel: :mortar_board:
+# Heading 1
+## Heading 2
+### Heading 3
+#### Heading 4
+##### Heading 5
+###### Heading 6
+> Block Quote
+- List
+ - List Sub-item
+[Link](http://i.giphy.com/xNrM4cGJ8u3ao.gif)
+[![Github](https://assets-cdn.github.com/favicon.ico)](https://github.com/mattermost/platform)
+| Left-Aligned Text | Center Aligned Text | Right Aligned Text |
+| :------------ |:---------------:| -----:|
+| Left column 1 | this text | $100 |
+```
+
+**The following links should not auto-link or generate previews:**
+```
+GIF: http://i.giphy.com/xNrM4cGJ8u3ao.gif
+Website: https://en.wikipedia.org/wiki/Dolphin
+```
+
+**The following should appear as a carriage return separating two lines of text:**
+```
+Line #1 followed by a blank line
+
+Line #2 following a blank line
+```
+
+### In-line Code
+
+The word `monospace` should render as in-line code.
+
+The following markdown in-line code should not render:
+`_Italics_`, `*Italics*`, `**Bold**`, `***Bold-italics***`, `**Bold-italics_**`, `~~Strikethrough~~`, `:)` , `:-)` , `;)` , `:-O` , `:bamboo:` , `:gift_heart:` , `:dolls:` , `# Heading 1`, `## Heading 2`, `### Heading 3`, `#### Heading 4`, `##### Heading 5`, `###### Heading 6`
+
+This GIF link should not preview: `http://i.giphy.com/xNrM4cGJ8u3ao.gif`
+This link should not auto-link: `https://en.wikipedia.org/wiki/Dolphin`
+
+This sentence with `
+in-line code
+` should appear on one line.
+
+### In-line Images
+
+Mattermost/platform build status: [![Build Status](https://travis-ci.org/mattermost/platform.svg?branch=master)](https://travis-ci.org/mattermost/platform)
+
+GitHub favicon: ![Github](https://assets-cdn.github.com/favicon.ico)
+
+GIF Image:
+![gif](http://i.giphy.com/xNrM4cGJ8u3ao.gif)
+
+4K Wallpaper Image (11Mb):
+![4K Image](http://4kwallpaper.xyz/wallpaper/Large-Galaxy-Lightyears-Space-4K-wallpaper.png)
+
+Panorama Image:
+![Pano](http://amardeepphotography.com/wp-content/uploads/2012/11/Untitled_Panorama6small.jpg)
+
+Tall Portrait Image:
+![Portrait](http://www.maniacworld.com/now-this-is-a-tall-building.jpg)
+
+
+### Lines
+
+Three lines should render with text between them:
+
+Text above line
+
+***
+
+Text between lines
+
+---
+
+Text between lines
+___
+
+Text below line
+
+### Block Quotes
+
+>This text should render in a block quote.
+
+**The following markdown should render within the block quote:**
+> #### Heading 4
+> _Italics_, *Italics*, **Bold**, ***Bold-italics***, **Bold-italics_**, ~~Strikethrough~~
+> :) :-) ;) :-O :bamboo: :gift_heart: :dolls:
+
+**The following text should render in two block quotes separated by one line of text:**
+> Block quote 1
+
+Text between block quotes
+
+> Block quote 2
+
+### Headings
+
+# Heading 1 font size
+## Heading 2 font size
+### Heading 3 font size
+#### Heading 4 font size
+##### Heading 5 font size
+###### Heading 6 font size
diff --git a/doc/developer/tests/test-markdown-lists.md b/doc/developer/tests/test-markdown-lists.md
new file mode 100644
index 000000000..905f5b0d5
--- /dev/null
+++ b/doc/developer/tests/test-markdown-lists.md
@@ -0,0 +1,177 @@
+# Markdown List Testing
+Verify that all list types render as expected.
+
+### Single-item Ordered List
+
+**Expected:**
+```
+7. Single Item
+```
+
+**Actual:**
+7. Single Item
+
+### Multi-item Ordered List
+
+**Expected:**
+```
+1. One
+2. Two
+3. Three
+```
+
+**Actual:**
+3. One
+2. Two
+1. Three
+
+### Nested Ordered List
+
+**Expected:**
+```
+1. Alpha
+ 1. Bravo
+2. Charlie
+3. Delta
+ 1. Echo
+ 1. Foxtrot
+```
+
+**Actual:**
+1. Alpha
+ 1. Bravo
+1. Charlie
+1. Delta
+ 1. Echo
+ 1. Foxtrot
+
+### Single-item Unordered List
+
+**Expected:**
+```
+• Single Item
+```
+
+**Actual:**
+* Single Item
+
+### Multi-item Unordered List
+
+**Expected:**
+```
+• One
+• Two
+• Three
+```
+
+**Actual:**
+* One
+- Two
++ Three
+
+### Nested Unordered List
+
+**Expected:**
+```
+• Alpha
+ • Bravo
+• Charlie
+• Delta
+ • Echo
+ • Foxtrot
+```
+
+**Actual:**
++ Alpha
+ * Bravo
+- Charlie
+* Delta
+ + Echo
+ - Foxtrot
+
+### Mixed List Starting Ordered
+
+**Expected:**
+```
+1. One
+2. Two
+3. Three
+```
+
+**Actual:**
+1. One
++ Two
+- Three
+
+### Mixed List Starting Unordered
+
+**Expected:**
+```
+• Monday
+• Tuesday
+• Wednesday
+```
+
+**Actual:**
++ Monday
+1. Tuesday
+* Wednesday
+
+### Nested Mixed List
+
+**Expected:**
+```
+• Alpha
+ 1. Bravo
+ • Charlie
+ • Delta
+• Echo
+• Foxtrot
+ • Golf
+ 1. Hotel
+ • India
+ 1. Juliet
+ 2. Kilo
+ • Lima
+• Mike
+ 1. November
+ 1. Oscar
+ 1. Papa
+```
+
+**Actual:**
+- Alpha
+ 1. Bravo
+ * Charlie
+ + Delta
+- Echo
+* Foxtrot
+ + Golf
+ 1. Hotel
+ - India
+ 2. Juliet
+ 3. Kilo
+ * Lima
+1. Mike
+ 1. November
+ 4. Oscar
+ 5. Papa
+
+### Ordered Lists Separated by Carriage Returns
+
+**Expected:**
+```
+1. One
+ • Two
+
+1. One
+2. Two
+```
+
+**Actual:**
+1. One
+ - Two
+
+
+1. One
+2. Two
diff --git a/doc/developer/tests/test-markdown.md b/doc/developer/tests/test-markdown.md
deleted file mode 100644
index 2532e504f..000000000
--- a/doc/developer/tests/test-markdown.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# 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**
-```
-
-Here is how it should render:
-
-# 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/help/Notifications.md b/doc/help/Notifications.md
new file mode 100644
index 000000000..31f06e713
--- /dev/null
+++ b/doc/help/Notifications.md
@@ -0,0 +1,31 @@
+# Notifications
+___
+
+Notifications in Mattermost alert you to unread mentions and messages.
+
+## Types of Notifications
+
+#### Email Notifications
+These are emails sent to your primary email address for any mentions you receive while offline or inactive.
+- Users are offline when they do not have Mattermost open.
+- Users are inactive when they have Mattermost open but haven’t performed an action for a set amount of time.
+- You can change the email to which these notifications are sent in **Account Settings** > **General** > **Email**.
+- You can turn email notifications on or off in **Account Settings** > **Notifications** > **Email Notifications**.
+
+#### Desktop Notifications
+These are browser notifications that are by default sent for all activity.
+- You can adjust this setting in **Account Settings** > **Notifications** > **Send Desktop Notifications**.
+- Channel specific notifications are automatically set to the global default but can be modified in **Channel Settings** > **Notification Preferences** > **Send Desktop Notifications**.
+- Desktop notifications are available on Firefox, Safari, and Chrome.
+
+
+#### Sound Notifications
+These accompany each desktop notification by default.
+- You can change this setting in **Account Settings** > **Notifications** > **Desktop Notification Sounds**.
+
+
+#### Browser Tab Notifications
+These appear in the Mattermost tab and inform you of any unread messages and alert you to the number of mentions you have.
+- Unread messages are denoted by an asterisk (*) next to the Mattermost icon.
+- Mentions and replies are denoted by a red Mattermost icon.
+- The total number of unread mentions and replies are shown in brackets next to the Mattermost icon. For example, if you have 3 unread mentions, you’ll see a (3) in the browser tab.
diff --git a/doc/help/README.md b/doc/help/README.md
index 3b3c1709b..23c8b192d 100644
--- a/doc/help/README.md
+++ b/doc/help/README.md
@@ -5,13 +5,18 @@
- 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)
+ - [Team Settings ](Team-Settings.md)
+ - [General Settings](Team-Settings.md#general)
+ - [Slack Import](Team-Settings.md#import-from-slack-beta)
- [Manage Members](Manage-Members.md)
- - Messaging
- - [Mattermost Markdown Formatting](help/Markdown.md)
+ - [Account Settings](Account-Settings.md)
+ - [Messaging](Messaging.md)
+ - [Mattermost Markdown Formatting](Markdown.md)
- [Search](Search.md)
+ - [Channels](Channels.md)
+ - [Channel Types](Channels.md#channel-types)
+ - [Managing Channels](Channels.md#managing-channels)
+ - [Channel Settings](Channels.md#channel-settings)
- System Console
- Team
diff --git a/doc/install/Configuration-Settings.md b/doc/install/Configuration-Settings.md
index a92893753..44730d40f 100644
--- a/doc/install/Configuration-Settings.md
+++ b/doc/install/Configuration-Settings.md
@@ -29,7 +29,12 @@ Set this key to enable embedding of YouTube video previews based on hyperlinks a
#### Webhooks
```"EnableIncomingWebhooks": true```
-Developers building integrations can create webhook URLs for channels and private groups. Please see http://mattermost.org/webhooks to learn about creating webhooks, view samples, and to let the community know about integrations you have built. "true": Incoming webhooks will be allowed. To manage incoming webhooks, go to Account Settings -> Integrations. The webhook URLs created in Account Settings can be used by external applications to create posts in any channels or private groups that you have access to; “false”: The Integrations tab of Account Settings is hidden and incoming webhooks are disabled.
+Developers building integrations can create webhook URLs for channels and private groups. Please see http://mattermost.org/webhooks to learn about creating webhooks, view samples, and to let the community know about integrations you have built. "true": Incoming webhooks will be allowed. To manage incoming webhooks, go to **Account Settings -> Integrations**. The webhook URLs created in Account Settings can be used by external applications to create posts in any channels or private groups that you have access to; “false”: The Integrations > Incoming Webhooks section of Account Settings is hidden and all incoming webhooks are disabled.
+
+Security note: By enabling this feature, users may be able to perform [phishing attacks](https://en.wikipedia.org/wiki/Phishing) by attempting to impersonate other users. To combat these attacks, a BOT tag appears next to all posts from a webhook. Enable at your own risk.
+
+```"EnableOutgoingWebhooks": true```
+Developers building integrations can create webhook tokens for public channels. Trigger words are used to fire new message events to external integrations. For security reasons, outgoing webhooks are only available in public channels. Please see our [documentation page](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Outgoing-Webhooks.md) to learn about creating webhooks and view samples. "true": Outgoing webhooks will be allowed. To manage outgoing webhooks, go to **Account Settings -> Integrations**; “false”: The Integrations > Outgoing Webhooks section of Account Settings is hidden and all outgoing webhooks are disabled.
Security note: By enabling this feature, users may be able to perform [phishing attacks](https://en.wikipedia.org/wiki/Phishing) by attempting to impersonate other users. To combat these attacks, a BOT tag appears next to all posts from a webhook. Enable at your own risk.
@@ -58,6 +63,12 @@ Maximum number of users per team, including both active and inactive users.
```"RestrictCreationToDomains": ""```
Teams can only be created by a verified email from this list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.org").
+```"RestrictTeamNames": true```
+"true": Newly created team names cannot contain the following restricted words: www, web, admin, support, notify, test, demo, mail, team, channel, internal, localhost, dockerhost, stag, post, cluster, api, oauth; “false”: Newly created team names are not restricted.
+
+```"EnableTeamListing": false```
+"true": Teams that are configured to appear in the team directory will appear on the system main page. Teams can configure this setting from **Team Settings -> Include this team in the Team Directory**; "true": Team directory on the system main page is disabled.
+
### SQL Settings
diff --git a/doc/process/overview.md b/doc/process/overview.md
index af632e393..b34908782 100644
--- a/doc/process/overview.md
+++ b/doc/process/overview.md
@@ -64,7 +64,7 @@ Mattermost priorities are managed in Jira tickets, which are created by the core
On non-holiday weekdays new tickets are reviewed in a process called "triage", and assigned a Fix Version of "backlog", indicating the ticket has enough specificity that it can be assigned to a developer to be completed.
-By default, all tickets are created as internal-only, and the triage process reviews them for sufficient specifity and abscense of sensitive information before switching their visibility to public as part of the triage process.
+By default, all tickets are created as public unless they contain sensitive information. The triage process reviews them for sufficient specifity. If the ticket is unclear, triage may reassign the ticket back to the original reporter to add more details.
View [current issues scheduled for the next triage meeting](https://mattermost.atlassian.net/browse/PLT-1203?filter=10105).
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index c23a72cd1..4c5502ddd 100644
--- a/docker/dev/config_docker.json
+++ b/docker/dev/config_docker.json
@@ -68,9 +68,8 @@
"ConnectionSecurity": "",
"InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
- "ApplePushServer": "",
- "ApplePushCertPublic": "",
- "ApplePushCertPrivate": ""
+ "SendPushNotifications": true,
+ "PushNotificationServer": "https://push.mattermost.com"
},
"RateLimitSettings": {
"EnableRateLimiter": true,
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index c23a72cd1..4c5502ddd 100644
--- a/docker/local/config_docker.json
+++ b/docker/local/config_docker.json
@@ -68,9 +68,8 @@
"ConnectionSecurity": "",
"InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
- "ApplePushServer": "",
- "ApplePushCertPublic": "",
- "ApplePushCertPrivate": ""
+ "SendPushNotifications": true,
+ "PushNotificationServer": "https://push.mattermost.com"
},
"RateLimitSettings": {
"EnableRateLimiter": true,
diff --git a/mattermost.go b/mattermost.go
index 2d5727400..eaab1de88 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -68,6 +68,7 @@ func main() {
manualtesting.InitManualTesting()
}
+ setDiagnosticId()
runSecurityAndDiagnosticsJobAndForget()
// wait for kill signal before attempting to gracefully shutdown
@@ -80,6 +81,21 @@ func main() {
}
}
+func setDiagnosticId() {
+ if result := <-api.Srv.Store.System().Get(); result.Err == nil {
+ props := result.Data.(model.StringMap)
+
+ id := props[model.SYSTEM_DIAGNOSTIC_ID]
+ if len(id) == 0 {
+ id = model.NewId()
+ systemId := &model.System{Name: model.SYSTEM_DIAGNOSTIC_ID, Value: id}
+ <-api.Srv.Store.System().Save(systemId)
+ }
+
+ utils.CfgDiagnosticId = id
+ }
+}
+
func runSecurityAndDiagnosticsJobAndForget() {
go func() {
for {
@@ -92,15 +108,9 @@ func runSecurityAndDiagnosticsJobAndForget() {
if (currentTime - lastSecurityTime) > 1000*60*60*24*1 {
l4g.Debug("Checking for security update from Mattermost")
- id := props[model.SYSTEM_DIAGNOSTIC_ID]
- if len(id) == 0 {
- id = model.NewId()
- systemId := &model.System{Name: model.SYSTEM_DIAGNOSTIC_ID, Value: id}
- <-api.Srv.Store.System().Save(systemId)
- }
-
v := url.Values{}
- v.Set(utils.PROP_DIAGNOSTIC_ID, id)
+
+ v.Set(utils.PROP_DIAGNOSTIC_ID, utils.CfgDiagnosticId)
v.Set(utils.PROP_DIAGNOSTIC_BUILD, model.CurrentVersion+"."+model.BuildNumber)
v.Set(utils.PROP_DIAGNOSTIC_DATABASE, utils.Cfg.SqlSettings.DriverName)
v.Set(utils.PROP_DIAGNOSTIC_OS, runtime.GOOS)
diff --git a/model/config.go b/model/config.go
index 50a8dc133..195cefae8 100644
--- a/model/config.go
+++ b/model/config.go
@@ -96,11 +96,8 @@ type EmailSettings struct {
ConnectionSecurity string
InviteSalt string
PasswordResetSalt string
-
- // For Future Use
- ApplePushServer string
- ApplePushCertPublic string
- ApplePushCertPrivate string
+ SendPushNotifications *bool
+ PushNotificationServer *string
}
type RateLimitSettings struct {
@@ -181,6 +178,17 @@ func (o *Config) SetDefaults() {
o.TeamSettings.EnableTeamListing = new(bool)
*o.TeamSettings.EnableTeamListing = false
}
+
+ if o.EmailSettings.SendPushNotifications == nil {
+ o.EmailSettings.SendPushNotifications = new(bool)
+ *o.EmailSettings.SendPushNotifications = true
+ }
+
+ if o.EmailSettings.PushNotificationServer == nil {
+ o.EmailSettings.PushNotificationServer = new(string)
+ *o.EmailSettings.PushNotificationServer = "https://push.mattermost.com"
+ }
+
}
func (o *Config) IsValid() *AppError {
diff --git a/model/push_notification.go b/model/push_notification.go
new file mode 100644
index 000000000..76f5bd125
--- /dev/null
+++ b/model/push_notification.go
@@ -0,0 +1,45 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+const (
+ PUSH_NOTIFY_APPLE = "apple"
+ PUSH_NOTIFY_ANDROID = "android"
+)
+
+type PushNotification struct {
+ Platform string `json:"platform"`
+ ServerId string `json:"server_id"`
+ DeviceId string `json:"device_id"`
+ Category string `json:"category"`
+ Sound string `json:"sound"`
+ Message string `json:"message"`
+ Badge int `json:"badge"`
+ ContentAvailable int `json:"cont_ava"`
+}
+
+func (me *PushNotification) ToJson() string {
+ b, err := json.Marshal(me)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func PushNotificationFromJson(data io.Reader) *PushNotification {
+ decoder := json.NewDecoder(data)
+ var me PushNotification
+ err := decoder.Decode(&me)
+ if err == nil {
+ return &me
+ } else {
+ return nil
+ }
+}
diff --git a/model/push_notification_test.go b/model/push_notification_test.go
new file mode 100644
index 000000000..94329f389
--- /dev/null
+++ b/model/push_notification_test.go
@@ -0,0 +1,19 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestPushNotification(t *testing.T) {
+ msg := PushNotification{Platform: "test"}
+ json := msg.ToJson()
+ result := PushNotificationFromJson(strings.NewReader(json))
+
+ if msg.Platform != result.Platform {
+ t.Fatal("Ids do not match")
+ }
+}
diff --git a/utils/apns.go b/utils/apns.go
deleted file mode 100644
index 06e8ce6ef..000000000
--- a/utils/apns.go
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package utils
-
-import (
- l4g "code.google.com/p/log4go"
- "fmt"
- "github.com/anachronistic/apns"
- "github.com/mattermost/platform/model"
-)
-
-func SendAppleNotifyAndForget(deviceId string, message string, badge int) {
- go func() {
- if err := SendAppleNotify(deviceId, message, badge); err != nil {
- l4g.Error(fmt.Sprintf("%v %v", err.Message, err.DetailedError))
- }
- }()
-}
-
-func SendAppleNotify(deviceId string, message string, badge int) *model.AppError {
- payload := apns.NewPayload()
- payload.Alert = message
- payload.Badge = 1
-
- pn := apns.NewPushNotification()
- pn.DeviceToken = deviceId
- pn.AddPayload(payload)
- client := apns.BareClient(Cfg.EmailSettings.ApplePushServer, Cfg.EmailSettings.ApplePushCertPublic, Cfg.EmailSettings.ApplePushCertPrivate)
- resp := client.Send(pn)
-
- if resp.Error != nil {
- return model.NewAppError("", "Could not send apple push notification", fmt.Sprintf("id=%v err=%v", deviceId, resp.Error))
- } else {
- return nil
- }
-}
diff --git a/utils/config.go b/utils/config.go
index 2fd799cd1..0b292a2ca 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -24,6 +24,7 @@ const (
)
var Cfg *model.Config = &model.Config{}
+var CfgDiagnosticId = ""
var CfgLastModified int64 = 0
var CfgFileName string = ""
var ClientCfg map[string]string = map[string]string{}
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index d0565a0e0..238ace3da 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -18,6 +18,7 @@ export default class EmailSettings extends React.Component {
this.state = {
sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications,
+ sendPushNotifications: this.props.config.EmailSettings.SendPushNotifications,
saveNeeded: false,
serverError: null,
emailSuccess: null,
@@ -36,6 +37,14 @@ export default class EmailSettings extends React.Component {
s.sendEmailNotifications = false;
}
+ if (action === 'sendPushNotifications_true') {
+ s.sendPushNotifications = true;
+ }
+
+ if (action === 'sendPushNotifications_false') {
+ s.sendPushNotifications = false;
+ }
+
this.setState(s);
}
@@ -43,11 +52,12 @@ export default class EmailSettings extends React.Component {
var config = this.props.config;
config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked;
config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
+ config.EmailSettings.SendPushlNotifications = ReactDOM.findDOMNode(this.refs.sendPushNotifications).checked;
config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked;
- config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
config.EmailSettings.FeedbackName = ReactDOM.findDOMNode(this.refs.feedbackName).value.trim();
config.EmailSettings.FeedbackEmail = ReactDOM.findDOMNode(this.refs.feedbackEmail).value.trim();
config.EmailSettings.SMTPServer = ReactDOM.findDOMNode(this.refs.SMTPServer).value.trim();
+ config.EmailSettings.PushNotificationServer = ReactDOM.findDOMNode(this.refs.PushNotificationServer).value.trim();
config.EmailSettings.SMTPPort = ReactDOM.findDOMNode(this.refs.SMTPPort).value.trim();
config.EmailSettings.SMTPUsername = ReactDOM.findDOMNode(this.refs.SMTPUsername).value.trim();
config.EmailSettings.SMTPPassword = ReactDOM.findDOMNode(this.refs.SMTPPassword).value.trim();
@@ -526,6 +536,61 @@ export default class EmailSettings extends React.Component {
</div>
<div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='sendPushNotifications'
+ >
+ {'Send Push Notifications: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendPushNotifications'
+ value='true'
+ ref='sendPushNotifications'
+ defaultChecked={this.props.config.EmailSettings.SendPushNotifications}
+ onChange={this.handleChange.bind(this, 'sendPushNotifications_true')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendPushNotifications'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.SendPushNotifications}
+ onChange={this.handleChange.bind(this, 'sendPushNotifications_false')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Typically set to true in production. When true, Mattermost attempts to send iOS and Android push notifications through the push notification server.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PushNotificationServer'
+ >
+ {'Push Notification Server:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PushNotificationServer'
+ ref='PushNotificationServer'
+ placeholder='E.g.: "https://push.mattermost.com"'
+ defaultValue={this.props.config.EmailSettings.PushNotificationServer}
+ onChange={this.handleChange}
+ disabled={!this.state.sendPushNotifications}
+ />
+ <p className='help-text'>{'Location of the push notification server.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
diff --git a/web/react/components/channel_notifications_modal.jsx b/web/react/components/channel_notifications_modal.jsx
index 79b769c8a..887589468 100644
--- a/web/react/components/channel_notifications_modal.jsx
+++ b/web/react/components/channel_notifications_modal.jsx
@@ -32,11 +32,13 @@ export default class ChannelNotificationsModal extends React.Component {
activeSection: ''
};
}
- componentDidMount() {
- ChannelStore.addChangeListener(this.onListenerChange);
- }
- componentWillUnmount() {
- ChannelStore.removeChangeListener(this.onListenerChange);
+ componentWillReceiveProps(nextProps) {
+ if (!this.props.show && nextProps.show) {
+ this.onListenerChange();
+ ChannelStore.addChangeListener(this.onListenerChange);
+ } else {
+ ChannelStore.removeChangeListener(this.onListenerChange);
+ }
}
onListenerChange() {
const curChannelId = ChannelStore.getCurrentId();
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index df5d6b8e1..fd20834f4 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -75,7 +75,7 @@ export default class GetLinkModal extends React.Component {
onHide={this.onHide}
>
<Modal.Header closeButton={true}>
- {this.props.title}
+ <h4 className='modal-title'>{this.props.title}</h4>
</Modal.Header>
<Modal.Body>
{helpText}
diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx
index 390d25f2e..f5d5ab28b 100644
--- a/web/react/components/member_list_item.jsx
+++ b/web/react/components/member_list_item.jsx
@@ -110,7 +110,7 @@ export default class MemberListItem extends React.Component {
height='36'
width='36'
/>
- <div className='member-name'>{member.username}</div>
+ <div className='member-name'>{Utils.displayUsername(member.id)}</div>
<div className='member-description'>{member.email}</div>
</td>
<td className='td--action lg'>{invite}</td>
diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx
index 27fb6a4c1..316fad01a 100644
--- a/web/react/components/member_list_team_item.jsx
+++ b/web/react/components/member_list_team_item.jsx
@@ -174,7 +174,7 @@ export default class MemberListTeamItem extends React.Component {
height='36'
width='36'
/>
- <span className='member-name'>{Utils.getDisplayName(user)}</span>
+ <span className='member-name'>{Utils.displayUsername(user.id)}</span>
<span className='member-email'>{email}</span>
<div className='dropdown member-drop'>
<a
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index b5000141a..f4cb542e4 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -5,6 +5,7 @@ import UserStore from '../stores/user_store.jsx';
var Popover = ReactBootstrap.Popover;
var Overlay = ReactBootstrap.Overlay;
import * as Utils from '../utils/utils.jsx';
+import Constants from '../utils/constants.jsx';
import ChannelStore from '../stores/channel_store.jsx';
@@ -68,7 +69,7 @@ export default class PopoverListMembers extends React.Component {
}
render() {
- let popoverHtml = [];
+ const popoverHtml = [];
const members = this.props.members;
const teamMembers = UserStore.getProfilesUsernameMap();
const currentUserId = UserStore.getCurrentId();
@@ -76,35 +77,13 @@ export default class PopoverListMembers extends React.Component {
if (members && teamMembers) {
members.sort((a, b) => {
- return a.username.localeCompare(b.username);
+ const aName = Utils.displayUsername(a.id);
+ const bName = Utils.displayUsername(b.id);
+
+ return aName.localeCompare(bName);
});
members.forEach((m, i) => {
- const details = [];
-
- const fullName = Utils.getFullName(m);
- if (fullName) {
- details.push(
- <span
- key={`${m.id}__full-name`}
- className='full-name'
- >
- {fullName}
- </span>
- );
- }
-
- if (m.nickname) {
- const separator = fullName ? ' - ' : '';
- details.push(
- <span
- key={`${m.nickname}__nickname`}
- >
- {separator + m.nickname}
- </span>
- );
- }
-
let button = '';
if (currentUserId !== m.id && ch.type !== 'D') {
button = (
@@ -118,7 +97,12 @@ export default class PopoverListMembers extends React.Component {
);
}
- if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
+ let name = '';
+ if (teamMembers[m.username]) {
+ name = Utils.displayUsername(teamMembers[m.username].id);
+ }
+
+ if (name && teamMembers[m.username].delete_at <= 0) {
popoverHtml.push(
<div
className='text-nowrap'
@@ -135,7 +119,7 @@ export default class PopoverListMembers extends React.Component {
<div
className='more-name'
>
- {m.username}
+ {name}
</div>
</div>
<div
@@ -157,8 +141,8 @@ export default class PopoverListMembers extends React.Component {
count = members.length;
}
- if (count > 20) {
- countText = '20+';
+ if (count > Constants.MAX_CHANNEL_POPOVER_COUNT) {
+ countText = Constants.MAX_CHANNEL_POPOVER_COUNT + '+';
} else if (count > 0) {
countText = count.toString();
}
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 278261e22..66d8c507a 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -87,6 +87,10 @@ export default class Post extends React.Component {
return true;
}
+ if (nextProps.displayNameType !== this.props.displayNameType) {
+ return true;
+ }
+
if (this.getCommentCount(nextProps) !== this.getCommentCount(this.props)) {
return true;
}
@@ -224,5 +228,6 @@ Post.propTypes = {
sameRoot: React.PropTypes.bool,
hideProfilePic: React.PropTypes.bool,
isLastComment: React.PropTypes.bool,
- shouldHighlight: React.PropTypes.bool
+ shouldHighlight: React.PropTypes.bool,
+ displayNameType: React.PropTypes.string
};
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index 5e374b877..242b26b91 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -2,15 +2,18 @@
// See License.txt for license information.
import UserStore from '../stores/user_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as Utils from '../utils/utils.jsx';
import Post from './post.jsx';
import Constants from '../utils/constants.jsx';
+const Preferences = Constants.Preferences;
export default class PostsView extends React.Component {
constructor(props) {
super(props);
+ this.updateState = this.updateState.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.isAtBottom = this.isAtBottom.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
@@ -22,6 +25,8 @@ export default class PostsView extends React.Component {
this.jumpToPostNode = null;
this.wasAtBottom = true;
this.scrollHeight = 0;
+
+ this.state = {displayNameType: PreferenceStore.getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value};
}
static get SCROLL_TYPE_FREE() {
return 1;
@@ -38,6 +43,9 @@ export default class PostsView extends React.Component {
static get SCROLL_TYPE_POST() {
return 5;
}
+ updateState() {
+ this.setState({displayNameType: PreferenceStore.getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value});
+ }
isAtBottom() {
return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight);
}
@@ -94,22 +102,53 @@ export default class PostsView extends React.Component {
const prevPostIsComment = Utils.isComment(prevPost);
const postFromWebhook = Boolean(post.props && post.props.from_webhook);
const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook);
+ let prevWebhookName = '';
+ if (prevPost.props && prevPost.props.override_username) {
+ prevWebhookName = prevPost.props.override_username;
+ }
+ let curWebhookName = '';
+ if (post.props && post.props.override_username) {
+ curWebhookName = post.props.override_username;
+ }
+
+ // consider posts from the same user if:
+ // the previous post was made by the same user as the current post,
+ // the previous post was made within 5 minutes of the current post,
+ // the previous post and current post are both from webhooks or both not,
+ // the previous post and current post have the same webhook usernames
+ if (prevPost.user_id === post.user_id &&
+ post.create_at - prevPost.create_at <= 1000 * 60 * 5 &&
+ postFromWebhook === prevPostFromWebhook &&
+ prevWebhookName === curWebhookName) {
+ sameUser = true;
+ }
- 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);
+ // consider posts from the same root if:
+ // the current post is a comment,
+ // the current post has the same root as the previous post
+ if (postIsComment && (prevPost.id === post.root_id || prevPost.root_id === post.root_id)) {
+ sameRoot = true;
+ }
+
+ // consider posts from the same root if:
+ // the current post is not a comment,
+ // the previous post is not a comment,
+ // the previous post is from the same user
+ if (!postIsComment && !prevPostIsComment && sameUser) {
+ sameRoot = true;
+ }
// hide the profile pic if:
// the previous post was made by the same user as the current post,
// the previous post is not a comment,
// the current post is not a comment,
- // the current post is not from a webhook
- // and the previous post is not from a webhook
- if ((prevPost.user_id === post.user_id) &&
+ // the previous post and current post are both from webhooks or both not,
+ // the previous post and current post have the same webhook usernames
+ if (prevPost.user_id === post.user_id &&
!prevPostIsComment &&
!postIsComment &&
- !postFromWebhook &&
- !prevPostFromWebhook) {
+ postFromWebhook === prevPostFromWebhook &&
+ prevWebhookName === curWebhookName) {
hideProfilePic = true;
}
}
@@ -135,6 +174,7 @@ export default class PostsView extends React.Component {
isLastComment={isLastComment}
shouldHighlight={shouldHighlight}
onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func
+ displayNameType={this.state.displayNameType}
/>
);
@@ -240,16 +280,20 @@ export default class PostsView extends React.Component {
this.updateScrolling();
}
window.addEventListener('resize', this.handleResize);
+ $(this.refs.postlist).perfectScrollbar();
+ PreferenceStore.addChangeListener(this.updateState);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
+ PreferenceStore.removeChangeListener(this.updateState);
}
componentDidUpdate() {
if (this.props.postList != null) {
this.updateScrolling();
}
+ $(this.refs.postlist).perfectScrollbar('update');
}
- shouldComponentUpdate(nextProps) {
+ shouldComponentUpdate(nextProps, nextState) {
if (this.props.isActive !== nextProps.isActive) {
return true;
}
@@ -268,6 +312,9 @@ export default class PostsView extends React.Component {
if (!Utils.areObjectsEqual(this.props.postList, nextProps.postList)) {
return true;
}
+ if (nextState.displayNameType !== this.state.displayNameType) {
+ return true;
+ }
return false;
}
@@ -326,7 +373,7 @@ export default class PostsView extends React.Component {
return (
<div
ref='postlist'
- className={'post-list-holder-by-time ' + activeClass}
+ className={'ps-container post-list-holder-by-time ' + activeClass}
onScroll={this.handleScroll}
>
<div className='post-list__table'>
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
index 367d3687e..6d6694fec 100644
--- a/web/react/components/posts_view_container.jsx
+++ b/web/react/components/posts_view_container.jsx
@@ -99,10 +99,12 @@ export default class PostsViewContainer extends React.Component {
if (newIndex === -1) {
newIndex = channels.length;
channels.push(channelId);
- postLists[newIndex] = this.getChannelPosts(channelId);
atTop[newIndex] = PostStore.getVisibilityAtTop(channelId);
}
+ // make sure we have the latest posts from the store
+ postLists[newIndex] = this.getChannelPosts(channelId);
+
this.setState({
currentChannelIndex: newIndex,
currentLastViewed: lastViewed,
diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx
index 100600c4b..f49b33f73 100644
--- a/web/react/components/register_app_modal.jsx
+++ b/web/react/components/register_app_modal.jsx
@@ -2,21 +2,57 @@
// See License.txt for license information.
import * as Client from '../utils/client.jsx';
+import ModalStore from '../stores/modal_store.jsx';
+
+const Modal = ReactBootstrap.Modal;
+
+import Constants from '../utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
export default class RegisterAppModal extends React.Component {
constructor() {
super();
- this.register = this.register.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
this.onHide = this.onHide.bind(this);
this.save = this.save.bind(this);
+ this.updateShow = this.updateShow.bind(this);
- this.state = {clientId: '', clientSecret: '', saved: false};
+ this.state = {
+ clientId: '',
+ clientSecret: '',
+ saved: false,
+ show: false
+ };
}
componentDidMount() {
- $(ReactDOM.findDOMNode(this)).on('hide.bs.modal', this.onHide);
+ ModalStore.addModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow);
+ }
+ componentWillUnmount() {
+ ModalStore.removeModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow);
+ }
+ updateShow(show) {
+ if (!show) {
+ if (this.state.clientId !== '' && !this.state.saved) {
+ return;
+ }
+
+ this.setState({
+ clientId: '',
+ clientSecret: '',
+ saved: false,
+ homepageError: null,
+ callbackError: null,
+ serverError: null,
+ nameError: null
+ });
+ }
+
+ this.setState({show});
}
- register() {
+ handleSubmit(e) {
+ e.preventDefault();
+
var state = this.state;
state.serverError = null;
@@ -94,6 +130,7 @@ export default class RegisterAppModal extends React.Component {
}
var body = '';
+ var footer = '';
if (this.state.clientId === '') {
body = (
<div className='settings-modal'>
@@ -148,24 +185,29 @@ export default class RegisterAppModal extends React.Component {
</div>
</div>
{serverError}
- <hr />
- <a
- className='btn btn-sm theme pull-right'
- href='#'
- data-dismiss='modal'
- aria-label='Close'
- >
- {'Cancel'}
- </a>
- <a
- className='btn btn-sm btn-primary pull-right'
- onClick={this.register}
- >
- {'Register'}
- </a>
</div>
</div>
);
+
+ footer = (
+ <div>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={() => this.updateShow(false)}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ onClick={this.handleSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='3'
+ >
+ {'Register'}
+ </button>
+ </div>
+ );
} else {
var btnClass = ' disabled';
if (this.state.saved) {
@@ -173,17 +215,35 @@ export default class RegisterAppModal extends React.Component {
}
body = (
- <div className='form-group user-settings'>
- <h3>{'Your Application Credentials'}</h3>
- <br/>
- <br/>
- <label className='col-sm-12 control-label'>{'Client ID: '}{this.state.clientId}</label>
- <label className='col-sm-12 control-label'>{'Client Secret: '}{this.state.clientSecret}</label>
+ <div className='form-horizontal user-settings'>
+ <h4 className='padding-bottom x3'>{'Your Application Credentials'}</h4>
<br/>
+ <div className='row'>
+ <label className='col-sm-4 control-label'>{'Client ID'}</label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ value={this.state.clientId}
+ readOnly='true'
+ />
+ </div>
+ </div>
<br/>
+ <div className='row padding-top x2'>
+ <label className='col-sm-4 control-label'>{'Client Secret'}</label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ value={this.state.clientSecret}
+ readOnly='true'
+ />
+ </div>
+ </div>
<br/>
<br/>
- <strong>{'Save these somewhere SAFE and SECURE. We can retrieve your Client Id if you lose it, but your Client Secret will be lost forever if you were to lose it.'}</strong>
+ <strong>{'Save these somewhere SAFE and SECURE. Treat your Client ID as your app\'s username and your Client Secret as the app\'s password.'}</strong>
<br/>
<br/>
<div className='checkbox'>
@@ -192,56 +252,50 @@ export default class RegisterAppModal extends React.Component {
ref='save'
type='checkbox'
checked={this.state.saved}
- onClick={this.save}
- >
- {'I have saved both my Client Id and Client Secret somewhere safe'}
- </input>
+ onChange={this.save}
+ />
+ {'I have saved both my Client Id and Client Secret somewhere safe'}
</label>
</div>
- <a
- className={'btn btn-sm btn-primary pull-right' + btnClass}
- href='#'
- data-dismiss='modal'
- aria-label='Close'
- >
- {'Close'}
- </a>
</div>
);
+
+ footer = (
+ <a
+ className={'btn btn-sm btn-primary pull-right' + btnClass}
+ href='#'
+ onClick={(e) => {
+ e.preventDefault();
+ this.updateShow(false);
+ }}
+ >
+ {'Close'}
+ </a>
+ );
}
return (
- <div
- className='modal fade'
- ref='modal'
- id='register_app'
- role='dialog'
- aria-hidden='true'
- >
- <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'>{'×'}</span>
- </button>
- <h4
- className='modal-title'
- ref='title'
- >
- {'Developer Applications'}
- </h4>
- </div>
- <div className='modal-body'>
- {body}
- </div>
- </div>
- </div>
- </div>
+ <span>
+ <Modal
+ show={this.state.show}
+ onHide={() => this.updateShow(false)}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Developer Applications'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ {body}
+ </Modal.Body>
+ <Modal.Footer>
+ {footer}
+ </Modal.Footer>
+ </form>
+ </Modal>
+ </span>
);
}
}
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 30422ff7d..5c8e73874 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -3,7 +3,6 @@
import NewChannelFlow from './new_channel_flow.jsx';
import MoreDirectChannels from './more_direct_channels.jsx';
-import SearchBox from './search_bar.jsx';
import SidebarHeader from './sidebar_header.jsx';
import UnreadChannelIndicator from './unread_channel_indicator.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
@@ -173,6 +172,10 @@ export default class Sidebar extends React.Component {
this.updateScrollbar();
window.addEventListener('resize', this.handleResize);
+
+ if ($(window).width() > 768) {
+ $('.nav-pills__container').perfectScrollbar();
+ }
}
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areObjectsEqual(nextState, this.state)) {
@@ -582,7 +585,6 @@ export default class Sidebar extends React.Component {
teamName={TeamStore.getCurrent().name}
teamType={TeamStore.getCurrent().type}
/>
- <SearchBox />
<UnreadChannelIndicator
show={this.state.showTopUnread}
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index 438c0bc82..ea104fedb 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -54,9 +54,11 @@ export default class UserProfile extends React.Component {
}
}
render() {
- var name = this.state.profile.username;
+ var name = Utils.displayUsername(this.state.profile.id);
if (this.props.overwriteName) {
name = this.props.overwriteName;
+ } else if (!name) {
+ name = '...';
}
if (this.props.disablePopover) {
@@ -107,7 +109,7 @@ export default class UserProfile extends React.Component {
rootClose={true}
overlay={
<Popover
- title={this.state.profile.username}
+ title={name}
id='user-profile-popover'
>
{dataContent}
diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx
index 2d02c255a..01e13be57 100644
--- a/web/react/components/user_settings/user_settings_developer.jsx
+++ b/web/react/components/user_settings/user_settings_developer.jsx
@@ -3,16 +3,19 @@
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
+import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
export default class DeveloperTab extends React.Component {
constructor(props) {
super(props);
+ this.register = this.register.bind(this);
+
this.state = {};
}
register() {
- $('#user_settings1').modal('hide');
- $('#register_app').modal('show');
+ this.props.closeModal();
+ EventHelpers.showRegisterAppModal();
}
render() {
var appSection;
@@ -21,7 +24,10 @@ export default class DeveloperTab extends React.Component {
var inputs = [];
inputs.push(
- <div className='form-group'>
+ <div
+ key='registerbtn'
+ className='form-group'
+ >
<div className='col-sm-7'>
<a
className='btn btn-sm btn-primary'
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index 91f4b3bdc..820f8fd8e 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -423,24 +423,29 @@ export default class ViewImageModal extends React.Component {
onClick={this.props.onModalDismissed}
>
<div
- className={'image-wrapper ' + bgClass}
- onMouseEnter={this.onMouseEnterImage}
- onMouseLeave={this.onMouseLeaveImage}
- onClick={(e) => e.stopPropagation()}
+ className={'image-wrapper'}
+ onClick={this.props.onModalDismissed}
>
<div
- className={closeButtonClass}
- onClick={this.props.onModalDismissed}
- />
- {content}
- <ViewImagePopoverBar
- show={this.state.showFooter}
- fileId={this.state.imgId}
- totalFiles={this.props.filenames.length}
- filename={name}
- fileURL={fileUrl}
- getPublicLink={this.getPublicLink}
- />
+ className={bgClass}
+ onMouseEnter={this.onMouseEnterImage}
+ onMouseLeave={this.onMouseLeaveImage}
+ onClick={(e) => e.stopPropagation()}
+ >
+ <div
+ className={closeButtonClass}
+ onClick={this.props.onModalDismissed}
+ />
+ {content}
+ <ViewImagePopoverBar
+ show={this.state.showFooter}
+ fileId={this.state.imgId}
+ totalFiles={this.props.filenames.length}
+ filename={name}
+ fileURL={fileUrl}
+ getPublicLink={this.getPublicLink}
+ />
+ </div>
</div>
{leftArrow}
{rightArrow}
diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx
index d7f255aaa..856eec2f1 100644
--- a/web/react/dispatcher/event_helpers.jsx
+++ b/web/react/dispatcher/event_helpers.jsx
@@ -104,3 +104,10 @@ export function showInviteMemberModal() {
value: true
});
}
+
+export function showRegisterAppModal() {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_REGISTER_APP_MODAL,
+ value: true
+ });
+}
diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx
index a26a97f53..9f33cf022 100644
--- a/web/react/stores/modal_store.jsx
+++ b/web/react/stores/modal_store.jsx
@@ -35,6 +35,7 @@ class ModalStoreClass extends EventEmitter {
case ActionTypes.TOGGLE_INVITE_MEMBER_MODAL:
case ActionTypes.TOGGLE_DELETE_POST_MODAL:
case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL:
+ case ActionTypes.TOGGLE_REGISTER_APP_MODAL:
this.emit(type, value, args);
break;
}
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index a8f0f9c63..2212edadb 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -211,7 +211,7 @@ class PostStoreClass extends EventEmitter {
postList.order = this.postsInfo[id].pendingPosts.order.concat(postList.order);
}
- // Add delteted posts
+ // Add deleted posts
if (this.postsInfo[id].hasOwnProperty('deletedPosts')) {
Object.assign(postList.posts, this.postsInfo[id].deletedPosts);
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 2009e07dd..99bd2453c 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -49,7 +49,8 @@ export default {
TOGGLE_IMPORT_THEME_MODAL: null,
TOGGLE_INVITE_MEMBER_MODAL: null,
TOGGLE_DELETE_POST_MODAL: null,
- TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null
+ TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
+ TOGGLE_REGISTER_APP_MODAL: null
}),
PayloadSources: keyMirror({
@@ -137,6 +138,7 @@ export default {
],
MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
MAX_DMS: 20,
+ MAX_CHANNEL_POPOVER_COUNT: 20,
DM_CHANNEL: 'D',
OPEN_CHANNEL: 'O',
PRIVATE_CHANNEL: 'P',
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index 9d9bdfb7a..f2721c81d 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -223,6 +223,16 @@ class MattermostMarkdownRenderer extends marked.Renderer {
return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
}
+ listitem(text) {
+ const taskListReg = /^\[([ |xX])\] /;
+ const isTaskList = taskListReg.exec(text);
+
+ if (isTaskList) {
+ return `<li>${'<input type="checkbox" disabled="disabled" ' + (isTaskList[1] === ' ' ? '' : 'checked="checked" ') + '/> '}${text.replace(taskListReg, '')}</li>`;
+ }
+ return `<li>${text}</li>`;
+ }
+
text(txt) {
return TextFormatting.doFormatText(txt, this.formattingOptions);
}
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index 3a912fd75..f0bd46f9d 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -188,7 +188,7 @@ function highlightCurrentMentions(text, tokens) {
const newAlias = `MM_SELFMENTION${index}`;
newTokens.set(newAlias, {
- value: `<span class='mention-highlight'>${alias}</span>` + token.extraText,
+ value: `<span class='mention-highlight'>${alias}</span>` + (token.extraText || ''),
originalText: token.originalText
});
output = output.replace(alias, newAlias);
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 80c377d7f..d6ed34e70 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -607,7 +607,7 @@ export function applyTheme(theme) {
changeCss('.popover.right>.arrow:after, .tip-overlay.tip-overlay--sidebar .arrow, .tip-overlay.tip-overlay--header .arrow', 'border-right-color:' + theme.centerChannelBg, 1);
changeCss('.popover.left>.arrow:after', 'border-left-color:' + theme.centerChannelBg, 1);
changeCss('.popover.top>.arrow:after, .tip-overlay.tip-overlay--chat .arrow', 'border-top-color:' + theme.centerChannelBg, 1);
- changeCss('.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1);
+ changeCss('@media(min-width: 960px){.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1);
changeCss('.attachment__content', 'background:' + theme.centerChannelBg, 1);
}
@@ -638,8 +638,7 @@ export function applyTheme(theme) {
changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2);
changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2);
changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1);
- changeCss('.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2);
- changeCss('@media(max-width: 960px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
+ changeCss('@media(min-width: 960px){.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2);
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('.attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
@@ -854,7 +853,7 @@ export function isMobile() {
export function isComment(post) {
if ('root_id' in post) {
- return post.root_id !== '';
+ return post.root_id !== '' && post.root_id != null;
}
return false;
}
@@ -981,13 +980,15 @@ export function displayUsername(userId) {
const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value;
let username = '';
- if (nameFormat === 'nickname_full_name') {
- username = user.nickname || getFullName(user);
- } else if (nameFormat === 'full_name') {
- username = getFullName(user);
- }
- if (!username.trim().length) {
- username = user.username;
+ if (user) {
+ if (nameFormat === 'nickname_full_name') {
+ username = user.nickname || getFullName(user);
+ } else if (nameFormat === 'full_name') {
+ username = getFullName(user);
+ }
+ if (!username.trim().length) {
+ username = user.username;
+ }
}
return username;
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index 6270c8608..4a56bc6c7 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -3,6 +3,7 @@
}
.modal-body {
padding: 20px 15px;
+ overflow: auto;
}
.modal {
width: 100%;
@@ -47,9 +48,6 @@
margin-left: auto;
margin-right: auto;
}
- .modal-body {
- overflow: auto;
- }
.modal-push-down {
margin-top: 5%;
}
@@ -195,21 +193,35 @@
width:100%;
height: 100%;
margin: 0 auto;
+ max-width: 100%;
+
+ .modal-body {
+ @include clearfix;
+ height: 100%;
+ display: table;
+ table-layout: fixed;
+ width: 100%;
+ max-height: 100%;
+ }
+
.image-wrapper {
position: relative;
max-width: 90%;
- min-height: 100px;
- min-width: 320px;
@include border-radius(3px);
- display: table;
- margin: 0 auto;
+ display: table-cell;
+ vertical-align: middle;
+ text-align: center;
+ width: 100%;
+
&:hover {
@include border-radius(3px 3px 0 0);
}
+
&.default {
width: 100%;
height: 80%;
}
+
.modal-close {
background: url("../images/close.png") no-repeat;
@include background-size(100% 100%);
@@ -225,24 +237,31 @@
transition: opacity 0.6s;
cursor: pointer;
z-index: 9999;
+
&.modal-close--show {
@include opacity(1);
}
+
}
- > a {
+
+ > div {
+ min-height: 100px;
+ min-width: 320px;
background: #FFF;
- display: table-cell;
- vertical-align: middle;
+ display: inline-block;
position: relative;
&:hover .file-playback-controls.stop {
@include opacity(1);
}
+
}
+
img {
max-width: 100%;
max-height: 100%;
}
+
.spinner.file__loading {
z-index: 2;
position: absolute;
@@ -259,7 +278,6 @@
height: 100%;
padding: 0;
border: none;
- display: table;
}
.image-body {
vertical-align: middle;
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index b7a305427..ed1632681 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -607,6 +607,11 @@ body.ios {
li ul, li ol {
padding: 0 0 0 20px
}
+
+ li input[type="checkbox"]:disabled {
+ vertical-align: sub;
+ cursor: default;
+ }
}
}
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 00fa7d817..a56c4bb17 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -484,7 +484,8 @@
padding: 0 10px 0 31px;
background: rgba(black, 0.2);
@include border-radius(3px);
- color: inherit;
+ color: #444;
+ background: #fff;
}
}
}
@@ -509,15 +510,15 @@
&.move--right {
@include translate3d(0, 0, 0);
}
+ > div {
+ padding-bottom: 0;
+ }
.nav-pills__unread-indicator-bottom {
bottom: 10px;
}
.badge {
top: 13px;
}
- > div {
- padding-bottom: 65px;
- }
.team__header {
display: none;
@include clearfix;
@@ -621,8 +622,10 @@
.modal {
.modal-image {
.image-wrapper {
- font-size: 12px;
- min-width: 250px;
+ > div {
+ font-size: 12px;
+ min-width: 250px;
+ }
.modal-close {
@include opacity(1);
}
diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss
index 27b55f214..b7f658114 100644
--- a/web/sass-files/sass/partials/_search.scss
+++ b/web/sass-files/sass/partials/_search.scss
@@ -38,6 +38,7 @@
font-size: 14px;
@include opacity(0.5);
display: none;
+ color: #777;
}
.search__form {
.search-bar__container & {