summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md128
-rw-r--r--Makefile10
-rw-r--r--api/channel.go21
-rw-r--r--api/channel_benchmark_test.go2
-rw-r--r--api/channel_test.go48
-rw-r--r--doc/developer/tests/test-search.md43
-rw-r--r--doc/install/Upgrade-Guide.md2
-rw-r--r--model/channel.go4
-rw-r--r--model/client.go4
-rw-r--r--store/sql_channel_store.go9
-rw-r--r--web/react/components/channel_invite_modal.jsx31
-rw-r--r--web/react/components/channel_members_modal.jsx49
-rw-r--r--web/react/components/file_attachment.jsx2
-rw-r--r--web/react/components/post_info.jsx2
-rw-r--r--web/react/components/posts_view.jsx7
-rw-r--r--web/react/utils/async_client.jsx3
-rw-r--r--web/react/utils/client.jsx11
-rw-r--r--web/sass-files/sass/partials/_post.scss27
-rw-r--r--web/sass-files/sass/partials/_post_right.scss1
19 files changed, 342 insertions, 62 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5094f06e..42b485742 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,133 @@
# Mattermost Changelog
+## Release v1.4.0
+
+Expected Release date: 2016-01-16
+
+### Release Highlights
+
+#### Data Center Support
+
+- Deployment guides on Red Hat Enterprise Linux 6 and 7 now available
+- Legal disclosure and support links (terms of service, privacy policy, help, about, and support email) now configurable
+- Over a dozen new configuration options in System Console
+
+#### Mobile Experience
+
+- iOS reference app [now available from iTunes](https://itunes.apple.com/us/app/mattermost/id984966508?ls=1&mt=8), compiled from [open source repo](https://github.com/mattermost/ios)
+- Date headers now show when scrolling on mobile, so you can quickly see when messages were sent
+- Added "rapid scroll" support for jumping quickily to bottom of channels on mobile
+
+### New Features
+
+Mobile Experience
+- Date headers now show when scrolling on mobile, so you can quickly see when messages were sent
+- Added "rapid scroll" support for jumping quickily to bottom of channels on mobile
+
+Authentication
+
+- Accounts can now switch between email and GitLab SSO sign-in options
+- New ability to customize session token length
+
+System Console
+
+- Added **Legal and Support Settings** so System Administrators can change the default Terms of Service, Privacy Policy, and Help links
+- Under **Service Settings** added options to customize expiry of web, mobile and SSO session tokens, expiry of caches in memory, and an EnableDeveloper option to turn on Developer Mode which alerts users to any console errors that occur
+
+### Improvements
+
+Performance and Testing
+
+- Added logging for email and push notifications events in DEBUG mode
+
+Integrations
+
+- Added support to allow optional parameters in the `Content-Type` of incoming webhook requests
+
+Files and Images
+
+- Animated GIFs autoplay in the image previewer
+
+Notifications and Email
+
+- Changed email notifications to display the server's local timezone instead of UTC
+
+User Interface
+
+- Updated the "About Mattermost" dialog formatting
+- Going to domain/teamname now goes to the last channel of your previous session, instead of Town Square
+- Various improvements to mobile UI, including a floating date indicator and the ability to quickly scroll to the bottom of the channel
+
+#### Bug Fixes
+
+- Fixed issue where usernames containing a "." did not get mention notifications
+- Fixed issue where System Console did not save the "Send push notifications" setting
+- Fixed issue with Font Display cancel button not working in Account Settings menu
+- Fixed incorrect default for "Team Name Display" settings
+- Fixed issue where various media files appeared broken in the media player on some browsers
+- Fixed cross-contamination issue when multiple accounts log into the same team on the same browser
+- Fixed issue where color pickers did not update when a theme was pasted in
+- Increased the maximum number of channels
+
+### Compatibility
+
+#### Config.json Changes from v1.3 to v1.4
+
+Multiple settings were added to `config.json`. Below is a list of the changes and their new default values in a fresh install.
+
+The following options can be modified in the System Console:
+
+- Under `ServiceSettings` in `config.json`:
+ - Added: `"EnableDeveloper": false` to set whether developer mode is enabled, which alerts users to any console errors that occur
+ - Added: `"SessionLengthWebInDays" : 30` to set the number of days before web sessions expire and users will need to log in again
+ - Added: `"SessionLengthMobileInDays" : 30` to set the number of days before native mobile sessions expire
+ - Added: `"SessionLengthSSOInDays" : 30` to set the number of days before SSO sessions expire
+ - Added: `"SessionCacheInMinutes" : 10` to set the number of minutes to cache a session in memory
+- Added `SupportSettings` section to `config.json`:
+ - Added: `"TermsOfServiceLink": "/static/help/terms.html"` to allow System Administrators to set the terms of service link
+ - Added: `"PrivacyPolicyLink": "/static/help/privacy.html"` to allow System Administrators to set the privacy policy link
+ - Added: `"AboutLink": "/static/help/about.html"` to allow System Administrators to set the about page link
+ - Added: `"HelpLink": "/static/help/help.html"` to allow System Administrators to set the help page link
+ - Added: `"ReportAProblemLink": "/static/help/report_problem.html"` to allow System Administrators to set the home page for the support website
+ - Added: `"SupportEmail":"feedback@mattermost.com"` to allow System Administrators to set an email address for feedback and support requests
+
+The following options are not present in the System Console, and can be modified manually in the `config.json` file:
+
+- Under `FileSettings` in `config.json`:
+ - Added: `"AmazonS3Endpoint": ""` to set an endpoint URL for an Amazon S3 instance
+ - Added: `"AmazonS3BucketEndpoint": ""` to set an endpoint URL for Amazon S3 buckets
+ - Added: `"AmazonS3LocationConstraint": false` to set whether the S3 region is location constrained
+ - Added: `"AmazonS3LowercaseBucket": false` to set whether bucket names are fully lowercase or not
+
+#### 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
+- No scroll bar in center channel
+- Pasting images into text box fails to upload on Firefox, Safari, and IE11
+- Public links for attachments attempt to download the file on IE, Edge, and Safari
+- Importing from Slack breaks @mentions and fails to load in certain cases with comments on files
+- System Console > TEAMS > Statistics > Newly Created Users shows all of the users are created "just now"
+- Favicon does not always become red when @mentions and direct messages are received on an inactive browser tab
+- Searching for a phrase in quotations returns more than just the phrase on Mattermost installations with a Postgres database
+- Deleted/Archived channels are not removed from the "More" menu of the person that deleted/archived the channel until after refresh
+- Search results don't highlight searches for @username, non-latin characters, or terms inside Markdown code blocks
+- Hashtags less than three characters long are not searchable
+- After deactivating a team member, the person remains in the channel counter
+- Certain symbols (<,>,-,+,=,%,^,#,*,|) directly before or after a hashtag cause the message to not show up in a hashtag search
+
+#### Contributors
+
+Many thanks to our external contributors. In no particular order:
+
+- [npcode](https://github.com/npcode)
+- [hjf288](https://github.com/hjf288)
+- [apskim](https://github.com/apskim)
+- [ejm2172](https://github.com/ejm2172)
+- [hvnsweeting](https://github.com/hvnsweeting)
+- [benburkert](https://github.com/benburkert)
+- [erikthered](https://github.com/erikthered)
+
## Release v1.3.0
Release date: 2015-12-16
diff --git a/Makefile b/Makefile
index 9fd74b959..1d7fbea5a 100644
--- a/Makefile
+++ b/Makefile
@@ -126,6 +126,10 @@ package:
cp -RL web/static/js/jquery-dragster $(DIST_PATH)/web/static/js/
cp -RL web/templates $(DIST_PATH)/web
+ cp -L web/static/js/react-0.14.3.js $(DIST_PATH)/web/static/js/
+ cp -L web/static/js/react-dom-0.14.3.js $(DIST_PATH)/web/static/js/
+ cp -L web/static/js/react-bootstrap-0.28.1.js $(DIST_PATH)/web/static/js/
+
mkdir -p $(DIST_PATH)/api
cp -RL api/templates $(DIST_PATH)/api
@@ -136,11 +140,11 @@ package:
mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js
mv $(DIST_PATH)/web/static/js/libs.min.js $(DIST_PATH)/web/static/js/libs-$(BUILD_NUMBER).min.js
- sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html
- sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html
+ #sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html
+ #sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
- sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/web/templates/head.html
+ #sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
diff --git a/api/channel.go b/api/channel.go
index b85de3071..674293e19 100644
--- a/api/channel.go
+++ b/api/channel.go
@@ -9,9 +9,14 @@ import (
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"net/http"
+ "strconv"
"strings"
)
+const (
+ defaultExtraMemberLimit = 100
+)
+
func InitChannel(r *mux.Router) {
l4g.Debug("Initializing channel api routes")
@@ -27,6 +32,7 @@ func InitChannel(r *mux.Router) {
sr.Handle("/update_notify_props", ApiUserRequired(updateNotifyProps)).Methods("POST")
sr.Handle("/{id:[A-Za-z0-9]+}/", ApiUserRequiredActivity(getChannel, false)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET")
+ sr.Handle("/{id:[A-Za-z0-9]+}/extra_info/{member_limit:-?[0-9]+}", ApiUserRequired(getChannelExtraInfo)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/join", ApiUserRequired(join)).Methods("POST")
sr.Handle("/{id:[A-Za-z0-9]+}/leave", ApiUserRequired(leave)).Methods("POST")
sr.Handle("/{id:[A-Za-z0-9]+}/delete", ApiUserRequired(deleteChannel)).Methods("POST")
@@ -730,10 +736,19 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) {
-
params := mux.Vars(r)
id := params["id"]
+ var memberLimit int
+ if memberLimitString, ok := params["member_limit"]; !ok {
+ memberLimit = defaultExtraMemberLimit
+ } else if memberLimitInt64, err := strconv.ParseInt(memberLimitString, 10, 0); err != nil {
+ c.Err = model.NewAppError("getChannelExtraInfo", "Failed to parse member limit", err.Error())
+ return
+ } else {
+ memberLimit = int(memberLimitInt64)
+ }
+
sc := Srv.Store.Channel().Get(id)
var channel *model.Channel
if cresult := <-sc; cresult.Err != nil {
@@ -743,13 +758,13 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) {
channel = cresult.Data.(*model.Channel)
}
- extraEtag := channel.ExtraEtag()
+ extraEtag := channel.ExtraEtag(memberLimit)
if HandleEtag(extraEtag, w, r) {
return
}
scm := Srv.Store.Channel().GetMember(id, c.Session.UserId)
- ecm := Srv.Store.Channel().GetExtraMembers(id, 100)
+ ecm := Srv.Store.Channel().GetExtraMembers(id, memberLimit)
ccm := Srv.Store.Channel().GetMemberCount(id)
if cmresult := <-scm; cmresult.Err != nil {
diff --git a/api/channel_benchmark_test.go b/api/channel_benchmark_test.go
index fb8dd61bc..d6e1e5a55 100644
--- a/api/channel_benchmark_test.go
+++ b/api/channel_benchmark_test.go
@@ -189,7 +189,7 @@ func BenchmarkGetChannelExtraInfo(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := range channels {
- Client.Must(Client.GetChannelExtraInfo(channels[j].Id, ""))
+ Client.Must(Client.GetChannelExtraInfo(channels[j].Id, -1, ""))
}
}
}
diff --git a/api/channel_test.go b/api/channel_test.go
index 4ef164cba..117278378 100644
--- a/api/channel_test.go
+++ b/api/channel_test.go
@@ -674,7 +674,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
- rget := Client.Must(Client.GetChannelExtraInfo(channel1.Id, ""))
+ rget := Client.Must(Client.GetChannelExtraInfo(channel1.Id, -1, ""))
data := rget.Data.(*model.ChannelExtra)
if data.Id != channel1.Id {
t.Fatal("couldnt't get extra info")
@@ -690,7 +690,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
currentEtag := rget.Etag
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, currentEtag); err != nil {
+ if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
t.Fatal(err)
} else if cache_result.Data.(*model.ChannelExtra) != nil {
t.Log(cache_result.Data)
@@ -708,7 +708,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
Client2.LoginByEmail(team.Name, user2.Email, "pwd")
Client2.Must(Client2.JoinChannel(channel1.Id))
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, currentEtag); err != nil {
+ if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
t.Fatal(err)
} else if cache_result.Data.(*model.ChannelExtra) == nil {
t.Log(cache_result.Data)
@@ -717,7 +717,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
currentEtag = cache_result.Etag
}
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, currentEtag); err != nil {
+ if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
t.Fatal(err)
} else if cache_result.Data.(*model.ChannelExtra) != nil {
t.Log(cache_result.Data)
@@ -728,7 +728,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
Client2.Must(Client2.LeaveChannel(channel1.Id))
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, currentEtag); err != nil {
+ if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
t.Fatal(err)
} else if cache_result.Data.(*model.ChannelExtra) == nil {
t.Log(cache_result.Data)
@@ -737,7 +737,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
currentEtag = cache_result.Etag
}
- if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, currentEtag); err != nil {
+ if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil {
t.Fatal(err)
} else if cache_result.Data.(*model.ChannelExtra) != nil {
t.Log(cache_result.Data)
@@ -745,6 +745,42 @@ func TestGetChannelExtraInfo(t *testing.T) {
} else {
currentEtag = cache_result.Etag
}
+
+ Client2.Must(Client2.JoinChannel(channel1.Id))
+
+ if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, 2, currentEtag); err != nil {
+ t.Fatal(err)
+ } else if extra := cache_result.Data.(*model.ChannelExtra); extra == nil {
+ t.Fatal("response should not be empty")
+ } else if len(extra.Members) != 2 {
+ t.Fatal("should've returned 2 members")
+ } else if extra.MemberCount != 2 {
+ t.Fatal("should've returned member count of 2")
+ } else {
+ currentEtag = cache_result.Etag
+ }
+
+ if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, 1, currentEtag); err != nil {
+ t.Fatal(err)
+ } else if extra := cache_result.Data.(*model.ChannelExtra); extra == nil {
+ t.Fatal("response should not be empty")
+ } else if len(extra.Members) != 1 {
+ t.Fatal("should've returned only 1 member")
+ } else if extra.MemberCount != 2 {
+ t.Fatal("should've returned member count of 2")
+ } else {
+ currentEtag = cache_result.Etag
+ }
+
+ if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, 1, currentEtag); err != nil {
+ t.Fatal(err)
+ } else if cache_result.Data.(*model.ChannelExtra) != nil {
+ t.Log(cache_result.Data)
+ t.Fatal("response should be empty")
+ } else {
+ currentEtag = cache_result.Etag
+ }
+
}
func TestAddChannelMember(t *testing.T) {
diff --git a/doc/developer/tests/test-search.md b/doc/developer/tests/test-search.md
new file mode 100644
index 000000000..0f0ba1153
--- /dev/null
+++ b/doc/developer/tests/test-search.md
@@ -0,0 +1,43 @@
+# Search Testing
+
+### Basic Search Testing
+
+This post is used by the core team to test search. It should be returned for the test cases for search, with proper highlighting in the search results.
+
+**Basic word search:** Hello world!
+**Emoji search:** :strawberry:
+**Accent search:** Crème friache
+**Non-latin search:**
+您好吗
+您好
+**Email search:** person@domain.org
+**Link search:** www.dropbox.com
+**Markdown search:**
+##### Hello
+```
+Hello
+```
+`Hello`
+
+
+### Hashtags:
+
+Click on the linked hashtags below, and confirm that the search results match the linked hashtags. Confirm that the highlighting in the search results is correct.
+
+#### Basic Hashtags
+
+#hello #world
+
+#### Hashtags containing punctuation:
+
+*Note: Make a separate post containing only the word “hashtag”, and confirm the hashtags below do not return the separate post.*
+
+#hashtag #hashtag-dash #hashtag_underscore #hashtag.dot
+
+#### Punctuation following a hashtag:
+
+#colon: #slash/ #backslash\ #percent% #dollar$ #semicolon; #ampersand& #bracket( #bracket) #lessthan< #greaterthan> #dash- #plus+ #equals= #caret^ #hashtag# #asterisk* #verticalbar| #invertedquestion¿ #atsign@ #quote” #apostrophe' #curlybracket{ #curlybracket} #squarebracket[ #squarebracket]
+
+#### Markdown surrounding a hashtag:
+
+*#markdown-hashtag*
diff --git a/doc/install/Upgrade-Guide.md b/doc/install/Upgrade-Guide.md
index 4480dedd2..1f3ff9510 100644
--- a/doc/install/Upgrade-Guide.md
+++ b/doc/install/Upgrade-Guide.md
@@ -43,7 +43,7 @@ The following instructions apply to updating installations of Mattermost v0.7-Be
Mattermost is designed to be upgraded sequentially through major version releases. If you skip versions when upgrading GitLab, you may find a `panic: The database schema version of 1.1.0 cannot be upgraded. You must not skip a version` error in your `/var/log/gitlab/mattermost/current` directory. If so:
1. Run `platform -version` to check your version of Mattermost
-2. If your version of the Mattermost binary doesn't match the version listed in the database error message, downgrade the version of the Mattermost binary you are using by [following the manual upgrade steps for Mattermost](/var/log/gitlab/mattermost/current) and using the database schema version listed in the error messages for the version you select in Step 1) iv).
+2. If your version of the Mattermost binary doesn't match the version listed in the database error message, downgrade the version of the Mattermost binary you are using by [following the manual upgrade steps for Mattermost](https://github.com/mattermost/platform/blob/master/doc/install/Upgrade-Guide.md#upgrading-mattermost-to-next-major-release) and using the database schema version listed in the error messages for the version you select in Step 1) iv).
3. Once Mattermost is working again, you can use the same upgrade procedure to upgrade Mattermost version by version to your current GitLab version. After this is done, GitLab automation should continue to work for future upgrades, so long as you don't skip versions.
| GitLab Version | Mattermost Version |
diff --git a/model/channel.go b/model/channel.go
index 0ce09f4bc..7109500d4 100644
--- a/model/channel.go
+++ b/model/channel.go
@@ -57,8 +57,8 @@ func (o *Channel) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
-func (o *Channel) ExtraEtag() string {
- return Etag(o.Id, o.ExtraUpdateAt)
+func (o *Channel) ExtraEtag(memberLimit int) string {
+ return Etag(o.Id, o.ExtraUpdateAt, memberLimit)
}
func (o *Channel) IsValid() *AppError {
diff --git a/model/client.go b/model/client.go
index f1773f3c7..14746f8ae 100644
--- a/model/client.go
+++ b/model/client.go
@@ -591,8 +591,8 @@ func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) {
}
}
-func (c *Client) GetChannelExtraInfo(id string, etag string) (*Result, *AppError) {
- if r, err := c.DoApiGet("/channels/"+id+"/extra_info", "", etag); err != nil {
+func (c *Client) GetChannelExtraInfo(id string, memberLimit int, etag string) (*Result, *AppError) {
+ if r, err := c.DoApiGet("/channels/"+id+"/extra_info/"+strconv.FormatInt(int64(memberLimit), 10), "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index c9e0d113f..4585647de 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -603,7 +603,14 @@ func (s SqlChannelStore) GetExtraMembers(channelId string, limit int) StoreChann
result := StoreResult{}
var members []model.ExtraMember
- _, err := s.GetReplica().Select(&members, "SELECT Id, Nickname, Email, ChannelMembers.Roles, Username FROM ChannelMembers, Users WHERE ChannelMembers.UserId = Users.Id AND ChannelId = :ChannelId LIMIT :Limit", map[string]interface{}{"ChannelId": channelId, "Limit": limit})
+ var err error
+
+ if limit != -1 {
+ _, err = s.GetReplica().Select(&members, "SELECT Id, Nickname, Email, ChannelMembers.Roles, Username FROM ChannelMembers, Users WHERE ChannelMembers.UserId = Users.Id AND ChannelId = :ChannelId LIMIT :Limit", map[string]interface{}{"ChannelId": channelId, "Limit": limit})
+ } else {
+ _, err = s.GetReplica().Select(&members, "SELECT Id, Nickname, Email, ChannelMembers.Roles, Username FROM ChannelMembers, Users WHERE ChannelMembers.UserId = Users.Id AND ChannelId = :ChannelId", map[string]interface{}{"ChannelId": channelId})
+ }
+
if err != nil {
result.Err = model.NewAppError("SqlChannelStore.GetExtraMembers", "We couldn't get the extra info for channel members", "channel_id="+channelId+", "+err.Error())
} else {
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 7dac39942..8b7485e5f 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -20,9 +20,14 @@ export default class ChannelInviteModal extends React.Component {
this.onListenerChange = this.onListenerChange.bind(this);
this.handleInvite = this.handleInvite.bind(this);
- this.state = this.getStateFromStores();
+ // the state gets populated when the modal is shown
+ this.state = {};
}
shouldComponentUpdate(nextProps, nextState) {
+ if (!this.props.show && !nextProps.show) {
+ return false;
+ }
+
if (!Utils.areObjectsEqual(this.props, nextProps)) {
return true;
}
@@ -34,13 +39,25 @@ export default class ChannelInviteModal extends React.Component {
return false;
}
getStateFromStores() {
- function getId(user) {
- return user.id;
+ const users = UserStore.getActiveOnlyProfiles();
+
+ if ($.isEmptyObject(users)) {
+ return {
+ loading: true
+ };
+ }
+
+ // make sure we have all members of this channel before rendering
+ const extraInfo = ChannelStore.getCurrentExtraInfo();
+ if (extraInfo.member_count !== extraInfo.members.length) {
+ AsyncClient.getChannelExtraInfo(this.props.channel.id, -1);
+
+ return {
+ loading: true
+ };
}
- var users = UserStore.getActiveOnlyProfiles();
- var memberIds = ChannelStore.getCurrentExtraInfo().members.map(getId);
- var loading = $.isEmptyObject(users);
+ const memberIds = extraInfo.members.map((user) => user.id);
var nonmembers = [];
for (var id in users) {
@@ -55,7 +72,7 @@ export default class ChannelInviteModal extends React.Component {
return {
nonmembers,
- loading
+ loading: false
};
}
onShow() {
diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx
index d1b9df988..513a720e7 100644
--- a/web/react/components/channel_members_modal.jsx
+++ b/web/react/components/channel_members_modal.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import LoadingScreen from './loading_screen.jsx';
import MemberList from './member_list.jsx';
import ChannelInviteModal from './channel_invite_modal.jsx';
@@ -21,9 +22,10 @@ export default class ChannelMembersModal extends React.Component {
this.onChange = this.onChange.bind(this);
this.handleRemove = this.handleRemove.bind(this);
- const state = this.getStateFromStores();
- state.showInviteModal = false;
- this.state = state;
+ // the rest of the state gets populated when the modal is shown
+ this.state = {
+ showInviteModal: false
+ };
}
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areObjectsEqual(this.props, nextProps)) {
@@ -37,8 +39,18 @@ export default class ChannelMembersModal extends React.Component {
return false;
}
getStateFromStores() {
+ const extraInfo = ChannelStore.getCurrentExtraInfo();
+
+ if (extraInfo.member_count !== extraInfo.members.length) {
+ AsyncClient.getChannelExtraInfo(this.props.channel.id, -1);
+
+ return {
+ loading: true
+ };
+ }
+
const users = UserStore.getActiveOnlyProfiles();
- const memberList = ChannelStore.getCurrentExtraInfo().members;
+ const memberList = extraInfo.members;
const nonmemberList = [];
for (const id in users) {
@@ -71,14 +83,14 @@ export default class ChannelMembersModal extends React.Component {
return {
nonmemberList,
- memberList
+ memberList,
+ loading: false
};
}
onShow() {
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
- this.onChange();
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
@@ -89,6 +101,8 @@ export default class ChannelMembersModal extends React.Component {
if (!this.props.show && nextProps.show) {
ChannelStore.addExtraInfoChangeListener(this.onChange);
ChannelStore.addChangeListener(this.onChange);
+
+ this.onChange();
} else if (this.props.show && !nextProps.show) {
ChannelStore.removeExtraInfoChangeListener(this.onChange);
ChannelStore.removeChangeListener(this.onChange);
@@ -154,6 +168,21 @@ export default class ChannelMembersModal extends React.Component {
isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles);
}
+ let content;
+ if (this.state.loading) {
+ content = (<LoadingScreen />);
+ } else {
+ content = (
+ <div className='team-member-list'>
+ <MemberList
+ memberList={this.state.memberList}
+ isAdmin={isAdmin}
+ handleRemove={this.handleRemove}
+ />
+ </div>
+ );
+ }
+
return (
<div>
<Modal
@@ -178,13 +207,7 @@ export default class ChannelMembersModal extends React.Component {
ref='modalBody'
style={{maxHeight}}
>
- <div className='team-member-list'>
- <MemberList
- memberList={this.state.memberList}
- isAdmin={isAdmin}
- handleRemove={this.handleRemove}
- />
- </div>
+ {content}
</Modal.Body>
<Modal.Footer>
<button
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index c10269680..eeb218bfe 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -266,7 +266,7 @@ export default class FileAttachment extends React.Component {
href={fileUrl}
download={filenameString}
data-toggle='tooltip'
- title={'Download ' + filenameString}
+ title={'Download \"' + filenameString + '\"'}
className='post-image__name'
>
{trimmedFilename}
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 21683bb01..26bd6adde 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -223,13 +223,13 @@ export default class PostInfo extends React.Component {
/>
</li>
<li className='col col__reply'>
- {comments}
<div
className='dropdown'
ref='dotMenu'
>
{dropdown}
</div>
+ {comments}
<Overlay
show={this.state.show}
target={() => ReactDOM.findDOMNode(this.refs.dotMenu)}
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index a28efbd04..7d8c7e265 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -24,6 +24,7 @@ export default class PostsView extends React.Component {
this.updateScrolling = this.updateScrolling.bind(this);
this.handleResize = this.handleResize.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this);
+ this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
this.jumpToPostNode = null;
this.wasAtBottom = true;
@@ -339,6 +340,10 @@ export default class PostsView extends React.Component {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
});
}
+ scrollToBottomAnimated() {
+ var postList = $(this.refs.postlist);
+ postList.animate({scrollTop: this.refs.postlist.scrollHeight}, '500');
+ }
componentDidMount() {
if (this.props.postList != null) {
this.updateScrolling();
@@ -458,7 +463,7 @@ export default class PostsView extends React.Component {
<ScrollToBottomArrows
isScrolling={this.state.isScrolling}
atBottom={this.wasAtBottom}
- onClick={this.scrollToBottom}
+ onClick={this.scrollToBottomAnimated}
/>
<div
ref='postlist'
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index f218270da..0ee89b9fa 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -168,7 +168,7 @@ export function getMoreChannels(force) {
}
}
-export function getChannelExtraInfo(id) {
+export function getChannelExtraInfo(id, memberLimit) {
let channelId;
if (id) {
channelId = id;
@@ -185,6 +185,7 @@ export function getChannelExtraInfo(id) {
client.getChannelExtraInfo(
channelId,
+ memberLimit,
(data, textStatus, xhr) => {
callTracker['getChannelExtraInfo_' + channelId] = 0;
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index e1c331aff..96d1ef720 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -824,10 +824,17 @@ export function getChannelCounts(success, error) {
});
}
-export function getChannelExtraInfo(id, success, error) {
+export function getChannelExtraInfo(id, memberLimit, success, error) {
+ let url = '/api/v1/channels/' + id + '/extra_info';
+
+ if (memberLimit) {
+ url += '/' + memberLimit;
+ }
+
$.ajax({
- url: '/api/v1/channels/' + id + '/extra_info',
+ url,
dataType: 'json',
+ contentType: 'application/json',
type: 'GET',
success,
error: function onError(xhr, status, err) {
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 937b08084..7b7c2d73a 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -286,8 +286,10 @@ body.ios {
z-index: 50;
@include opacity(0);
@include single-transition(all, 0.3s);
+ display: none;
&.scrolling {
+ display: block;
@include opacity(1);
}
}
@@ -417,12 +419,6 @@ body.ios {
background-color: beige;
}
- ul {
- margin: 0;
- padding: 0;
- }
-
-
p {
margin: 0;
line-height: 1.6em;
@@ -601,6 +597,7 @@ body.ios {
right: 0;
top: 30px;
width: 65px;
+ white-space: nowrap;
}
.permalink-popover {
@@ -634,8 +631,7 @@ body.ios {
.dropdown {
display: inline-block;
visibility: hidden;
- position: absolute;
- right: 0;
+ margin-right: 5px;
top: -1px;
.dropdown-menu {
@@ -671,20 +667,17 @@ body.ios {
@include legacy-pie-clearfix;
width: calc(100% - 75px);
- img {
- max-height: 400px;
+ p {
+ margin: 0 0 0.4em;
}
- ul {
- margin-bottom: 0.6em;
- padding: 5px 0 0 20px;
- }
-
- ul + p {
- margin-top: 1em;
+ img {
+ max-height: 400px;
}
ul, ol {
+ margin-bottom: 0.4em;
+
p {
margin-bottom: 0;
}
diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss
index d820447f5..bd3d60622 100644
--- a/web/sass-files/sass/partials/_post_right.scss
+++ b/web/sass-files/sass/partials/_post_right.scss
@@ -25,6 +25,7 @@
.col__reply {
top: 0;
+ text-align: right;
}
}