summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
author=Corey Hulen <corey@hulen.com>2015-10-22 09:19:18 -0700
committer=Corey Hulen <corey@hulen.com>2015-10-22 09:19:18 -0700
commit54894e90ac705eab3bf0f34c8dd197bb0929feae (patch)
tree2fd5c6bc1bdb8cef1ea6095e8f12f8fd73a52ce9
parentc4c04e5f02253dfdfc2bd09e06955f1f5932ce18 (diff)
parent649f42e3fc706f8fa829276bcdb825381bc703f2 (diff)
downloadchat-54894e90ac705eab3bf0f34c8dd197bb0929feae.tar.gz
chat-54894e90ac705eab3bf0f34c8dd197bb0929feae.tar.bz2
chat-54894e90ac705eab3bf0f34c8dd197bb0929feae.zip
Fixing merge conflict
-rw-r--r--CHANGELOG.md91
-rw-r--r--api/command.go29
-rw-r--r--api/file.go10
-rw-r--r--api/team.go9
-rw-r--r--config/config.json3
-rw-r--r--doc/help/Search.md11
-rw-r--r--doc/install/Troubleshooting.md10
-rw-r--r--doc/install/Upgrade-Guide.md8
-rw-r--r--model/config.go6
-rw-r--r--model/team.go4
-rw-r--r--model/team_test.go16
-rw-r--r--store/sql_team_store.go5
-rw-r--r--utils/config.go1
-rw-r--r--web/react/components/admin_console/team_settings.jsx34
-rw-r--r--web/react/components/create_comment.jsx24
-rw-r--r--web/react/components/file_attachment.jsx158
-rw-r--r--web/react/components/file_upload_overlay.jsx16
-rw-r--r--web/react/components/team_signup_url_page.jsx10
-rw-r--r--web/react/components/view_image.jsx89
-rw-r--r--web/react/stores/post_store.jsx13
-rw-r--r--web/react/utils/emoticons.jsx2
-rw-r--r--web/react/utils/markdown.jsx11
-rw-r--r--web/react/utils/text_formatting.jsx9
-rw-r--r--web/react/utils/utils.jsx1
-rw-r--r--web/sass-files/sass/partials/_files.scss39
-rw-r--r--web/sass-files/sass/partials/_modal.scss13
-rw-r--r--web/sass-files/sass/partials/_post.scss48
-rw-r--r--web/sass-files/sass/partials/_responsive.scss23
-rw-r--r--web/sass-files/sass/partials/_settings.scss3
-rw-r--r--web/static/images/filesOverlay.pngbin0 -> 8392 bytes
-rw-r--r--web/static/images/logoWhite.pngbin0 -> 5876 bytes
31 files changed, 577 insertions, 119 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49a0ead89..54d9122c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -145,7 +145,6 @@ Messaging, Comments and Notifications
- Full markdown support in messages, comments, and channel description
- Support for emoji codes rendering to image files
-
Files and Images
- Added ability to play video and audio files
@@ -198,86 +197,86 @@ Licensing
### Bug Fixes
-- Fixed issue so that SSO option automatically set EmailVerified=true (it was false previously)
+- Fixed issue so that SSO option automatically set `EmailVerified=true` (it was false previously)
### Compatibility
A large number of settings were changed in [`config.json`](./config/config.json) and a System Console UI was added. This is a very large change due to Mattermost releasing as v1.0 and it's unlikely a change of this size would happen again.
-Prior to upgrading the Mattermost binaries from the previous versions, the below options would need to be manually updated in existing config.json file to migrate successfully. This is a list of changes and their new default values in a fresh install:
+Prior to upgrading the Mattermost binaries from the previous versions, the below options would need to be manually updated in your existing config.json file to migrate successfully. This is a list of changes and their new default values in a fresh install:
#### Config.json Changes from v0.7 to v1.0
##### Service Settings
- Under `ServiceSettings` in [`config.json`](./config/config.json):
- - **Moved:** `"SiteName": "Mattermost"` which was added to `TeamSettings`
- - **Removed:** `"Mode" : "dev"` which deprecates a high level dev mode, now replaced by granular controls
- - **Renamed:** `"AllowTesting" : false` to `"EnableTesting": false` which allows the use of `/loadtest` slash commands during development
- - **Removed:** `"UseSSL": false` boolean replaced by `"ConnectionSecurity": ""` under `Security` with new options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`)
- - **Renamed**: `"Port": "8065"` to `"ListenAddress": ":8065"` to define address on which to listen. Must be prepended with a colon.
- - **Removed:** `"Version": "developer"` removed and version information now stored in `model/version.go`
- - **Removed:** `"Shards": {}` which was not used
- - **Moved:** `"InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"` to `EmailSettings`
- - **Moved:** `"PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4"` to `FileSettings`
- - **Renamed and Moved** `"ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t"` to `"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL"` and moved to `EmailSettings`
- - **Removed:** `"AnalyticsUrl": ""` which was not used
- - **Removed:** `"UseLocalStorage": true` which is replaced by `"DriverName": "local"` in `FileSettings`
- - **Renamed and Moved:** `"StorageDirectory": "./data/"` to `Directory` and moved to `FileSettings`
- - **Renamed:** `"AllowedLoginAttempts": 10` to `"MaximumLoginAttempts": 10`
- - **Renamed, Reversed and Moved:** `"DisableEmailSignUp": false` renamed `"EnableSignUpWithEmail": true`, reversed meaning of `true`, and moved to `EmailSettings`
- - **Added:** `"EnableOAuthServiceProvider": false` to enable OAuth2 service provider functionality
- - **Added:** `"EnableIncomingWebhooks": false` to enable incoming webhooks feature
+ - Moved: `"SiteName": "Mattermost"` which was added to `TeamSettings`
+ - Removed: `"Mode" : "dev"` which deprecates a high level dev mode, now replaced by granular controls
+ - Renamed: `"AllowTesting" : false` to `"EnableTesting": false` which allows the use of `/loadtest` slash commands during development
+ - Removed: `"UseSSL": false` boolean replaced by `"ConnectionSecurity": ""` under `Security` with new options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`)
+ - Renamed: `"Port": "8065"` to `"ListenAddress": ":8065"` to define address on which to listen. Must be prepended with a colon.
+ - Removed: `"Version": "developer"` removed and version information now stored in `model/version.go`
+ - Removed: `"Shards": {}` which was not used
+ - Moved: `"InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"` to `EmailSettings`
+ - Moved: `"PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4"` to `FileSettings`
+ - Renamed and Moved `"ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t"` to `"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL"` and moved to `EmailSettings`
+ - Removed: `"AnalyticsUrl": ""` which was not used
+ - Removed: `"UseLocalStorage": true` which is replaced by `"DriverName": "local"` in `FileSettings`
+ - Renamed and Moved: `"StorageDirectory": "./data/"` to `Directory` and moved to `FileSettings`
+ - Renamed: `"AllowedLoginAttempts": 10` to `"MaximumLoginAttempts": 10`
+ - Renamed, Reversed and Moved: `"DisableEmailSignUp": false` renamed `"EnableSignUpWithEmail": true`, reversed meaning of `true`, and moved to `EmailSettings`
+ - Added: `"EnableOAuthServiceProvider": false` to enable OAuth2 service provider functionality
+ - Added: `"EnableIncomingWebhooks": false` to enable incoming webhooks feature
##### Team Settings
- Under `TeamSettings` in [`config.json`](./config/config.json):
- - **Renamed:** `"AllowPublicLink": true` renamed to `"EnablePublicLink": true` and moved to `FileSettings`
- - **Removed:** `AllowValetDefault` which was a guest account feature that is deprecated
- - **Removed:** `"TermsLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- - **Removed:** `"PrivacyLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- - **Removed:** `"AboutLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- - **Removed:** `"HelpLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- - **Removed:** `"ReportProblemLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- - **Removed:** `"TourLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- - **Removed:** `"DefaultThemeColor": "#2389D7"` removed since theme colors changed from 1 to 18, default theme color option may be added back later after theme color design stablizes
- - **Renamed:** `"DisableTeamCreation": false` to `"EnableUserCreation": true` and reversed
- - **Added:** ` "EnableUserCreation": true` added to disable ability to create new user accounts in the system
+ - Renamed: `"AllowPublicLink": true` renamed to `"EnablePublicLink": true` and moved to `FileSettings`
+ - Removed: `AllowValetDefault` which was a guest account feature that is deprecated
+ - Removed: `"TermsLink": "/static/help/configure_links.html"` removed since option didn't need configuration
+ - Removed: `"PrivacyLink": "/static/help/configure_links.html"` removed since option didn't need configuration
+ - Removed: `"AboutLink": "/static/help/configure_links.html"` removed since option didn't need configuration
+ - Removed: `"HelpLink": "/static/help/configure_links.html"` removed since option didn't need configuration
+ - Removed: `"ReportProblemLink": "/static/help/configure_links.html"` removed since option didn't need configuration
+ - Removed: `"TourLink": "/static/help/configure_links.html"` removed since option didn't need configuration
+ - Removed: `"DefaultThemeColor": "#2389D7"` removed since theme colors changed from 1 to 18, default theme color option may be added back later after theme color design stablizes
+ - Renamed: `"DisableTeamCreation": false` to `"EnableUserCreation": true` and reversed
+ - Added: ` "EnableUserCreation": true` added to disable ability to create new user accounts in the system
##### SSO Settings
- Under `SSOSettings` in [`config.json`](./config/config.json):
- - **Renamed Category:** `SSOSettings` to `GitLabSettings`
- - **Renamed:** `"Allow": false` to `"Enable": false` to enable GitLab SSO
+ - Renamed Category: `SSOSettings` to `GitLabSettings`
+ - Renamed: `"Allow": false` to `"Enable": false` to enable GitLab SSO
##### AWS Settings
- Under `AWSSettings` in [`config.json`](./config/config.json):
- This section was removed and settings moved to `FileSettings`
- - **Renamed and Moved:** `"S3AccessKeyId": ""` renamed `"AmazonS3AccessKeyId": "",` and moved to `FileSettings`
- - **Renamed and Moved:** `"S3SecretAccessKey": ""` renamed `"AmazonS3SecretAccessKey": "",` and moved to `FileSettings`
- - **Renamed and Moved:** `"S3Bucket": ""` renamed `"AmazonS3Bucket": "",` and moved to `FileSettings`
- - **Renamed and Moved:** `"S3Region": ""` renamed `"AmazonS3Region": "",` and moved to `FileSettings`
+ - Renamed and Moved: `"S3AccessKeyId": ""` renamed `"AmazonS3AccessKeyId": "",` and moved to `FileSettings`
+ - Renamed and Moved: `"S3SecretAccessKey": ""` renamed `"AmazonS3SecretAccessKey": "",` and moved to `FileSettings`
+ - Renamed and Moved: `"S3Bucket": ""` renamed `"AmazonS3Bucket": "",` and moved to `FileSettings`
+ - Renamed and Moved: `"S3Region": ""` renamed `"AmazonS3Region": "",` and moved to `FileSettings`
##### Image Settings
- Under `ImageSettings` in [`config.json`](./config/config.json):
- - **Renamed:** `"ImageSettings"` section to `"FileSettings"`
- - **Added:** `"DriverName" : "local"` to specify the file storage method, `amazons3` can also be used to setup S3
+ - Renamed: `"ImageSettings"` section to `"FileSettings"`
+ - Added: `"DriverName" : "local"` to specify the file storage method, `amazons3` can also be used to setup S3
##### EmailSettings
- Under `EmailSettings` in [`config.json`](./config/config.json):
- - **Removed:** `"ByPassEmail": "true"` which is replaced with `SendEmailNotifications` and `RequireEmailVerification`
- - **Added:** `"SendEmailNotifications" : "false"` to control whether email notifications are sent
- - **Added:** `"RequireEmailVerification" : "false"` to control if users need to verify their emails
- - **Replaced:** `"UseTLS": "false"` with `"ConnectionSecurity": ""` with options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`)
- - **Replaced:** `"UseStartTLS": "false"` with `"ConnectionSecurity": ""` with options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`)
+ - Removed: `"ByPassEmail": "true"` which is replaced with `SendEmailNotifications` and `RequireEmailVerification`
+ - Added: `"SendEmailNotifications" : "false"` to control whether email notifications are sent
+ - Added: `"RequireEmailVerification" : "false"` to control if users need to verify their emails
+ - Replaced: `"UseTLS": "false"` with `"ConnectionSecurity": ""` with options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ (`"StartTLS"`)
+ - Replaced: `"UseStartTLS": "false"` with `"ConnectionSecurity": ""` with options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ (`"StartTLS"`)
##### Privacy Settings
- Under `PrivacySettings` in [`config.json`](./config/config.json):
- - **Removed:** `"ShowPhoneNumber": "true"` which was not used
- - **Removed:** `"ShowSkypeId" : "true"` which was not used
+ - Removed: `"ShowPhoneNumber": "true"` which was not used
+ - Removed: `"ShowSkypeId" : "true"` which was not used
### Database Changes from v0.7 to v1.0
diff --git a/api/command.go b/api/command.go
index 52ff8fffd..54f863c48 100644
--- a/api/command.go
+++ b/api/command.go
@@ -22,6 +22,7 @@ var commands = []commandHandler{
joinCommand,
loadTestCommand,
echoCommand,
+ shrugCommand,
}
var echoSem chan bool
@@ -160,6 +161,34 @@ func echoCommand(c *Context, command *model.Command) bool {
return false
}
+func shrugCommand(c *Context, command *model.Command) bool {
+ cmd := "/shrug"
+
+ if !command.Suggest && strings.Index(command.Command, cmd) == 0 {
+ message := "¯\\_(ツ)_/¯"
+
+ parameters := strings.SplitN(command.Command, " ", 2)
+ if len(parameters) > 1 {
+ message += " " + parameters[1]
+ }
+
+ post := &model.Post{}
+ post.Message = message
+ post.ChannelId = command.ChannelId
+ if _, err := CreatePost(c, post, false); err != nil {
+ l4g.Error("Unable to create /shrug post post, err=%v", err)
+ return false
+ }
+ command.Response = model.RESP_EXECUTED
+ return true
+
+ } else if strings.Index(cmd, command.Command) == 0 {
+ command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Adds ¯\\_(ツ)_/¯ to your message, /shrug [message]"})
+ }
+
+ return false
+}
+
func joinCommand(c *Context, command *model.Command) bool {
// looks for "/join channel-name"
diff --git a/api/file.go b/api/file.go
index 142ef7ac7..94eea516a 100644
--- a/api/file.go
+++ b/api/file.go
@@ -23,6 +23,7 @@ import (
"image/jpeg"
"io"
"io/ioutil"
+ "mime"
"net/http"
"net/url"
"os"
@@ -331,9 +332,18 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=2592000, public")
+ var mimeType string
+ ext := filepath.Ext(filename)
+ if model.IsFileExtImage(ext) {
+ mimeType = model.GetImageMimeType(ext)
+ } else {
+ mimeType = mime.TypeByExtension(ext)
+ }
+
result := make(map[string]string)
result["filename"] = filename
result["size"] = size
+ result["mime"] = mimeType
w.Write([]byte(model.MapToJson(result)))
}
diff --git a/api/team.go b/api/team.go
index 666fd909b..d39d8ed60 100644
--- a/api/team.go
+++ b/api/team.go
@@ -108,7 +108,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
team.Name = model.CleanTeamName(team.Name)
- if err := team.IsValid(); err != nil {
+ if err := team.IsValid(*utils.Cfg.TeamSettings.RestrictTeamNames); err != nil {
c.Err = err
return
}
@@ -164,7 +164,7 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) {
teamSignup.Team.PreSave()
- if err := teamSignup.Team.IsValid(); err != nil {
+ if err := teamSignup.Team.IsValid(*utils.Cfg.TeamSettings.RestrictTeamNames); err != nil {
c.Err = err
return
}
@@ -379,11 +379,6 @@ func FindTeamByName(c *Context, name string, all string) bool {
return false
}
- if model.IsReservedTeamName(name) {
- c.Err = model.NewAppError("findTeamByName", "This URL is unavailable. Please try another.", "name="+name)
- return false
- }
-
if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
return false
} else {
diff --git a/config/config.json b/config/config.json
index 37109428d..7bac58df7 100644
--- a/config/config.json
+++ b/config/config.json
@@ -17,7 +17,8 @@
"MaxUsersPerTeam": 50,
"EnableTeamCreation": true,
"EnableUserCreation": true,
- "RestrictCreationToDomains": ""
+ "RestrictCreationToDomains": "",
+ "RestrictTeamNames": true
},
"SqlSettings": {
"DriverName": "mysql",
diff --git a/doc/help/Search.md b/doc/help/Search.md
new file mode 100644
index 000000000..f36e079bd
--- /dev/null
+++ b/doc/help/Search.md
@@ -0,0 +1,11 @@
+# Search
+
+The search box in Mattermost brings back results from any channel of which you’re a member. No results are returned from channels where you are not a member - even if they are open channels.
+
+Some things to know about search:
+
+- Multiple search terms are connected with “OR” by default. Typing in `Mattermost website` returns results containing “Mattermost” or “website”
+- You can use quotes to return search results for exact terms, like `"Mattermost website"` will only return messages containing the entire phrase `"Mattermost website"` and not return messages with only `Mattermost` or `website`
+- You can use the `*` character for wildcard searches that match within words. For example: Searching for `rea*` brings back messages containing `reach`, `reason` and other words starting with `rea`.
+
+Search in Mattermost uses the full text search features in MySQL and Postgres databases. Special cases that are not supported in default full text search, such as searching for IP addresses like `10.100.200.101`, can be added in future as the search feature evolves.
diff --git a/doc/install/Troubleshooting.md b/doc/install/Troubleshooting.md
index 6a7260ddf..21839c86f 100644
--- a/doc/install/Troubleshooting.md
+++ b/doc/install/Troubleshooting.md
@@ -1,12 +1,16 @@
-### Mattermost Troubleshooting
+# Mattermost Troubleshooting
#### Important notes
-1. **DO NOT manipulate the Mattermost database**
+##### **DO NOT manipulate the Mattermost database**
- In particular, DO NOT delete data from the database, as Mattermost is designed to stop working if data integrity has been compromised. The system is designed to archive content continously and generally assumes data is never deleted.
#### Common Issues
-1. Error message in logs when attempting to sign-up: `x509: certificate signed by unknown authority`
+##### Error message in logs when attempting to sign-up: `x509: certificate signed by unknown authority`
- This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost. You can resolve this issue by setting up a load balancer like Ngnix. A ticket exists to [add support for self-signed certificates in future](x509: certificate signed by unknown authority).
+
+##### Lost System Administrator account
+ - If the System Administrator account becomes unavailable, a person leaving the organization for example, you can set a new system admin from the commandline using `./platform -assign_role -team_name="yourteam" -email="you@example.com" -role="system_admin"`
+
diff --git a/doc/install/Upgrade-Guide.md b/doc/install/Upgrade-Guide.md
index e86cf8166..cecd45353 100644
--- a/doc/install/Upgrade-Guide.md
+++ b/doc/install/Upgrade-Guide.md
@@ -1,12 +1,14 @@
# Mattermost Upgrade Guide
-### Upgrading Mattermost v0.7 to v1.1
+### Upgrading Mattermost v0.7 to v1.1.1
-If you've manually changed Mattermost v0.7 configuration by updating the `config.json` file, you'll need to port those changes to Mattermost v1.1:
+_Note: [Mattermost v1.1.1](https://github.com/mattermost/platform/releases/tag/v1.1.1) is a special release of Mattermost v1.1 that upgrades the database to Mattermost v1.1 from EITHER Mattermost v0.7 or Mattermost v1.0. The following instructions are for upgrading from Mattermost v0.7 to v1.1.1 and skipping the upgrade to Mattermost v1.0._
+
+If you've manually changed Mattermost v0.7 configuration by updating the `config.json` file, you'll need to port those changes to Mattermost v1.1.1:
1. Go to the `config.json` file that you manually updated and note any differences from the [default `config.json` file in Mattermost 0.7](https://github.com/mattermost/platform/blob/v0.7.0/config/config.json).
-2. For each setting that you changed, check [the changelog documentation](https://github.com/mattermost/platform/blob/master/CHANGELOG.md#configjson-changes-from-v07-to-v10) on whether the configuration setting has changed between v0.7 and v1.1
+2. For each setting that you changed, check [the changelog documentation](https://github.com/mattermost/platform/blob/master/CHANGELOG.md#configjson-changes-from-v07-to-v10) on whether the configuration setting has changed between v0.7 and v1.1.1
3. Update your new [`config.json` file in Mattermost v1.1](https://github.com/mattermost/platform/blob/v1.1.0/config/config.json), based on your preferences and the changelog documentation above.
diff --git a/model/config.go b/model/config.go
index 3a39df2f1..216b1de86 100644
--- a/model/config.go
+++ b/model/config.go
@@ -122,6 +122,7 @@ type TeamSettings struct {
EnableTeamCreation bool
EnableUserCreation bool
RestrictCreationToDomains string
+ RestrictTeamNames *bool
}
type Config struct {
@@ -169,6 +170,11 @@ func (o *Config) SetDefaults() {
o.ServiceSettings.EnableSecurityFixAlert = new(bool)
*o.ServiceSettings.EnableSecurityFixAlert = true
}
+
+ if o.TeamSettings.RestrictTeamNames == nil {
+ o.TeamSettings.RestrictTeamNames = new(bool)
+ *o.TeamSettings.RestrictTeamNames = true
+ }
}
func (o *Config) IsValid() *AppError {
diff --git a/model/team.go b/model/team.go
index 584c78f8d..9da2cd5b2 100644
--- a/model/team.go
+++ b/model/team.go
@@ -97,7 +97,7 @@ func (o *Team) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
-func (o *Team) IsValid() *AppError {
+func (o *Team) IsValid(restrictTeamNames bool) *AppError {
if len(o.Id) != 26 {
return NewAppError("Team.IsValid", "Invalid Id", "")
@@ -127,7 +127,7 @@ func (o *Team) IsValid() *AppError {
return NewAppError("Team.IsValid", "Invalid URL Identifier", "id="+o.Id)
}
- if IsReservedTeamName(o.Name) {
+ if restrictTeamNames && IsReservedTeamName(o.Name) {
return NewAppError("Team.IsValid", "This URL is unavailable. Please try another.", "id="+o.Id)
}
diff --git a/model/team_test.go b/model/team_test.go
index fd2428f03..112d48a9d 100644
--- a/model/team_test.go
+++ b/model/team_test.go
@@ -21,45 +21,45 @@ func TestTeamJson(t *testing.T) {
func TestTeamIsValid(t *testing.T) {
o := Team{}
- if err := o.IsValid(); err == nil {
+ if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.Id = NewId()
- if err := o.IsValid(); err == nil {
+ if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.CreateAt = GetMillis()
- if err := o.IsValid(); err == nil {
+ if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.UpdateAt = GetMillis()
- if err := o.IsValid(); err == nil {
+ if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.Email = strings.Repeat("01234567890", 20)
- if err := o.IsValid(); err == nil {
+ if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.Email = "corey@hulen.com"
o.DisplayName = strings.Repeat("01234567890", 20)
- if err := o.IsValid(); err == nil {
+ if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.DisplayName = "1234"
o.Name = "ZZZZZZZ"
- if err := o.IsValid(); err == nil {
+ if err := o.IsValid(true); err == nil {
t.Fatal("should be invalid")
}
o.Name = "zzzzz"
o.Type = TEAM_OPEN
- if err := o.IsValid(); err != nil {
+ if err := o.IsValid(true); err != nil {
t.Fatal(err)
}
}
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 2d65435b0..380d979bd 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -5,6 +5,7 @@ package store
import (
"github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
)
type SqlTeamStore struct {
@@ -52,7 +53,7 @@ func (s SqlTeamStore) Save(team *model.Team) StoreChannel {
team.PreSave()
- if result.Err = team.IsValid(); result.Err != nil {
+ if result.Err = team.IsValid(*utils.Cfg.TeamSettings.RestrictTeamNames); result.Err != nil {
storeChannel <- result
close(storeChannel)
return
@@ -84,7 +85,7 @@ func (s SqlTeamStore) Update(team *model.Team) StoreChannel {
team.PreUpdate()
- if result.Err = team.IsValid(); result.Err != nil {
+ if result.Err = team.IsValid(*utils.Cfg.TeamSettings.RestrictTeamNames); result.Err != nil {
storeChannel <- result
close(storeChannel)
return
diff --git a/utils/config.go b/utils/config.go
index c1e3a8d2a..fd9856a67 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -182,6 +182,7 @@ func getClientConfig(c *model.Config) map[string]string {
props["SiteName"] = c.TeamSettings.SiteName
props["EnableTeamCreation"] = strconv.FormatBool(c.TeamSettings.EnableTeamCreation)
+ props["RestrictTeamNames"] = strconv.FormatBool(*c.TeamSettings.RestrictTeamNames)
props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider)
diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx
index da4299714..9ecd14a1e 100644
--- a/web/react/components/admin_console/team_settings.jsx
+++ b/web/react/components/admin_console/team_settings.jsx
@@ -31,6 +31,7 @@ export default class TeamSettings extends React.Component {
config.TeamSettings.RestrictCreationToDomains = ReactDOM.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
config.TeamSettings.EnableTeamCreation = ReactDOM.findDOMNode(this.refs.EnableTeamCreation).checked;
config.TeamSettings.EnableUserCreation = ReactDOM.findDOMNode(this.refs.EnableUserCreation).checked;
+ config.TeamSettings.RestrictTeamNames = ReactDOM.findDOMNode(this.refs.RestrictTeamNames).checked;
var MaxUsersPerTeam = 50;
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
@@ -209,6 +210,39 @@ export default class TeamSettings extends React.Component {
</div>
<div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='RestrictTeamNames'
+ >
+ {'Restrict Team Names: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='RestrictTeamNames'
+ value='true'
+ ref='RestrictTeamNames'
+ defaultChecked={this.props.config.TeamSettings.RestrictTeamNames}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='RestrictTeamNames'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.RestrictTeamNames}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, You cannot create a team name with reserved words like www, admin, support, test, channel, etc'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 12d1af6ff..435c7d542 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -13,8 +13,10 @@ const MsgTyping = require('./msg_typing.jsx');
const FileUpload = require('./file_upload.jsx');
const FilePreview = require('./file_preview.jsx');
const Utils = require('../utils/utils.jsx');
+
const Constants = require('../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
+const KeyCodes = Constants.KeyCodes;
export default class CreateComment extends React.Component {
constructor(props) {
@@ -25,6 +27,7 @@ export default class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
+ this.handleArrowUp = this.handleArrowUp.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
@@ -158,6 +161,26 @@ export default class CreateComment extends React.Component {
$('.post-right__scroll').perfectScrollbar('update');
this.setState({messageText: messageText});
}
+ handleArrowUp(e) {
+ if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
+ e.preventDefault();
+
+ const channelId = ChannelStore.getCurrentId();
+ const lastPost = PostStore.getCurrentUsersLatestPost(channelId, this.props.rootId);
+ if (!lastPost) {
+ return;
+ }
+
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.RECIEVED_EDIT_POST,
+ refocusId: '#reply_textbox',
+ title: 'Comment',
+ message: lastPost.message,
+ postId: lastPost.id,
+ channelId: lastPost.channel_id
+ });
+ }
+ }
handleUploadStart(clientIds) {
let draft = PostStore.getCommentDraft(this.props.rootId);
@@ -290,6 +313,7 @@ export default class CreateComment extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.commentMsgKeyPress}
+ onKeyDown={this.handleArrowUp}
messageText={this.state.messageText}
createMessage='Add a comment...'
initialText=''
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index 307c543a2..4d4e8390c 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -10,9 +10,12 @@ export default class FileAttachment extends React.Component {
super(props);
this.loadFiles = this.loadFiles.bind(this);
+ this.playGif = this.playGif.bind(this);
+ this.stopGif = this.stopGif.bind(this);
+ this.addBackgroundImage = this.addBackgroundImage.bind(this);
this.canSetState = false;
- this.state = {fileSize: -1};
+ this.state = {fileSize: -1, mime: '', playing: false, loading: false, format: ''};
}
componentDidMount() {
this.loadFiles();
@@ -28,15 +31,9 @@ export default class FileAttachment extends React.Component {
var filename = this.props.filename;
if (filename) {
- var fileInfo = utils.splitFileLocation(filename);
+ var fileInfo = this.getFileInfoFromName(filename);
var type = utils.getFileType(fileInfo.ext);
- // This is a temporary patch to fix issue with old files using absolute paths
- if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
- fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
- }
- fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path;
-
if (type === 'image') {
var self = this; // Need this reference since we use the given "this"
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) {
@@ -58,11 +55,7 @@ export default class FileAttachment extends React.Component {
$(imgDiv).addClass('normal');
}
- var re1 = new RegExp(' ', 'g');
- var re2 = new RegExp('\\(', 'g');
- var re3 = new RegExp('\\)', 'g');
- var url = path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
- $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')');
+ self.addBackgroundImage(name, path);
}
};
}(fileInfo.path, filename));
@@ -93,6 +86,75 @@ export default class FileAttachment extends React.Component {
return true;
}
+ playGif(e, filename) {
+ var img = new Image();
+ var fileUrl = utils.getFileUrl(filename);
+
+ this.setState({loading: true});
+ img.load(fileUrl);
+ img.onload = () => {
+ var state = {playing: true, loading: false};
+
+ switch (true) {
+ case img.width > img.height:
+ state.format = 'landscape';
+ break;
+ case img.height > img.width:
+ state.format = 'portrait';
+ break;
+ default:
+ state.format = 'quadrat';
+ break;
+ }
+
+ this.setState(state);
+
+ // keep displaying background image for a short moment while browser is
+ // loading gif, to prevent white background flashing through
+ setTimeout(() => this.removeBackgroundImage.bind(this)(filename), 100);
+ };
+ img.onError = () => this.setState({loading: false});
+
+ e.stopPropagation();
+ }
+ stopGif(e, filename) {
+ this.setState({playing: false});
+ this.addBackgroundImage(filename);
+ e.stopPropagation();
+ }
+ getFileInfoFromName(name) {
+ var fileInfo = utils.splitFileLocation(name);
+
+ // This is a temporary patch to fix issue with old files using absolute paths
+ if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
+ fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
+ }
+ fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path;
+
+ return fileInfo;
+ }
+ addBackgroundImage(name, path) {
+ var fileUrl = path;
+
+ if (name in this.refs) {
+ if (!path) {
+ fileUrl = this.getFileInfoFromName(name).path;
+ }
+
+ var imgDiv = ReactDOM.findDOMNode(this.refs[name]);
+ var re1 = new RegExp(' ', 'g');
+ var re2 = new RegExp('\\(', 'g');
+ var re3 = new RegExp('\\)', 'g');
+ var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
+
+ $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')');
+ }
+ }
+ removeBackgroundImage(name) {
+ if (name in this.refs) {
+ $(ReactDOM.findDOMNode(this.refs[name])).css('background-image', 'initial');
+ }
+ }
render() {
var filename = this.props.filename;
@@ -100,15 +162,71 @@ export default class FileAttachment extends React.Component {
var fileUrl = utils.getFileUrl(filename);
var type = utils.getFileType(fileInfo.ext);
- var thumbnail;
- if (type === 'image') {
- thumbnail = (
+ var playbackControls = '';
+ var loadedFile = '';
+ var loadingIndicator = '';
+ if (this.state.mime === 'image/gif') {
+ playbackControls = (
<div
- ref={filename}
- className='post__load'
- style={{backgroundImage: 'url(/static/images/load.gif)'}}
+ className='file-playback-controls play'
+ onClick={(e) => this.playGif(e, filename)}
+ >
+ {"►"}
+ </div>
+ );
+ }
+ if (this.state.playing) {
+ loadedFile = (
+ <img
+ className={'file__loaded ' + this.state.format}
+ src={fileUrl}
+ />
+ );
+ playbackControls = (
+ <div
+ className='file-playback-controls stop'
+ onClick={(e) => this.stopGif(e, filename)}
+ >
+ {"■"}
+ </div>
+ );
+ }
+ if (this.state.loading) {
+ loadingIndicator = (
+ <img
+ className='spinner file__loading'
+ src='/static/images/load.gif'
/>
);
+ playbackControls = '';
+ }
+
+ var thumbnail;
+ if (type === 'image') {
+ if (this.state.playing) {
+ thumbnail = (
+ <div
+ ref={filename}
+ className='post__load'
+ style={{backgroundImage: 'url(/static/images/load.gif)'}}
+ >
+ {playbackControls}
+ {loadedFile}
+ </div>
+ );
+ } else {
+ thumbnail = (
+ <div
+ ref={filename}
+ className='post__load'
+ style={{backgroundImage: 'url(/static/images/load.gif)'}}
+ >
+ {loadingIndicator}
+ {playbackControls}
+ {loadedFile}
+ </div>
+ );
+ }
} else {
thumbnail = <div className={'file-icon ' + utils.getIconClassName(type)}/>;
}
@@ -119,7 +237,7 @@ export default class FileAttachment extends React.Component {
filename,
function success(data) {
if (this.canSetState) {
- this.setState({fileSize: parseInt(data.size, 10)});
+ this.setState({fileSize: parseInt(data.size, 10), mime: data.mime});
}
}.bind(this),
function error() {}
diff --git a/web/react/components/file_upload_overlay.jsx b/web/react/components/file_upload_overlay.jsx
index 4fcee6cb0..d991dd625 100644
--- a/web/react/components/file_upload_overlay.jsx
+++ b/web/react/components/file_upload_overlay.jsx
@@ -12,9 +12,19 @@ export default class FileUploadOverlay extends React.Component {
return (
<div className={overlayClass}>
- <div>
- <i className='fa fa-upload'></i>
- <span>Drop a file to upload it.</span>
+ <div className='overlay__circle'>
+ <img
+ className='overlay__files'
+ src='/static/images/filesOverlay.png'
+ alt='Files'
+ />
+ <span><i className='fa fa-upload'></i>{'Drop a file to upload it.'}</span>
+ <img
+ className='overlay__logo'
+ src='/static/images/logoWhite.png'
+ width='100'
+ alt='Logo'
+ />
</div>
</div>
);
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index 67e4c9dd7..6d333aed6 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -40,10 +40,12 @@ export default class TeamSignupUrlPage extends React.Component {
return;
}
- for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) {
- if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) {
- this.setState({nameError: 'URL is taken or contains a reserved word'});
- return;
+ if (global.window.mm_config.RestrictTeamNames === 'true') {
+ for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) {
+ if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) {
+ this.setState({nameError: 'URL is taken or contains a reserved word'});
+ return;
+ }
}
}
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index 766edf0ac..92d7cd835 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -38,7 +38,10 @@ export default class ViewImageModal extends React.Component {
progress: progress,
images: {},
fileSizes: {},
- showFooter: false
+ fileMimes: {},
+ showFooter: false,
+ isPlaying: {},
+ isLoading: {}
};
}
handleNext(e) {
@@ -122,6 +125,36 @@ export default class ViewImageModal extends React.Component {
this.setState({loaded});
}
}
+ playGif(e, filename, fileUrl) {
+ var isLoading = this.state.isLoading;
+ var isPlaying = this.state.isPlaying;
+
+ isLoading[filename] = fileUrl;
+ this.setState({isLoading});
+
+ var img = new Image();
+ img.load(fileUrl);
+ img.onload = () => {
+ delete isLoading[filename];
+ isPlaying[filename] = fileUrl;
+ this.setState({isPlaying, isLoading});
+ };
+ img.onError = () => {
+ delete isLoading[filename];
+ this.setState({isLoading});
+ };
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ stopGif(e, filename) {
+ var isPlaying = this.state.isPlaying;
+ delete isPlaying[filename];
+ this.setState({isPlaying});
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
componentDidMount() {
$(window).on('keyup', this.handleKeyPress);
@@ -154,6 +187,10 @@ export default class ViewImageModal extends React.Component {
var fileType = Utils.getFileType(fileInfo.ext);
if (fileType === 'image') {
+ if (filename in this.state.isPlaying) {
+ return this.state.isPlaying[filename];
+ }
+
// This is a temporary patch to fix issue with old files using absolute paths
if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
@@ -189,12 +226,62 @@ export default class ViewImageModal extends React.Component {
var fileType = Utils.getFileType(fileInfo.ext);
if (fileType === 'image') {
+ if (!(filename in this.state.fileMimes)) {
+ Client.getFileInfo(
+ filename,
+ (data) => {
+ if (this.canSetState) {
+ var fileMimes = this.state.fileMimes;
+ fileMimes[filename] = data.mime;
+ this.setState(fileMimes);
+ }
+ },
+ () => {}
+ );
+ }
+
+ var playbackControls = '';
+ if (this.state.fileMimes[filename] === 'image/gif' && !(filename in this.state.isLoading)) {
+ if (filename in this.state.isPlaying) {
+ playbackControls = (
+ <div
+ className='file-playback-controls stop'
+ onClick={(e) => this.stopGif(e, filename)}
+ >
+ {"■"}
+ </div>
+ );
+ } else {
+ playbackControls = (
+ <div
+ className='file-playback-controls play'
+ onClick={(e) => this.playGif(e, filename, fileUrl)}
+ >
+ {"►"}
+ </div>
+ );
+ }
+ }
+
+ var loadingIndicator = '';
+ if (this.state.isLoading[filename] === fileUrl) {
+ loadingIndicator = (
+ <img
+ className='spinner file__loading'
+ src='/static/images/load.gif'
+ />
+ );
+ playbackControls = '';
+ }
+
// image files just show a preview of the file
content = (
<a
href={fileUrl}
target='_blank'
>
+ {loadingIndicator}
+ {playbackControls}
<img
style={{maxHeight: this.state.imgHeight}}
ref='image'
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index b073ca68d..4a9314b31 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -230,7 +230,7 @@ class PostStoreClass extends EventEmitter {
getPosts(channelId) {
return BrowserStore.getItem('posts_' + channelId);
}
- getCurrentUsersLatestPost(channelId) {
+ getCurrentUsersLatestPost(channelId, rootId) {
const userId = UserStore.getCurrentId();
var postList = makePostListNonNull(this.getPosts(channelId));
var i = 0;
@@ -239,8 +239,15 @@ class PostStoreClass extends EventEmitter {
for (i; i < len; i++) {
if (postList.posts[postList.order[i]].user_id === userId) {
- lastPost = postList.posts[postList.order[i]];
- break;
+ if (rootId) {
+ if (postList.posts[postList.order[i]].root_id === rootId || postList.posts[postList.order[i]].id === rootId) {
+ lastPost = postList.posts[postList.order[i]];
+ break;
+ }
+ } else {
+ lastPost = postList.posts[postList.order[i]];
+ break;
+ }
}
}
diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx
index 7b43e48b4..aabddcffd 100644
--- a/web/react/utils/emoticons.jsx
+++ b/web/react/utils/emoticons.jsx
@@ -133,7 +133,7 @@ export function handleEmoticons(text, tokens) {
const alias = `MM_EMOTICON${index}`;
tokens.set(alias, {
- value: `<img align="absmiddle" alt=${match} class="emoji" src=${getImagePathForEmoticon(name)} title=${match} />`,
+ value: `<img align="absmiddle" alt="${match}" class="emoji" src="${getImagePathForEmoticon(name)}" title="${match}" />`,
originalText: match
});
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index 2813798d2..7a4e70054 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -11,6 +11,7 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
super(options);
this.heading = this.heading.bind(this);
+ this.paragraph = this.paragraph.bind(this);
this.text = this.text.bind(this);
this.formattingOptions = formattingOptions;
@@ -53,11 +54,17 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
}
paragraph(text) {
+ let outText = text;
+
+ if (!('emoticons' in this.options) || this.options.emoticon) {
+ outText = TextFormatting.doFormatEmoticons(text);
+ }
+
if (this.formattingOptions.singleline) {
- return `<p class="markdown__paragraph-inline">${text}</p>`;
+ return `<p class="markdown__paragraph-inline">${outText}</p>`;
}
- return super.paragraph(text);
+ return super.paragraph(outText);
}
table(header, body) {
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index d79aeed68..5c2e68f1e 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -69,6 +69,15 @@ export function doFormatText(text, options) {
return output;
}
+export function doFormatEmoticons(text) {
+ const tokens = new Map();
+
+ let output = Emoticons.handleEmoticons(text, tokens);
+ output = replaceTokens(output, tokens);
+
+ return output;
+}
+
export function sanitizeHtml(text) {
let output = text;
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 53e328384..c72c3f32c 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -544,6 +544,7 @@ export function applyTheme(theme) {
if (theme.buttonBg) {
changeCss('.btn.btn-primary', 'background:' + theme.buttonBg, 1);
changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25), 1);
+ changeCss('.file-playback-controls', 'color:' + changeColor(theme.buttonBg, -0.25), 1);
}
if (theme.buttonColor) {
diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss
index 01057423d..d3ab3b9f8 100644
--- a/web/sass-files/sass/partials/_files.scss
+++ b/web/sass-files/sass/partials/_files.scss
@@ -133,12 +133,34 @@
height: 100%;
background-color: #FFF;
background-repeat: no-repeat;
+ overflow: hidden;
+ position: relative;
+ text-align: center;
&.small {
background-position: center;
}
&.normal {
background-position: top left;
}
+ .spinner.file__loading {
+ position: absolute;
+ left: 50%;
+ margin-left: -16px;
+ top: 50%;
+ margin-top: -16px;
+ }
+ .file__loaded {
+ max-width: initial;
+ &.landscape, &.quadrat {
+ height: 100px;
+ }
+ &.portrait {
+ width: 120px;
+ }
+ }
+ &:hover .file-playback-controls.stop {
+ @include opacity(1);
+ }
}
.post-image__thumbnail {
float: left;
@@ -215,3 +237,20 @@
}
}
}
+
+.file-playback-controls {
+ position: absolute;
+ right: 5px;
+ bottom: 0;
+ font-size: 22px;
+ cursor: pointer;
+ z-index: 2;
+ -webkit-transition: opacity 0.6s;
+ -moz-transition: opacity 0.6s;
+ -o-transition: opacity 0.6s;
+ transition: opacity 0.6s;
+
+ &.stop {
+ @include opacity(0);
+ }
+}
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index 5570b5ce4..1dcdbf348 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -228,11 +228,24 @@
background: #FFF;
display: table-cell;
vertical-align: middle;
+ position: relative;
+
+ &:hover .file-playback-controls.stop {
+ @include opacity(1);
+ }
}
img {
max-width: 100%;
max-height: 100%;
}
+ .spinner.file__loading {
+ z-index: 2;
+ position: absolute;
+ left: 50%;
+ margin-left: -16px;
+ top: 50%;
+ margin-top: -16px;
+ }
}
.modal-content{
box-shadow: none;
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 0f3cc0ef6..6ecc0d965 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -119,20 +119,52 @@ body.ios {
background-color: rgba(0, 0, 0, 0.6);
text-align: center;
color: #FFF;
- display: table;
- font-size: 1.7em;
+ font-size: em(20px);
font-weight: 600;
z-index: 6;
- > div {
- display: table-cell;
- vertical-align: middle;
+ &.right-file-overlay {
+ font-size: em(18px);
+ .overlay__circle {
+ width: 300px;
+ height: 300px;
+ margin: -150px 0 0 -150px;
+ }
+ .overlay__files {
+ margin: 60px auto 15px;
+ width: 150px;
+ }
}
- .fa {
+ .overlay__circle {
+ background: #111;
+ background: rgba(black, 0.7);
+ width: 370px;
+ height: 370px;
+ margin: -185px 0 0 -185px;
+ @include border-radius(500px);
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ }
+
+ .overlay__files {
display: block;
- font-size: 2em;
- margin: 0 0 0.3em;
+ margin: 75px auto 20px;
+ }
+
+ .overlay__logo {
+ position: absolute;
+ left: 50%;
+ bottom: 30px;
+ margin-left: -50px;
+ @include opacity(0.3);
+ }
+
+ .fa {
+ display: inline-block;
+ font-size: 1.1em;
+ margin-right: 8px;
}
}
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 09ac2047c..c8bb24f3a 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -199,9 +199,6 @@
}
@media screen and (max-width: 960px) {
- .center-file-overlay {
- font-size: 1.5em;
- }
.post {
.post-header .post-header-col.post-header__reply {
.comment-icon__container__hide {
@@ -278,8 +275,17 @@
display: block;
}
}
- .center-file-overlay {
- font-size: 1.3em;
+ .file-overlay {
+ font-size: em(18px);
+ .overlay__circle {
+ width: 300px;
+ height: 300px;
+ margin: -150px 0 0 -150px;
+ }
+ .overlay__files {
+ margin: 60px auto 15px;
+ width: 150px;
+ }
}
.date-separator, .new-separator {
&.hovered--after {
@@ -639,6 +645,9 @@
}
&.has-close {
.btn-close {
+ width: 40px;
+ text-align: center;
+ right: 0;
display: block;
@include opacity(0.5);
}
@@ -749,6 +758,10 @@
.post-comments {
padding: 9px 21px 10px 10px !important;
}
+
+ .post-image__column .post__image .file-playback-controls.stop, .image-wrapper > a .file-playback-controls.stop {
+ @include opacity(1);
+ }
}
@media screen and (max-width: 640px) {
.access-history__table {
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index c4591d7b6..bc53dc0e4 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -103,6 +103,9 @@
text-overflow: ellipsis;
margin-bottom: 0;
}
+ .input-group-addon {
+ background: transparent;
+ }
.radio {
label {
font-weight: 600;
diff --git a/web/static/images/filesOverlay.png b/web/static/images/filesOverlay.png
new file mode 100644
index 000000000..d24da7626
--- /dev/null
+++ b/web/static/images/filesOverlay.png
Binary files differ
diff --git a/web/static/images/logoWhite.png b/web/static/images/logoWhite.png
new file mode 100644
index 000000000..11bbd4632
--- /dev/null
+++ b/web/static/images/logoWhite.png
Binary files differ