diff options
62 files changed, 396 insertions, 478 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c27041165..8606fc72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -144,24 +144,37 @@ The following is for informational purposes only, no action needed. Mattermost a ##### Licenses Table 1. Added `Licenses` Table +##### Commands Table +1. Added `Commands` Table + #### Known Issues - Navigating to a page with new messages containing inline images added via markdown causes the channel to scroll up and down while loading the inline images. -- Microsoft Edge does not yet support drag and drop for file attachments. +- Microsoft Edge does not yet support drag and drop for file attachments. +- No error message on IE11 when uploading more than 5 files or a file over 50 MB. +- File name tooltip stays open after clicking to download. - Scroll bar does not appear in the center channel. - Unable to paste images into the text box on Firefox, Safari, and IE11. -- Importing from Slack fails to load messages in certain cases and breaks @mentions. -- System Console > TEAMS > Statistics > Newly Created Users shows all users as created "just now". -- Favicon does not turn red when @mentions and direct messages are received in an inactive browser tab. +- Importing from Slack fails to load channels in certain cases. +- System Console > Teams > Statistics > Newly Created Users shows all users as created "just now". +- Username and email display on single line in System Console user management tab. - Searching for a phrase in quotations returns more than just the phrase on installations with a Postgres database. - Archived channels are not removed from the "More" menu for the person that archived the channel until after refresh. +- First load of an empty channel does not display the introduction message. - Search results don't highlight searches for @username, non-latin characters, or terms inside Markdown code blocks. - Searching for a username or hashtag containing a dot returns a search where the dot is replaced with the "or" operator. +- Search term highlighting doesn't update on IE11 when search terms change but return the same posts. - Hashtags less than three characters long are not searchable. -- Users remains in the channel counter after being deactivated. -- Messages with symbols (<,>,-,+,=,%,^,#,*,|) directly before or after a hashtag are not searchable. -- Permalinks for the second message or later consecutively sent in a group by the same author displaces the copy link popover or causes an error -- Emoji smileys ending with a letter at the end of a message do not auto-complete as expected +- Hashtags containing a dash incorrectly highlight in the search results. +- Users remain in the channel counter after being deactivated. +- Permalinks for the second message or later consecutively sent in a group by the same author displaces the copy link popover or causes an error. +- Emoji smileys ending with a letter at the end of a message do not auto-complete as expected. +- Logout slash command does not force a logout. +- Incorrect formatting when a new line is added directly after a list. +- Timestamps are displayed in 12-hour format when set to 24-hour format. +- GIF links inside code blocks auto-post the GIFs. +- Syntax highlighting code block is missing the label for Latex documents. +- Deleted messages don't delete in the RHS until a page refresh. #### Contributors diff --git a/api/admin.go b/api/admin.go index e8cb8b3c7..d04991353 100644 --- a/api/admin.go +++ b/api/admin.go @@ -120,6 +120,7 @@ func getConfig(c *Context, w http.ResponseWriter, r *http.Request) { cfg := model.ConfigFromJson(strings.NewReader(json)) json = cfg.ToJson() + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Write([]byte(json)) } diff --git a/api/channel.go b/api/channel.go index ff5b0f8da..e97e08fc0 100644 --- a/api/channel.go +++ b/api/channel.go @@ -729,7 +729,6 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } else { w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag()) - w.Header().Set("Expires", "-1") w.Write([]byte(data.ToJson())) } } @@ -798,7 +797,6 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) { data := model.ChannelExtra{Id: channel.Id, Members: extraMembers, MemberCount: memberCount} w.Header().Set(model.HEADER_ETAG_SERVER, extraEtag) - w.Header().Set("Expires", "-1") w.Write([]byte(data.ToJson())) } } diff --git a/api/context.go b/api/context.go index b91981ecd..d0b4f85d2 100644 --- a/api/context.go +++ b/api/context.go @@ -165,6 +165,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } else { // All api response bodies will be JSON formatted by default w.Header().Set("Content-Type", "application/json") + + if r.Method == "GET" { + w.Header().Set("Expires", "0") + } } if len(token) != 0 { diff --git a/api/post.go b/api/post.go index fadabd66e..9d3ba5ab1 100644 --- a/api/post.go +++ b/api/post.go @@ -1197,6 +1197,5 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) { } w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Expires", "0") w.Write([]byte(posts.ToJson())) } diff --git a/api/team.go b/api/team.go index 6d59e94e9..052d6e698 100644 --- a/api/team.go +++ b/api/team.go @@ -647,7 +647,6 @@ func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } else { w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.Team).Etag()) - w.Header().Set("Expires", "-1") w.Write([]byte(result.Data.(*model.Team).ToJson())) return } diff --git a/api/templates/email_change_body.html b/api/templates/email_change_body.html index 4f28584c4..41b1bcd7d 100644 --- a/api/templates/email_change_body.html +++ b/api/templates/email_change_body.html @@ -18,7 +18,7 @@ <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2> - <p>{{.Props.Info}}</p> + <p>{{.Html.Info}}</p> </td> </tr> <tr> diff --git a/api/user.go b/api/user.go index 8f381aeda..7919da168 100644 --- a/api/user.go +++ b/api/user.go @@ -897,7 +897,6 @@ func getMe(c *Context, w http.ResponseWriter, r *http.Request) { } else { result.Data.(*model.User).Sanitize(map[string]bool{}) w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.User).Etag()) - w.Header().Set("Expires", "-1") w.Write([]byte(result.Data.(*model.User).ToJson())) return } @@ -1761,14 +1760,14 @@ func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayNa go func() { subjectPage := NewServerTemplatePage("email_change_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.email_change_body", + subjectPage.Props["Subject"] = c.T("api.templates.email_change_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) bodyPage := NewServerTemplatePage("email_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title") - bodyPage.Props["Info"] = c.T("api.templates.email_change_body.info", - map[string]interface{}{"TeamDisplayName": teamDisplayName, "NewEmail": newEmail}) + bodyPage.Html["Info"] = template.HTML(c.T("api.templates.email_change_body.info", + map[string]interface{}{"TeamDisplayName": teamDisplayName, "NewEmail": newEmail})) if err := utils.SendMail(oldEmail, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_email_change_email_and_forget.error"), err) diff --git a/docker/1.2/Dockerrun.aws.zip b/docker/1.2/Dockerrun.aws.zip Binary files differdeleted file mode 100644 index 4de6ec362..000000000 --- a/docker/1.2/Dockerrun.aws.zip +++ /dev/null diff --git a/docker/1.2/Dockerrun.aws/Dockerrun.aws.json b/docker/1.2/Dockerrun.aws/Dockerrun.aws.json deleted file mode 100755 index c32a998e4..000000000 --- a/docker/1.2/Dockerrun.aws/Dockerrun.aws.json +++ /dev/null @@ -1,13 +0,0 @@ -{
- "AWSEBDockerrunVersion": "1",
- "Image": {
- "Name": "mattermost/platform:1.2",
- "Update": "true"
- },
- "Ports": [
- {
- "ContainerPort": "80"
- }
- ],
- "Logging": "/var/log/"
-}
diff --git a/docker/1.2/config_docker.json b/docker/1.2/config_docker.json deleted file mode 100644 index c23a72cd1..000000000 --- a/docker/1.2/config_docker.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "ServiceSettings": { - "ListenAddress": ":80", - "MaximumLoginAttempts": 10, - "SegmentDeveloperKey": "", - "GoogleDeveloperKey": "", - "EnableOAuthServiceProvider": false, - "EnableIncomingWebhooks": false, - "EnableOutgoingWebhooks": false, - "EnablePostUsernameOverride": false, - "EnablePostIconOverride": false, - "EnableTesting": false, - "EnableSecurityFixAlert": true - }, - "TeamSettings": { - "SiteName": "Mattermost", - "MaxUsersPerTeam": 50, - "EnableTeamCreation": true, - "EnableUserCreation": true, - "RestrictCreationToDomains": "", - "RestrictTeamNames": true, - "EnableTeamListing": false - }, - "SqlSettings": { - "DriverName": "mysql", - "DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8", - "DataSourceReplicas": [], - "MaxIdleConns": 10, - "MaxOpenConns": 10, - "Trace": false, - "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QVg" - }, - "LogSettings": { - "EnableConsole": false, - "ConsoleLevel": "INFO", - "EnableFile": true, - "FileLevel": "INFO", - "FileFormat": "", - "FileLocation": "" - }, - "FileSettings": { - "DriverName": "local", - "Directory": "/mattermost/data/", - "EnablePublicLink": true, - "PublicLinkSalt": "A705AklYF8MFDOfcwh3I488G8vtLlVip", - "ThumbnailWidth": 120, - "ThumbnailHeight": 100, - "PreviewWidth": 1024, - "PreviewHeight": 0, - "ProfileWidth": 128, - "ProfileHeight": 128, - "InitialFont": "luximbi.ttf", - "AmazonS3AccessKeyId": "", - "AmazonS3SecretAccessKey": "", - "AmazonS3Bucket": "", - "AmazonS3Region": "" - }, - "EmailSettings": { - "EnableSignUpWithEmail": true, - "SendEmailNotifications": false, - "RequireEmailVerification": false, - "FeedbackName": "", - "FeedbackEmail": "", - "SMTPUsername": "", - "SMTPPassword": "", - "SMTPServer": "", - "SMTPPort": "", - "ConnectionSecurity": "", - "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS", - "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL", - "ApplePushServer": "", - "ApplePushCertPublic": "", - "ApplePushCertPrivate": "" - }, - "RateLimitSettings": { - "EnableRateLimiter": true, - "PerSec": 10, - "MemoryStoreSize": 10000, - "VaryByRemoteAddr": true, - "VaryByHeader": "" - }, - "PrivacySettings": { - "ShowEmailAddress": true, - "ShowFullName": true - }, - "GitLabSettings": { - "Enable": false, - "Secret": "", - "Id": "", - "Scope": "", - "AuthEndpoint": "", - "TokenEndpoint": "", - "UserApiEndpoint": "" - } -} diff --git a/docker/1.3/Dockerfile b/docker/1.3/Dockerfile deleted file mode 100644 index 4a25198af..000000000 --- a/docker/1.3/Dockerfile +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -# See License.txt for license information. -FROM ubuntu:14.04 - -# -# Install SQL -# - -ENV MYSQL_ROOT_PASSWORD=mostest -ENV MYSQL_USER=mmuser -ENV MYSQL_PASSWORD=mostest -ENV MYSQL_DATABASE=mattermost_test - -RUN groupadd -r mysql && useradd -r -g mysql mysql - -RUN apt-key adv --keyserver pool.sks-keyservers.net --recv-keys A4A9406876FCBD3C456770C88C718D3B5072E1F5 - -ENV MYSQL_MAJOR 5.6 -ENV MYSQL_VERSION 5.6.25 - -RUN echo "deb http://repo.mysql.com/apt/debian/ wheezy mysql-${MYSQL_MAJOR}" > /etc/apt/sources.list.d/mysql.list - -RUN apt-get update \ - && export DEBIAN_FRONTEND=noninteractive \ - && apt-get -y install perl wget mysql-server \ - && rm -rf /var/lib/apt/lists/* \ - && rm -rf /var/lib/mysql && mkdir -p /var/lib/mysql - -RUN sed -Ei 's/^(bind-address|log)/#&/' /etc/mysql/my.cnf - -VOLUME /var/lib/mysql -# --------------------------------------------------------------------------------------------------------------------- - -WORKDIR /mattermost - -# Copy over files -ADD https://github.com/mattermost/platform/releases/download/v1.3.0/mattermost.tar.gz / -RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz -ADD config_docker.json / -ADD docker-entry.sh / - -RUN chmod +x /docker-entry.sh -ENTRYPOINT /docker-entry.sh - -# Create default storage directory -RUN mkdir /mattermost-data/ - -# Ports -EXPOSE 80 diff --git a/docker/1.3/Dockerrun.aws.zip b/docker/1.3/Dockerrun.aws.zip Binary files differdeleted file mode 100644 index dd201d990..000000000 --- a/docker/1.3/Dockerrun.aws.zip +++ /dev/null diff --git a/docker/1.3/Dockerrun.aws/.ebextensions/01_files.config b/docker/1.3/Dockerrun.aws/.ebextensions/01_files.config deleted file mode 100644 index 7f40a8b34..000000000 --- a/docker/1.3/Dockerrun.aws/.ebextensions/01_files.config +++ /dev/null @@ -1,14 +0,0 @@ -files: - "/etc/nginx/conf.d/proxy.conf": - mode: "000755" - owner: root - group: root - content: | - client_max_body_size 50M; - "/opt/elasticbeanstalk/hooks/appdeploy/post/init.sh": - mode: "000755" - owner: root - group: root - content: | - #!/usr/bin/env bash - gpasswd -a ec2-user docker diff --git a/docker/1.3/README.md b/docker/1.3/README.md deleted file mode 100644 index f737a1554..000000000 --- a/docker/1.3/README.md +++ /dev/null @@ -1,23 +0,0 @@ -Mattermost -========== - -http:/mattermost.org - -Mattermost is an open-source team communication service. It brings team messaging and file sharing into one place, accessible across PCs and phones, with archiving and search. - -Installing Mattermost -===================== - -To run an instance of the latest version of mattermost on your local machine you can run: - -`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform` - -To update this image to the latest version you can run: - -`docker pull mattermost/platform` - -To run an instance of the latest code from the master branch on GitHub you can run: - -`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:dev` - -Any questions, please visit http://forum.mattermost.org diff --git a/docker/1.3/docker-entry.sh b/docker/1.3/docker-entry.sh deleted file mode 100755 index 6bd2a1263..000000000 --- a/docker/1.3/docker-entry.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/bin/bash -# Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -# See License.txt for license information. - -mkdir -p web/static/js - -echo "127.0.0.1 dockerhost" >> /etc/hosts -/etc/init.d/networking restart - -echo configuring mysql - -# SQL!!! -set -e - -get_option () { - local section=$1 - local option=$2 - local default=$3 - ret=$(my_print_defaults $section | grep '^--'${option}'=' | cut -d= -f2-) - [ -z $ret ] && ret=$default - echo $ret -} - - -# Get config -DATADIR="$("mysqld" --verbose --help 2>/dev/null | awk '$1 == "datadir" { print $2; exit }')" -SOCKET=$(get_option mysqld socket "$DATADIR/mysql.sock") -PIDFILE=$(get_option mysqld pid-file "/var/run/mysqld/mysqld.pid") - -if [ ! -d "$DATADIR/mysql" ]; then - if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" ]; then - echo >&2 'error: database is uninitialized and MYSQL_ROOT_PASSWORD not set' - echo >&2 ' Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?' - exit 1 - fi - - mkdir -p "$DATADIR" - chown -R mysql:mysql "$DATADIR" - - echo 'Running mysql_install_db' - mysql_install_db --user=mysql --datadir="$DATADIR" --rpm --keep-my-cnf - echo 'Finished mysql_install_db' - - mysqld --user=mysql --datadir="$DATADIR" --skip-networking & - for i in $(seq 30 -1 0); do - [ -S "$SOCKET" ] && break - echo 'MySQL init process in progress...' - sleep 1 - done - if [ $i = 0 ]; then - echo >&2 'MySQL init process failed.' - exit 1 - fi - - # These statements _must_ be on individual lines, and _must_ end with - # semicolons (no line breaks or comments are permitted). - # TODO proper SQL escaping on ALL the things D: - - tempSqlFile=$(mktemp /tmp/mysql-first-time.XXXXXX.sql) - cat > "$tempSqlFile" <<-EOSQL - -- What's done in this file shouldn't be replicated - -- or products like mysql-fabric won't work - SET @@SESSION.SQL_LOG_BIN=0; - - DELETE FROM mysql.user ; - CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; - GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ; - DROP DATABASE IF EXISTS test ; - EOSQL - - if [ "$MYSQL_DATABASE" ]; then - echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" >> "$tempSqlFile" - fi - - if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then - echo "CREATE USER '"$MYSQL_USER"'@'%' IDENTIFIED BY '"$MYSQL_PASSWORD"' ;" >> "$tempSqlFile" - - if [ "$MYSQL_DATABASE" ]; then - echo "GRANT ALL ON \`"$MYSQL_DATABASE"\`.* TO '"$MYSQL_USER"'@'%' ;" >> "$tempSqlFile" - fi - fi - - echo 'FLUSH PRIVILEGES ;' >> "$tempSqlFile" - - mysql -uroot < "$tempSqlFile" - - rm -f "$tempSqlFile" - kill $(cat $PIDFILE) - for i in $(seq 30 -1 0); do - [ -f "$PIDFILE" ] || break - echo 'MySQL init process in progress...' - sleep 1 - done - if [ $i = 0 ]; then - echo >&2 'MySQL hangs during init process.' - exit 1 - fi - echo 'MySQL init process done. Ready for start up.' -fi - -chown -R mysql:mysql "$DATADIR" - -mysqld & - -sleep 5 - -# ------------------------ - -echo starting platform -cd /mattermost/bin -./platform -config=/config_docker.json diff --git a/docker/1.2/Dockerfile b/docker/2.0/Dockerfile index e00c4e5ca..0f7a13e45 100644 --- a/docker/1.2/Dockerfile +++ b/docker/2.0/Dockerfile @@ -34,7 +34,7 @@ VOLUME /var/lib/mysql WORKDIR /mattermost # Copy over files -ADD https://github.com/mattermost/platform/releases/download/v1.2.1/mattermost.tar.gz / +ADD https://github.com/mattermost/platform/releases/download/v2.0.0-rc2/mattermost.tar.gz / RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz ADD config_docker.json / ADD docker-entry.sh / diff --git a/docker/2.0/Dockerrun.aws.zip b/docker/2.0/Dockerrun.aws.zip Binary files differnew file mode 100644 index 000000000..bfbe56400 --- /dev/null +++ b/docker/2.0/Dockerrun.aws.zip diff --git a/docker/2.0/Dockerrun.aws/.ebextensions/.zip b/docker/2.0/Dockerrun.aws/.ebextensions/.zip Binary files differnew file mode 100644 index 000000000..053a7f725 --- /dev/null +++ b/docker/2.0/Dockerrun.aws/.ebextensions/.zip diff --git a/docker/1.2/Dockerrun.aws/.ebextensions/01_files.config b/docker/2.0/Dockerrun.aws/.ebextensions/01_files.config index 7f40a8b34..7f40a8b34 100644 --- a/docker/1.2/Dockerrun.aws/.ebextensions/01_files.config +++ b/docker/2.0/Dockerrun.aws/.ebextensions/01_files.config diff --git a/docker/1.3/Dockerrun.aws/Dockerrun.aws.json b/docker/2.0/Dockerrun.aws/Dockerrun.aws.json index d4027e67c..7c739c0c5 100755 --- a/docker/1.3/Dockerrun.aws/Dockerrun.aws.json +++ b/docker/2.0/Dockerrun.aws/Dockerrun.aws.json @@ -1,7 +1,7 @@ {
"AWSEBDockerrunVersion": "1",
"Image": {
- "Name": "mattermost/platform:1.3",
+ "Name": "mattermost/platform:2.0",
"Update": "true"
},
"Ports": [
diff --git a/docker/1.2/README.md b/docker/2.0/README.md index f737a1554..f737a1554 100644 --- a/docker/1.2/README.md +++ b/docker/2.0/README.md diff --git a/docker/1.3/config_docker.json b/docker/2.0/config_docker.json index a35abb9da..6a1290189 100644 --- a/docker/1.3/config_docker.json +++ b/docker/2.0/config_docker.json @@ -7,10 +7,18 @@ "EnableOAuthServiceProvider": false, "EnableIncomingWebhooks": false, "EnableOutgoingWebhooks": false, + "EnableCommands": false, + "EnableOnlyAdminIntegrations": true, "EnablePostUsernameOverride": false, "EnablePostIconOverride": false, "EnableTesting": false, - "EnableSecurityFixAlert": true + "EnableDeveloper": false, + "EnableSecurityFixAlert": true, + "EnableInsecureOutgoingConnections": false, + "SessionLengthWebInDays" : 30, + "SessionLengthMobileInDays" : 30, + "SessionLengthSSOInDays" : 30, + "SessionCacheInMinutes" : 10 }, "TeamSettings": { "SiteName": "Mattermost", @@ -53,10 +61,16 @@ "AmazonS3AccessKeyId": "", "AmazonS3SecretAccessKey": "", "AmazonS3Bucket": "", - "AmazonS3Region": "" + "AmazonS3Region": "", + "AmazonS3Endpoint": "", + "AmazonS3BucketEndpoint": "", + "AmazonS3LocationConstraint": false, + "AmazonS3LowercaseBucket": false }, "EmailSettings": { "EnableSignUpWithEmail": true, + "EnableSignInWithEmail": true, + "EnableSignInWithUsername": false, "SendEmailNotifications": false, "RequireEmailVerification": false, "FeedbackName": "", @@ -82,6 +96,14 @@ "ShowEmailAddress": true, "ShowFullName": true }, + "SupportSettings": { + "TermsOfServiceLink": "/static/help/terms.html", + "PrivacyPolicyLink": "/static/help/privacy.html", + "AboutLink": "/static/help/about.html", + "HelpLink": "/static/help/help.html", + "ReportAProblemLink": "/static/help/report_problem.html", + "SupportEmail": "feedback@mattermost.com" + }, "GitLabSettings": { "Enable": false, "Secret": "", diff --git a/docker/1.2/docker-entry.sh b/docker/2.0/docker-entry.sh index 6bd2a1263..6bd2a1263 100755 --- a/docker/1.2/docker-entry.sh +++ b/docker/2.0/docker-entry.sh diff --git a/model/gitlab/gitlab.go b/model/gitlab/gitlab.go index 8b96c64f6..3ca499976 100644 --- a/model/gitlab/gitlab.go +++ b/model/gitlab/gitlab.go @@ -67,6 +67,18 @@ func gitLabUserFromJson(data io.Reader) *GitLabUser { } } +func (glu *GitLabUser) IsValid() bool { + if glu.Id == 0 { + return false + } + + if len(glu.Email) == 0 { + return false + } + + return true +} + func (glu *GitLabUser) getAuthData() string { return strconv.FormatInt(glu.Id, 10) } @@ -76,9 +88,20 @@ func (m *GitLabProvider) GetIdentifier() string { } func (m *GitLabProvider) GetUserFromJson(data io.Reader) *model.User { - return userFromGitLabUser(gitLabUserFromJson(data)) + glu := gitLabUserFromJson(data) + if glu.IsValid() { + return userFromGitLabUser(glu) + } + + return &model.User{} } func (m *GitLabProvider) GetAuthDataFromJson(data io.Reader) string { - return gitLabUserFromJson(data).getAuthData() + glu := gitLabUserFromJson(data) + + if glu.IsValid() { + return glu.getAuthData() + } + + return "" } diff --git a/model/version.go b/model/version.go index 69529e7a1..8fbd65d03 100644 --- a/model/version.go +++ b/model/version.go @@ -13,6 +13,7 @@ import ( // It should be maitained in chronological order with most current // release at the front of the list. var versions = []string{ + "2.0.0", "1.4.0", "1.3.0", "1.2.1", diff --git a/model/version_test.go b/model/version_test.go index d73273ce5..e0346c43a 100644 --- a/model/version_test.go +++ b/model/version_test.go @@ -83,28 +83,23 @@ func TestIsCurrentVersion(t *testing.T) { func TestIsPreviousVersionsSupported(t *testing.T) { - // 1.4.0 CURRENT RELEASED VERSION - if !IsPreviousVersionsSupported(versions[0]) { + if !IsPreviousVersionsSupported(versionsWithoutHotFixes[0]) { t.Fatal() } - // 1.3.0 - if !IsPreviousVersionsSupported(versions[1]) { + if !IsPreviousVersionsSupported(versionsWithoutHotFixes[1]) { t.Fatal() } - // 1.2.1 - if !IsPreviousVersionsSupported(versions[2]) { + if !IsPreviousVersionsSupported(versionsWithoutHotFixes[2]) { t.Fatal() } - // 1.2.0 - if !IsPreviousVersionsSupported(versions[3]) { + if IsPreviousVersionsSupported(versionsWithoutHotFixes[4]) { t.Fatal() } - // 1.1.0 NOT SUPPORTED - if IsPreviousVersionsSupported(versions[4]) { + if IsPreviousVersionsSupported(versionsWithoutHotFixes[5]) { t.Fatal() } } diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go index 8b52dae12..87ee2bb11 100644 --- a/store/sql_channel_store.go +++ b/store/sql_channel_store.go @@ -615,9 +615,36 @@ func (s SqlChannelStore) GetExtraMembers(channelId string, limit int) StoreChann 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}) + _, err = s.GetReplica().Select(&members, ` + SELECT + Id, + Nickname, + Email, + ChannelMembers.Roles, + Username + FROM + ChannelMembers, + Users + WHERE + ChannelMembers.UserId = Users.Id + AND Users.DeleteAt = 0 + 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}) + _, err = s.GetReplica().Select(&members, ` + SELECT + Id, + Nickname, + Email, + ChannelMembers.Roles, + Username + FROM + ChannelMembers, + Users + WHERE + ChannelMembers.UserId = Users.Id + AND Users.DeleteAt = 0 + AND ChannelId = :ChannelId`, map[string]interface{}{"ChannelId": channelId}) } if err != nil { diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go index a3b0c2286..816a85aef 100644 --- a/store/sql_channel_store_test.go +++ b/store/sql_channel_store_test.go @@ -377,6 +377,18 @@ func TestChannelMemberStore(t *testing.T) { if t4 != t3 { t.Fatal("Should not update time upon failure") } + + // rejoin the channel and make sure that an inactive user isn't returned by GetExtraMambers + Must(store.Channel().SaveMember(&o2)) + + u2.DeleteAt = 1000 + Must(store.User().Update(&u2, true)) + + if result := <-store.Channel().GetExtraMembers(o1.ChannelId, 20); result.Err != nil { + t.Fatal(result.Err) + } else if extraMembers := result.Data.([]model.ExtraMember); len(extraMembers) != 1 { + t.Fatal("should have 1 extra members") + } } func TestChannelDeleteMemberStore(t *testing.T) { diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx index 0c1a55cc1..009a9f004 100644 --- a/web/react/components/admin_console/user_item.jsx +++ b/web/react/components/admin_console/user_item.jsx @@ -353,7 +353,7 @@ export default class UserItem extends React.Component { return ( <tr> - <td className='row member-div'> + <td className='row member-div padding--equal'> <img className='post-profile-img pull-left' src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} diff --git a/web/react/components/channel_info_modal.jsx b/web/react/components/channel_info_modal.jsx index 5067f5913..83f5aba65 100644 --- a/web/react/components/channel_info_modal.jsx +++ b/web/react/components/channel_info_modal.jsx @@ -56,7 +56,7 @@ class ChannelInfoModal extends React.Component { </div> <div className='col-sm-9'>{channelURL}</div> </div> - <div className='row'> + <div className='row form-group'> <div className='col-sm-3 info__label'> <FormattedMessage id='channel_info.id' diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index 174c8c4e1..f3000ee05 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -95,6 +95,8 @@ class ChannelLoader extends React.Component { $(window).on('focus', function windowFocus() { AsyncClient.updateLastViewedAt(); + ChannelStore.resetCounts(ChannelStore.getCurrentId()); + ChannelStore.emitChange(); window.isActive = true; }); @@ -185,4 +187,4 @@ ChannelLoader.propTypes = { intl: intlShape.isRequired }; -export default injectIntl(ChannelLoader);
\ No newline at end of file +export default injectIntl(ChannelLoader); diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index e6a9fbd25..835298635 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -25,6 +25,7 @@ const ActionTypes = Constants.ActionTypes; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import {FormattedMessage} from 'mm-intl'; +import attachFastClick from 'fastclick'; const Popover = ReactBootstrap.Popover; const OverlayTrigger = ReactBootstrap.OverlayTrigger; @@ -59,6 +60,7 @@ export default class Navbar extends React.Component { ChannelStore.addChangeListener(this.onChange); ChannelStore.addExtraInfoChangeListener(this.onChange); $('.inner__wrap').click(this.hideSidebars); + attachFastClick(document.body); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChange); diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index f217229ed..afff78bae 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -107,7 +107,7 @@ export default class PopoverListMembers extends React.Component { name = Utils.displayUsername(teamMembers[m.username].id); } - if (name && teamMembers[m.username].delete_at <= 0) { + if (name) { popoverHtml.push( <div className='text-nowrap' diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index 6d82423d5..c44223b1f 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -144,7 +144,8 @@ export default class PostInfo extends React.Component { ); } - handlePermalink() { + handlePermalink(e) { + e.preventDefault(); EventHelpers.showGetPostLinkModal(this.props.post); } diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx index ebe19abad..19ab7d5aa 100644 --- a/web/react/components/posts_view.jsx +++ b/web/react/components/posts_view.jsx @@ -535,7 +535,15 @@ function FloatingTimestamp({isScrolling, post}) { return <noscript />; } - const dateString = Utils.getDateForUnixTicks(post.create_at).toDateString(); + const dateString = ( + <FormattedDate + value={post.create_at} + weekday='short' + day='2-digit' + month='short' + year='numeric' + /> + ); let className = 'post-list__timestamp'; if (isScrolling) { diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index 9c85e9940..0d15c8599 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -31,6 +31,7 @@ class RhsComment extends React.Component { this.retryComment = this.retryComment.bind(this); this.parseEmojis = this.parseEmojis.bind(this); + this.handlePermalink = this.handlePermalink.bind(this); this.state = {}; } @@ -67,6 +68,10 @@ class RhsComment extends React.Component { parseEmojis() { twemoji.parse(ReactDOM.findDOMNode(this), {size: Constants.EMOJI_SIZE}); } + handlePermalink(e) { + e.preventDefault(); + EventHelpers.showGetPostLinkModal(this.props.post); + } componentDidMount() { this.parseEmojis(); } @@ -92,6 +97,23 @@ class RhsComment extends React.Component { var dropdownContents = []; + dropdownContents.push( + <li + key='rhs-root-permalink' + role='presentation' + > + <a + href='#' + onClick={this.handlePermalink} + > + <FormattedMessage + id='rhs_comment.permalink' + defaultMessage='Permalink' + /> + </a> + </li> + ); + if (isOwner) { dropdownContents.push( <li diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index f9f7f8f81..54f2e8262 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -21,6 +21,7 @@ export default class RhsRootPost extends React.Component { super(props); this.parseEmojis = this.parseEmojis.bind(this); + this.handlePermalink = this.handlePermalink.bind(this); this.state = {}; } @@ -31,6 +32,10 @@ export default class RhsRootPost extends React.Component { folder: Emoji.getImagePathForEmoticon() }); } + handlePermalink(e) { + e.preventDefault(); + EventHelpers.showGetPostLinkModal(this.props.post); + } componentDidMount() { this.parseEmojis(); } @@ -83,6 +88,23 @@ export default class RhsRootPost extends React.Component { var dropdownContents = []; + dropdownContents.push( + <li + key='rhs-root-permalink' + role='presentation' + > + <a + href='#' + onClick={this.handlePermalink} + > + <FormattedMessage + id='rhs_root.permalink' + defaultMessage='Permalink' + /> + </a> + </li> + ); + if (isOwner) { dropdownContents.push( <li diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx index 4adc3afe0..12c066734 100644 --- a/web/react/components/search_results.jsx +++ b/web/react/components/search_results.jsx @@ -15,13 +15,16 @@ function getStateFromStores() { const results = SearchStore.getSearchResults(); const channels = new Map(); - const channelIds = results.order.map((postId) => results.posts[postId].channel_id); - for (const id of channelIds) { - if (channels.has(id)) { - continue; - } - channels.set(id, ChannelStore.get(id)); + if (results && results.order) { + const channelIds = results.order.map((postId) => results.posts[postId].channel_id); + for (const id of channelIds) { + if (channels.has(id)) { + continue; + } + + channels.set(id, ChannelStore.get(id)); + } } return { diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx index 0a1b02853..c1b2a2e7f 100644 --- a/web/react/components/team_general_tab.jsx +++ b/web/react/components/team_general_tab.jsx @@ -486,13 +486,9 @@ class GeneralTab extends React.Component { inputs.push( <div key='teamInviteSetting'> <div className='row'> - <label className='col-sm-5 control-label'> - <FormattedMessage - id='general_tab.codeTitle' - defaultMessage='Invite Code' - /> + <label className='col-sm-5 control-label visible-xs-block'> </label> - <div className='col-sm-7'> + <div className='col-sm-12'> <input className='form-control' type='text' diff --git a/web/react/components/team_signup_email_item.jsx b/web/react/components/team_signup_email_item.jsx index feb70dc71..790ec2e5d 100644 --- a/web/react/components/team_signup_email_item.jsx +++ b/web/react/components/team_signup_email_item.jsx @@ -83,4 +83,4 @@ TeamSignupEmailItem.propTypes = { email: React.PropTypes.string }; -export default injectIntl(TeamSignupEmailItem);
\ No newline at end of file +export default injectIntl(TeamSignupEmailItem, {withRef: true}); diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx index 46a6bc68e..343db13e8 100644 --- a/web/react/components/team_signup_send_invites_page.jsx +++ b/web/react/components/team_signup_send_invites_page.jsx @@ -33,8 +33,8 @@ export default class TeamSignupSendInvitesPage extends React.Component { var emails = []; for (var i = 0; i < this.props.state.invites.length; i++) { - if (this.refs['email_' + i].validate(this.props.state.team.email)) { - emails.push(this.refs['email_' + i].getValue()); + if (this.refs['email_' + i].getWrappedInstance().validate(this.props.state.team.email)) { + emails.push(this.refs['email_' + i].getWrappedInstance().getValue()); } else { valid = false; } diff --git a/web/react/components/user_settings/manage_command_hooks.jsx b/web/react/components/user_settings/manage_command_hooks.jsx index f4009aeaa..bd0659a47 100644 --- a/web/react/components/user_settings/manage_command_hooks.jsx +++ b/web/react/components/user_settings/manage_command_hooks.jsx @@ -257,7 +257,7 @@ export default class ManageCommandCmds extends React.Component { let triggerDiv; if (cmd.trigger && cmd.trigger.length !== 0) { triggerDiv = ( - <div className='padding-top'> + <div className='padding-top x2'> <strong> <FormattedMessage id='user.settings.cmds.trigger' @@ -371,7 +371,7 @@ export default class ManageCommandCmds extends React.Component { /> </a> <a - className='webcmd__remove' + className='webhook__remove webcmd__remove' href='#' onClick={this.removeCmd.bind(this, cmd.id)} > diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx index c6532b018..e79ec6f6c 100644 --- a/web/react/components/user_settings/manage_incoming_hooks.jsx +++ b/web/react/components/user_settings/manage_incoming_hooks.jsx @@ -183,7 +183,7 @@ export default class ManageIncomingHooks extends React.Component { <div key='addIncomingHook'> <FormattedHTMLMessage id='user.settings.hooks_in.description' - defaultMessage='Create webhook URLs for use in external integrations. Please see<a href="http://mattermost.org/webhooks" target="_blank">http://mattermost.org/webhooks</a> to learn more.' + defaultMessage='Create webhook URLs for use in external integrations. Please see <a href="http://mattermost.org/webhooks" target="_blank">http://mattermost.org/webhooks</a> to learn more.' /> <div><label className='control-label padding-top x2'> <FormattedMessage diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx index 3f88e9f41..44aab486e 100644 --- a/web/react/components/user_settings/manage_outgoing_hooks.jsx +++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx @@ -18,6 +18,10 @@ const holders = defineMessages({ callbackHolder: { id: 'user.settings.hooks_out.callbackHolder', defaultMessage: 'Each URL must start with http:// or https://' + }, + select: { + id: 'user.settings.hooks_out.select', + defaultMessage: '--- Select a channel ---' } }); @@ -153,10 +157,7 @@ class ManageOutgoingHooks extends React.Component { key='select-channel' value='' > - <FormattedMessage - id='user.settings.hooks_out.select' - defaultMessage='--- Select a channel ---' - /> + {this.props.intl.formatMessage(holders.select)} </option> ); diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index a7541073e..5442f7ac4 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -7,6 +7,7 @@ import SettingsSidebar from '../settings_sidebar.jsx'; import UserStore from '../../stores/user_store.jsx'; import * as Utils from '../../utils/utils.jsx'; +import Constants from '../../utils/constants.jsx'; const Modal = ReactBootstrap.Modal; @@ -224,14 +225,19 @@ class UserSettingsModal extends React.Component { resetTheme() { const user = UserStore.getCurrentUser(); - if (user.theme_props != null) { + if (user.theme_props == null) { + Utils.applyTheme(Constants.THEMES.default); + } else { Utils.applyTheme(user.theme_props); } } render() { const {formatMessage} = this.props.intl; + var currentUser = UserStore.getCurrentUser(); + var isAdmin = Utils.isAdmin(currentUser.roles); var tabs = []; + tabs.push({name: 'general', uiName: formatMessage(holders.general), icon: 'glyphicon glyphicon-cog'}); tabs.push({name: 'security', uiName: formatMessage(holders.security), icon: 'glyphicon glyphicon-lock'}); tabs.push({name: 'notifications', uiName: formatMessage(holders.notifications), icon: 'glyphicon glyphicon-exclamation-sign'}); @@ -240,8 +246,17 @@ class UserSettingsModal extends React.Component { } if (global.window.mm_config.EnableIncomingWebhooks === 'true' || global.window.mm_config.EnableOutgoingWebhooks === 'true' || global.window.mm_config.EnableCommands === 'true') { - tabs.push({name: 'integrations', uiName: formatMessage(holders.integrations), icon: 'glyphicon glyphicon-transfer'}); + var show = global.window.mm_config.EnableOnlyAdminIntegrations !== 'true'; + + if (global.window.mm_config.EnableOnlyAdminIntegrations === 'true' && isAdmin) { + show = true; + } + + if (show) { + tabs.push({name: 'integrations', uiName: formatMessage(holders.integrations), icon: 'glyphicon glyphicon-transfer'}); + } } + tabs.push({name: 'display', uiName: formatMessage(holders.display), icon: 'glyphicon glyphicon-eye-open'}); tabs.push({name: 'advanced', uiName: formatMessage(holders.advanced), icon: 'glyphicon glyphicon-list-alt'}); diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx index 5693047c2..53d79906f 100644 --- a/web/react/components/user_settings/user_settings_security.jsx +++ b/web/react/components/user_settings/user_settings_security.jsx @@ -11,6 +11,7 @@ import TeamStore from '../../stores/team_store.jsx'; import * as Client from '../../utils/client.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; +import * as Utils from '../../utils/utils.jsx'; import Constants from '../../utils/constants.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; @@ -216,15 +217,12 @@ class SecurityTab extends React.Component { var describe; var d = new Date(this.props.user.last_password_update); - var timeOfDay = ' am'; - if (d.getHours() >= 12) { - timeOfDay = ' pm'; - } const locale = global.window.mm_locale; + const hours12 = !Utils.isMilitaryTime(); describe = formatMessage(holders.lastUpdated, { date: d.toLocaleDateString(locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: d.toLocaleTimeString(locale, {hours12: true, hour: '2-digit', minute: '2-digit'}) + timeOfDay + time: d.toLocaleTimeString(locale, {hour12: hours12, hour: '2-digit', minute: '2-digit'}) }); updateSectionStatus = function updateSection() { diff --git a/web/react/package.json b/web/react/package.json index fce3e6555..5fc2f2851 100644 --- a/web/react/package.json +++ b/web/react/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "autolinker": "0.22.0", + "fastclick": "^1.0.6", "flux": "2.1.1", "highlight.js": "8.9.1", "keymirror": "0.1.1", diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index ac800a988..60cb10de7 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -308,7 +308,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { ChannelStore.storeChannels(action.channels); ChannelStore.storeChannelMembers(action.members); currentId = ChannelStore.getCurrentId(); - if (currentId && !document.hidden) { + if (currentId && window.isActive) { ChannelStore.resetCounts(currentId); } ChannelStore.setUnreadCounts(); @@ -321,7 +321,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { ChannelStore.pStoreChannelMember(action.member); } currentId = ChannelStore.getCurrentId(); - if (currentId && !document.hidden) { + if (currentId && window.isActive) { ChannelStore.resetCounts(currentId); } ChannelStore.setUnreadCount(action.channel.id); diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index b5bb93576..f5c342163 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -83,8 +83,6 @@ class PostStoreClass extends EventEmitter { this.getCommentDraft = this.getCommentDraft.bind(this); this.clearDraftUploads = this.clearDraftUploads.bind(this); this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this); - this.storeLatestUpdate = this.storeLatestUpdate.bind(this); - this.getLatestUpdate = this.getLatestUpdate.bind(this); this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this); this.getCommentCount = this.getCommentCount.bind(this); @@ -258,7 +256,7 @@ class PostStoreClass extends EventEmitter { const np = newPosts.posts[pid]; if (np.delete_at === 0) { combinedPosts.posts[pid] = np; - if (combinedPosts.order.indexOf(pid) === -1) { + if (combinedPosts.order.indexOf(pid) === -1 && newPosts.order.indexOf(pid) !== -1) { combinedPosts.order.push(pid); } } @@ -507,19 +505,6 @@ class PostStoreClass extends EventEmitter { } }); } - storeLatestUpdate(channelId, time) { - if (!this.postsInfo.hasOwnProperty(channelId)) { - this.postsInfo[channelId] = {}; - } - this.postsInfo[channelId].latestPost = time; - } - getLatestUpdate(channelId) { - if (this.postsInfo.hasOwnProperty(channelId) && this.postsInfo[channelId].hasOwnProperty('latestPost')) { - return this.postsInfo[channelId].latestPost; - } - - return 0; - } getCommentCount(post) { const posts = this.getAllPosts(post.channel_id).posts; diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index e1b65fe14..efb57e226 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -32,6 +32,8 @@ class SocketStoreClass extends EventEmitter { this.failCount = 0; + this.translations = this.getDefaultTranslations(); + this.initialize(); } @@ -174,6 +176,18 @@ class SocketStoreClass extends EventEmitter { this.translations = messages; } + getDefaultTranslations() { + return ({ + socketError: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.', + someone: 'Someone', + posted: 'Posted', + uploadedImage: ' uploaded an image', + uploadedFile: ' uploaded a file', + something: ' did something new', + wrote: ' wrote: ' + }); + } + close() { if (conn && conn.readyState === WebSocket.OPEN) { conn.close(); @@ -188,10 +202,10 @@ function handleNewPostEvent(msg, translations) { // Update channel state if (ChannelStore.getCurrentId() === msg.channel_id) { - if (document.hidden) { - AsyncClient.getChannel(msg.channel_id); - } else { + if (window.isActive) { AsyncClient.updateLastViewedAt(); + } else { + AsyncClient.getChannel(msg.channel_id); } } else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) { AsyncClient.getChannel(msg.channel_id); diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index c8676f45d..13b57092d 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -521,18 +521,25 @@ export function getPosts(id) { return; } - if (PostStore.getAllPosts(channelId) == null) { + const postList = PostStore.getAllPosts(channelId); + + if ($.isEmptyObject(postList) || postList.order.length < Constants.POST_CHUNK_SIZE) { getPostsPage(channelId, Constants.POST_CHUNK_SIZE); return; } - const latestUpdate = PostStore.getLatestUpdate(channelId); + const latestPost = PostStore.getLatestPost(channelId); + let latestPostTime = 0; + + if (latestPost != null && latestPost.update_at != null) { + latestPostTime = latestPost.create_at; + } callTracker['getPosts_' + channelId] = utils.getTimestamp(); client.getPosts( channelId, - latestUpdate, + latestPostTime, (data, textStatus, xhr) => { if (xhr.status === 304 || !data) { return; @@ -542,7 +549,7 @@ export function getPosts(id) { type: ActionTypes.RECEIVED_POSTS, id: channelId, before: true, - numRequested: Constants.POST_CHUNK_SIZE, + numRequested: 0, post_list: data }); diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index 8b3602a89..493916058 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -152,7 +152,7 @@ class MattermostMarkdownRenderer extends marked.Renderer { } codespan(text) { - return '<pre class="text-nowrap">' + super.codespan(text) + '</pre>'; + return '<span class="codespan__pre-wrap">' + super.codespan(text) + '</span>'; } br() { @@ -222,7 +222,7 @@ class MattermostMarkdownRenderer extends marked.Renderer { } table(header, body) { - return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`; + return `<div class="table-responsive"><table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table></div>`; } listitem(text) { diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index e2a5b9620..7f124149d 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -260,6 +260,10 @@ export function displayTimeFormatted(ticks) { ); } +export function isMilitaryTime() { + return PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time'); +} + export function displayDateTime(ticks) { var seconds = Math.floor((Date.now() - ticks) / 1000); diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss index a08379ae1..f34b5ec19 100644 --- a/web/sass-files/sass/partials/_markdown.scss +++ b/web/sass-files/sass/partials/_markdown.scss @@ -31,6 +31,7 @@ } .post-body--code__language { + -webkit-transform: translate3d(0,0,0); position: absolute; top: 0; right: 0; @@ -54,11 +55,9 @@ code { white-space: pre; } - pre { - &.text-nowrap { - code { - white-space: nowrap; - } + .codespan__pre-wrap { + code { + white-space: pre-wrap; } } } diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index db99e840b..9d4e62bc3 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -7,6 +7,67 @@ padding: 20px 15px; overflow: auto; } +.more-table { + margin: 0; + table-layout: fixed; + p { + font-size: 0.9em; + overflow: hidden; + text-overflow: ellipsis; + @include opacity(0.8); + margin: 5px 0; + } + .profile-img { + -moz-border-radius: 50px; + -webkit-border-radius: 50px; + border-radius: 50px; + margin-right: 8px; + } + .more-name { + font-weight: 600; + font-size: 0.95em; + overflow: hidden; + text-overflow: ellipsis; + } + .more-description { + @include opacity(0.7); + display: block; + overflow: hidden; + text-overflow: ellipsis; + } + tbody { + > tr { + &:hover td { + background: #f7f7f7; + } + &:first-child { + td { + border: none; + } + } + td { + width: 100%; + white-space: nowrap; + @include legacy-pie-clearfix; + text-overflow: ellipsis; + padding: 8px 8px 8px 15px; + &.padding--equal { + padding: 8px; + } + &.td--action { + text-align: right; + padding: 8px 15px 8px 8px; + width: 80px; + vertical-align: middle; + position: relative; + &.lg { + width: 110px; + } + } + } + } + } +} .modal { width: 100%; color: #333; @@ -148,64 +209,6 @@ padding: 0; } } - .more-table { - margin: 0; - table-layout: fixed; - p { - font-size: 0.9em; - overflow: hidden; - text-overflow: ellipsis; - @include opacity(0.8); - margin: 5px 0; - } - .profile-img { - -moz-border-radius: 50px; - -webkit-border-radius: 50px; - border-radius: 50px; - margin-right: 8px; - } - .more-name { - font-weight: 600; - font-size: 0.95em; - overflow: hidden; - text-overflow: ellipsis; - } - .more-description { - @include opacity(0.7); - display: block; - overflow: hidden; - text-overflow: ellipsis; - } - tbody { - > tr { - &:hover td { - background: #f7f7f7; - } - &:first-child { - td { - border: none; - } - } - td { - width: 100%; - white-space: nowrap; - @include legacy-pie-clearfix; - text-overflow: ellipsis; - padding: 8px 8px 8px 15px; - &.td--action { - text-align: right; - padding: 8px 15px 8px 8px; - width: 80px; - vertical-align: middle; - position: relative; - &.lg { - width: 110px; - } - } - } - } - } - } .modal-image { position:relative; width:100%; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 5d6cbee60..2374a9dbb 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -776,6 +776,24 @@ } } @media screen and (max-width: 480px) { + .settings-modal { + + .settings-table { + + .security-links { + margin-bottom: 10px; + display: block; + + &:last-child { + margin-bottom: 0; + } + + } + + } + + } + .tip-overlay.tip-overlay--sidebar { min-height: 350px; } diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index 1bbec566c..de7df403f 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -207,6 +207,7 @@ .section-title { margin-bottom: 5px; font-weight: 600; + padding-right: 50px; } .section-edit { diff --git a/web/sass-files/sass/partials/_tutorial.scss b/web/sass-files/sass/partials/_tutorial.scss index 0a2d1e704..1f93fc6a9 100644 --- a/web/sass-files/sass/partials/_tutorial.scss +++ b/web/sass-files/sass/partials/_tutorial.scss @@ -105,7 +105,7 @@ font-size: 12px; span { - @include opacity(0.5); + @include opacity(0.9); } } diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index ade6462e9..9fbd5c343 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -827,11 +827,13 @@ "rename_channel.cancel": "Cancel", "rename_channel.save": "Save", "rhs_comment.comment": "Comment", + "rhs_comment.permalink": "Permalink", "rhs_comment.edit": "Edit", "rhs_comment.del": "Delete", "rhs_comment.retry": "Retry", "rhs_header.details": "Message Details", "rhs_root.direct": "Direct Message", + "rhs_root.permalink": "Permalink", "rhs_root.edit": "Edit", "rhs_root.del": "Delete", "search_bar.search": "Search", @@ -1068,33 +1070,33 @@ "user.settings.cmds.add_trigger.placeholder": "Command trigger e.g. \"hello\" not including the slash", "user.settings.cmds.auto_complete_desc.placeholder": "Example: \"Returns search results for patient records\"", "user.settings.cmds.auto_complete_hint.placeholder": "Example: [Patient Name]", - "user.settings.cmds.auto_complete_desc_desc": "Optional short description of slash command for the autocomplete list.", "user.settings.cmds.url.placeholder": "Must start with http:// or https://", "user.settings.cmds.auto_complete.yes": "yes", "user.settings.cmds.auto_complete.no": "no", "user.settings.cmds.trigger": "Command Trigger Word: ", - "user.settings.cmds.display_name": "Descriptive Label: ", + "user.settings.cmds.url": "Request URL: ", + "user.settings.cmds.request_type": "Request Method: ", "user.settings.cmds.username": "Response Username: ", "user.settings.cmds.icon_url": "Response Icon: ", "user.settings.cmds.auto_complete": "Autocomplete: ", - "user.settings.cmds.auto_complete_desc": "Autocomplete Description: ", "user.settings.cmds.auto_complete_hint": "Autocomplete Hint: ", - "user.settings.cmds.request_type": "Request Method: ", - "user.settings.cmds.url": "Request URL: ", + "user.settings.cmds.auto_complete_desc": "Autocomplete Description: ", + "user.settings.cmds.display_name": "Descriptive Label: ", "user.settings.cmds.token": "Token: ", "user.settings.cmds.regen": "Regen Token", "user.settings.cmds.none": "None", "user.settings.cmds.existing": "Existing commands", - "user.settings.cmds.add_desc": "Create slash commands to send events to external integrations and receive a response. For example typing `/patient Joe Smith` could bring back search results from your internal health records management system for the name 'Joe Smith'. Please see <a href=\"http://docs.mattermost.com/developer/slash-commands.html\">Slash commands documentation</a> for detailed instructions.", + "user.settings.cmds.add_desc": "Create slash commands to send events to external integrations and receive a response. For example typing `/patient Joe Smith` could bring back search results from your internal health records management system for the name “Joe Smith”. Please see <a href=\"http://docs.mattermost.com/developer/slash-commands.html\">Slash commands documentation</a> for detailed instructions.", "user.settings.cmds.add_new": "Add a new command", - "user.settings.cmds.cmd_display_name": "Brief description of slash command to show in listings.", + "user.settings.cmds.trigger_desc": "Examples: /patient, /client, /employee Reserved: /echo, /join, /logout, /me, /shrug", + "user.settings.cmds.url_desc": "The callback URL to receive the HTTP POST or GET event request when the slash command is run.", + "user.settings.cmds.request_type_desc": "The type of command request issued to the Request URL.", "user.settings.cmds.username_desc": "Choose a username override for responses for this slash command. Usernames can consist of up to 22 characters consisting of lowercase letters, numbers and they symbols \"-\", \"_\", and \".\" .", "user.settings.cmds.icon_url_desc": "Choose a profile picture override for the post responses to this slash command. Enter the URL of a .png or .jpg file at least 128 pixels by 128 pixels.", - "user.settings.cmds.trigger_desc": "Examples: /patient, /client, /employee Reserved: /echo, /join, /logout, /me, /shrug", "user.settings.cmds.auto_complete_help": " Show this command in the autocomplete list.", "user.settings.cmds.auto_complete_hint_desc": "Optional hint in the autocomplete list about parameters needed for command.", - "user.settings.cmds.request_type_desc": "The type of command request issued to the Request URL.", - "user.settings.cmds.url_desc": "The callback URL to receive the HTTP POST or GET event request when the slash command is run.", + "user.settings.cmds.auto_complete_desc_desc": "Optional short description of slash command for the autocomplete list.", + "user.settings.cmds.cmd_display_name": "Brief description of slash command to show in listings.", "user.settings.cmds.add": "Add", "user.settings.hooks_in.channel": "Channel: ", "user.settings.hooks_in.none": "None", @@ -1263,4 +1265,4 @@ "intro_messages.beginning": "Beginning of {name}", "intro_messages.invite": "Invite others to this {type}", "intro_messages.setHeader": "Set a Header" -} +}
\ No newline at end of file diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index e4ddd76ce..c44545e67 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -674,6 +674,8 @@ "get_link.clipboard": " Enlace copiado al portapapeles.", "get_link.close": "Cerrar", "get_link.copy": "Copiar Enlace", + "get_post_link_modal.help": "En enlace de abajo permite a los usuarios autorizados a ver tu mensaje.", + "get_post_link_modal.title": "Copiar enlace Permanente", "get_team_invite_link_modal.help": "Enviar a los compañeros de equipo el enlace que se muestra a continuación para permitirles registrarse a este equipo.", "get_team_invite_link_modal.helpDisabled": "La creación de usuario ha sido deshabilitada para tu equipo. Por favor solicita más detalles a tu administrador de equipo.", "get_team_invite_link_modal.title": "Enlace de Invitación al Equipo", @@ -1058,19 +1060,40 @@ "user.settings.advance.sendTitle": "Enviar mensajes con Ctrl + Retorno", "user.settings.advance.title": "Configuración Avanzada", "user.settings.cmds.add": "Agregar", + "user.settings.cmds.add_desc": "Crea comandos de barra para enviar eventos a integraciones externas y recibir una respuesta. Por ejemplo al escribir `/paciente Joe Smith` podría retornar resultados de una búsqueda en tu sistema de adminitración de salud para el nombre “Joe Smith”. Por favor revisa la <a href=\"http://docs.mattermost.com/developer/slash-commands.html\">Documentación de comandos de barra</a> para instrucciones detallas.", + "user.settings.cmds.add_display_name.placeholder": "Ejemplo: \"Buscar registros del paciente\"", "user.settings.cmds.add_new": "Agregar un nuevo comando", "user.settings.cmds.add_trigger.placeholder": "Gatillador del Comando ej. \"hola\" no se debe incluir la barra", "user.settings.cmds.add_username.placeholder": "Nombre de usuario", + "user.settings.cmds.auto_complete": "Autocompletado: ", "user.settings.cmds.auto_complete.no": "no", "user.settings.cmds.auto_complete.yes": "sí", + "user.settings.cmds.auto_complete_desc": "Descripción del Autocompletado: ", + "user.settings.cmds.auto_complete_desc.placeholder": "Ejemplo: \"Retorna resultados de una búsqueda con los registros de un paciente\"", + "user.settings.cmds.auto_complete_desc_desc": "Descripción corta opcional para la lista de autocompletado del comando de barra.", "user.settings.cmds.auto_complete_help": "Mostrar este comando en la lista de auto completado.", + "user.settings.cmds.auto_complete_hint": "Pista del Autocompletado: ", + "user.settings.cmds.auto_complete_hint.placeholder": "Ejemplo: [Nombre del Paciente]", + "user.settings.cmds.auto_complete_hint_desc": "Pista opcional que aparece como paramentros necesarios en la lista de autocompletado para el comando.", + "user.settings.cmds.cmd_display_name": "Breve descripción del comando de barra para mostrar en el listado.", + "user.settings.cmds.display_name": "Etiqueta Descriptiva: ", "user.settings.cmds.existing": "Comandos existentes", + "user.settings.cmds.icon_url": "Icono de Respuesta: ", + "user.settings.cmds.icon_url_desc": "Escoge una imagen de perfil que reemplazara los mensajes publicados por este comando de barra. Ingresa el URL de un archivo .png o .jpg de al menos 128 x 128 pixels.", "user.settings.cmds.none": "Ninguno", "user.settings.cmds.regen": "Regenerar Token", + "user.settings.cmds.request_type": "Método de Solicitud: ", + "user.settings.cmds.request_type_desc": "El tipo de comando que se utiliza al hacer una solicitud al URL.", "user.settings.cmds.request_type_get": "GET", "user.settings.cmds.request_type_post": "POST", "user.settings.cmds.token": "Token: ", + "user.settings.cmds.trigger": "Palabra Gatilladora del Comando: ", + "user.settings.cmds.trigger_desc": "Ejemplos: /paciente, /cliente, /empleado Reservadas: /echo, /join, /logout, /me, /shrug", + "user.settings.cmds.url": "URL de Solicitud: ", "user.settings.cmds.url.placeholder": "Debe comenzar con http:// o https://", + "user.settings.cmds.url_desc": "El URL para recibir el evento de la solicitud HTTP POST o GET cuando se ejecuta el comando de barra.", + "user.settings.cmds.username": "Nombre de usuario de Respuesta: ", + "user.settings.cmds.username_desc": "Escoge un nombre de usuario que reemplazara los mensajes publicados por este comando de barra. Los nombres de usuario pueden tener hasta 22 caracteres y contener letras en minúsculas, números y los siguientes símbolos \"-\", \"_\", y \".\" .", "user.settings.custom_theme.awayIndicator": "Indicador Ausente", "user.settings.custom_theme.buttonBg": "Fondo Botón", "user.settings.custom_theme.buttonColor": "Texto Botón", @@ -1173,6 +1196,8 @@ "user.settings.import_theme.importHeader": "Importar Tema de Slack", "user.settings.import_theme.submit": "Enviar", "user.settings.import_theme.submitError": "Formato inválido, por favor intenta copiando y pegando nuevamente.", + "user.settings.integrations.commands": "Comandos de Barra", + "user.settings.integrations.commandsDescription": "Administra tus comandos de barra", "user.settings.integrations.incomingWebhooks": "Webhooks de entrada", "user.settings.integrations.incomingWebhooksDescription": "Administra tus webhooks de entrada", "user.settings.integrations.outWebhooks": "Webhooks de salida", @@ -1238,4 +1263,4 @@ "view_image_popover.download": "Descargar", "view_image_popover.file": "Archivo {count} de {total}", "view_image_popover.publicLink": "Obtener Enlace Público" -} +}
\ No newline at end of file diff --git a/web/templates/head.html b/web/templates/head.html index da65e1779..94a86e3dd 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -79,6 +79,7 @@ $(function() { if (window.mm_preferences != null) { PreferenceStore.setPreferences(window.mm_preferences); + PreferenceStore.emitChange(); } }); |