summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore12
-rw-r--r--.travis.yml56
-rw-r--r--Makefile325
-rw-r--r--api/webhook.go113
-rw-r--r--config/config.json6
-rw-r--r--i18n/en.json2
-rw-r--r--i18n/pt.json108
-rw-r--r--model/version.go8
-rw-r--r--store/sql_post_store.go5
-rw-r--r--web/web_test.go3
-rw-r--r--webapp/action_creators/global_actions.jsx34
-rw-r--r--webapp/action_creators/websocket_actions.jsx227
-rw-r--r--webapp/components/about_build_modal.jsx6
-rw-r--r--webapp/components/access_history_modal.jsx8
-rw-r--r--webapp/components/activity_log_modal.jsx8
-rw-r--r--webapp/components/admin_console/license_settings.jsx64
-rw-r--r--webapp/components/channel_header.jsx1
-rw-r--r--webapp/components/channel_invite_button.jsx79
-rw-r--r--webapp/components/channel_invite_modal.jsx51
-rw-r--r--webapp/components/create_comment.jsx8
-rw-r--r--webapp/components/create_post.jsx7
-rw-r--r--webapp/components/filtered_user_list.jsx5
-rw-r--r--webapp/components/invite_member_modal.jsx7
-rw-r--r--webapp/components/logged_in.jsx18
-rw-r--r--webapp/components/more_direct_channels.jsx18
-rw-r--r--webapp/components/msg_typing.jsx78
-rw-r--r--webapp/components/navbar.jsx11
-rw-r--r--webapp/components/popover_list_members.jsx6
-rw-r--r--webapp/components/rhs_comment.jsx12
-rw-r--r--webapp/components/rhs_root_post.jsx5
-rw-r--r--webapp/components/rhs_thread.jsx42
-rw-r--r--webapp/components/search_results.jsx3
-rw-r--r--webapp/components/sidebar.jsx18
-rw-r--r--webapp/components/sidebar_right.jsx65
-rw-r--r--webapp/components/spinner_button.jsx48
-rw-r--r--webapp/components/team_settings_modal.jsx8
-rw-r--r--webapp/components/tutorial/tutorial_intro_screens.jsx19
-rw-r--r--webapp/components/tutorial/tutorial_tip.jsx11
-rw-r--r--webapp/components/user_list.jsx22
-rw-r--r--webapp/components/user_list_row.jsx9
-rw-r--r--webapp/components/user_settings/user_settings_advanced.jsx46
-rw-r--r--webapp/components/user_settings/user_settings_display.jsx40
-rw-r--r--webapp/components/user_settings/user_settings_modal.jsx22
-rw-r--r--webapp/i18n/en.json16
-rw-r--r--webapp/i18n/es.json8
-rw-r--r--webapp/i18n/pt.json56
-rw-r--r--webapp/package.json7
-rw-r--r--webapp/root.jsx42
-rw-r--r--webapp/sass/components/_modal.scss1
-rw-r--r--webapp/sass/components/_spinner-button.scss8
-rw-r--r--webapp/sass/layout/_headers.scss4
-rw-r--r--webapp/sass/layout/_navigation.scss2
-rw-r--r--webapp/sass/layout/_sidebar-left.scss1
-rw-r--r--webapp/sass/routes/_admin-console.scss17
-rw-r--r--webapp/stores/browser_store.jsx47
-rw-r--r--webapp/stores/error_store.jsx12
-rw-r--r--webapp/stores/notificaiton_store.jsx98
-rw-r--r--webapp/stores/preference_store.jsx113
-rw-r--r--webapp/stores/socket_store.jsx343
-rw-r--r--webapp/stores/user_store.jsx50
-rw-r--r--webapp/stores/user_typing_store.jsx108
-rw-r--r--webapp/utils/async_client.jsx17
-rw-r--r--webapp/utils/constants.jsx32
-rw-r--r--webapp/utils/utils.jsx12
64 files changed, 1459 insertions, 1179 deletions
diff --git a/.gitignore b/.gitignore
index 4c343021e..422c6d5f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,14 +9,15 @@ npm-debug.log
web/static/js/bundle*.js
web/static/js/bundle*.js.map
web/static/js/libs*.js
-.npminstall
config/active.dat
+# Enteprise imports file
+imports.go
+
# Build Targets
-.prepare
-.prepare-go
-.prepare-jsx
+.prebuild
+.npminstall
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
@@ -69,6 +70,3 @@ api/data/*
.agignore
.ctags
tags
-
-model/version.go
-model/version.go.bak
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 71dcbefbc..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,56 +0,0 @@
-language: generic
-sudo: required
-services:
-- docker
-env:
-- TRAVIS_DB=mysql
-- TRAVIS_DB=postgres
-before_install:
- - docker run --name mattermost-mysql -e MYSQL_ROOT_PASSWORD=mostest -e MYSQL_USER=mmuser -e MYSQL_PASSWORD=mostest -e MYSQL_DATABASE=mattermost_test -d mysql:5.7
- - docker run --name mattermost-postgres -e POSTGRES_USER=mmuser -e POSTGRES_PASSWORD=mostest -d postgres:9.4
- - sleep 10
- - docker exec mattermost-postgres psql -c 'create database mattermost_test ;' -U postgres
- - docker exec mattermost-postgres psql -c 'grant all privileges on database "mattermost_test" to mmuser ;' -U postgres
-script: make dist-travis
-addons:
- hosts:
- - 127.0.0.1 dockerhost
-after_success:
- - sudo chown -R `whoami` dist
- - sudo chmod -R 777 dist
- - cd dist && curl -F "pr_number=$TRAVIS_PULL_REQUEST" -F "file=@mattermost.tar.gz" mattermod.mattermost.com:8087/upload_pr_build
- - md5sum mattermost.tar.gz > mm.md5
- - sha1sum mattermost.tar.gz > mm.sha1
- - sha256sum mattermost.tar.gz > mm.sha256
- - cat mm.md5 mm.sha1 mm.sha256
- - cd ..
-before_deploy:
- - sudo rm -rf dist/mattermost
- - rvm 1.9.3 do gem install mime-types -v 2.6.2
-deploy:
-# Github releases, builds only on tags
- - provider: releases
- api_key:
- secure: ma8Y0oimU+LB6LTAh8to2E1/ghaDPhcsAFXBrODsHpd4JgxA6HYoEwSEBCJFHSpu/JteclsxSTfp9hcuzw/IOtlwlSAiVoBZ60s24MRKTIAQNtrJ4QrX5wyfAZi+Bcuk/E8NynmoIW5qpaElSAdjgocyjAJIQ5ChMEztglL0cAEBXQRWbWMqSZ0hVLPrKDCIkWIyv3pFxqdLOxktkzxW07r2dlT0hppXR3dCaPJo0nelArS2H3LdN/3Iv6cAddfS27RaZkqDj/PDh6OZr4EguC99TxlVNChIr7nPr3/OiAssbkvEnhlSLeABFO9+7KfutL2WhAjpFXTjtPVq6Qalc8UW0K0gxq//sVfhb1MzjenmdOf06uB2bilQ8kgwHo7dDdRZBqqAtxQ6Q0Ht3SFMj6v/1zVD3s+YX/kWCEbUTHm6r2G/eF794ozcJyU+6j1L8hm6mvf8Mr9XCqBfgpZy6FCLX+9OKdMvX2jY8reo3Xz1PA9R6yzhN08vjku+jW+fsoYrBLd0fY1UGK2uOuvBByCeJzXupd3YpBMjEyRupVxqEj7K0GWOJeml65mkqKSNsHdDSeSjMpb8mwneZyTbdjsxCFQRLcLgpAajFrkk4G2Yz3KfhXSo29XKEGX+EbY5NuP8KmDsBsguPI0zfwv/co0hAY8PIIcehxcdoR9Vb2c=
- file: dist/mattermost.tar.gz
- skip_cleanup: true
- on:
- repo: mattermost/platform
- tags: true
- condition: $TRAVIS_DB = mysql
-
-# S3 deploy for latest master
- - provider: s3
- access_key_id: AKIAJCO3KJYEGWJIKDIQ
- secret_access_key:
- secure: p66X2tJBmKgtcVyPtGgkAwW29IiRojqGA39RjCJkIWNTJ0e/9JvBOiMS2c4a7I4aOads38rsthwdaigBWagDWNH7bGsEZN7B0TszZuFAuU+XGjU5A66MIOfFfzbUg8AnByysr+XG5/bknFIrP/XhM2fbRr6gbYrFUK7TNkpgjFs5u3BzUrz2iTAV8uOpSJqKSnaf0pTZk1EywOK/X8W8ViIjc7Di3FzQcqIW9K3D27N+3rVsv8SRT1hWASVlnG6aThqqebiM8FCGCzAYVgQb3h3Wu8JT5fIz7Qo7A6siVRwNBwWwzP8HkGoinEK32Wsj/fDXk27vjpFQO/+9sV0xfcTbIZA6MnuYWF4rHOT59KcshCWCD3V0FopX57p/dtOzM9+6lxIctAT++izxWoZit/5c5A4633iY1d+RMeTko1POix6MSlxPMRHZUFwSXROgFuWWRpyD6TlUTCST9/wTTd0WDPklAAiYcnuEPW3qCnw0r0xkrA4AwWUXqXdAIwDt5bA27KcjRyY4Fofv9NxH09BNuBTXNPrvnYPZMmaKrv+HOX3NFTreuV6+5LJdhYUxYSBvSWo1jeWIQ5Q9RUdTU0PqmKpMhJKbKey/S4gxCXHg2HR8DwLCcbIZcvneF9yPEAT71YA6zpLKoPVSwWwH97huKSzjpic/RUfFXQOcgCQ=
- bucket: mattermost-travis-master
- local_dir: dist
- acl: public_read
- region: us-east-1
- skip_cleanup: true
- detect_encoding: true
- on:
- repo: mattermost/platform
- branch: master
- condition: $TRAVIS_DB = mysql
diff --git a/Makefile b/Makefile
index eb0bdc06f..492275eca 100644
--- a/Makefile
+++ b/Makefile
@@ -1,47 +1,47 @@
-.PHONY: all dist dist-local dist-travis start-docker build-server package build-client test travis-init build-container stop-docker clean-docker clean nuke run run-client run-server stop stop-client stop-server setup-mac cleandb docker-build docker-run restart-server
+.PHONY: build package run stop run-client run-server stop-client stop-server restart-server restart-client start-docker clean-dist clean nuke check-style check-unit-tests test dist setup-mac prepare-enteprise
-GOPATH ?= $(GOPATH:)
-GOFLAGS ?= $(GOFLAGS:)
+# Build Flags
BUILD_NUMBER ?= $(BUILD_NUMBER:)
BUILD_DATE = $(shell date -u)
BUILD_HASH = $(shell git rev-parse HEAD)
-
-ENTERPRISE_DIR ?= ../enterprise
-BUILD_ENTERPRISE ?= true
-
-GO=$(GOPATH)/bin/godep go
-ESLINT=node_modules/eslint/bin/eslint.js
-
+# If we don't set the build number it defaults to dev
ifeq ($(BUILD_NUMBER),)
BUILD_NUMBER := dev
endif
-
-ifeq ($(TRAVIS_BUILD_NUMBER),)
- BUILD_NUMBER := dev
+BUILD_ENTERPRISE_DIR ?= ../enterprise
+BUILD_ENTERPRISE ?= true
+BUILD_ENTERPRISE_READY = false
+ifneq ($(wildcard $(BUILD_ENTERPRISE_DIR)/.),)
+ ifeq ($(BUILD_ENTERPRISE),true)
+ BUILD_ENTERPRISE_READY = true
+ else
+ BUILD_ENTERPRISE_READY = false
+ endif
else
- BUILD_NUMBER := $(TRAVIS_BUILD_NUMBER)
+ BUILD_ENTERPRISE_READY = false
endif
+BUILD_WEBAPP_DIR = ./webapp
+# Golang Flags
+GOPATH ?= $(GOPATH:)
+GOFLAGS ?= $(GOFLAGS:)
+GO=$(GOPATH)/bin/godep go
+GO_LINKER_FLAGS ?= -ldflags \
+ "-X github.com/mattermost/platform/model.BuildNumber=$(BUILD_NUMBER)\
+ -X 'github.com/mattermost/platform/model.BuildDate=$(BUILD_DATE)'\
+ -X github.com/mattermost/platform/model.BuildHash=$(BUILD_HASH)\
+ -X github.com/mattermost/platform/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)"
+
+# Output paths
DIST_ROOT=dist
DIST_PATH=$(DIST_ROOT)/mattermost
+# Tests
TESTS=.
-DOCKERNAME ?= mm-dev
-DOCKER_CONTAINER_NAME ?= mm-test
-
-all: dist-local
-
-dist: | build-server build-client go-test package
- mv ./model/version.go.bak ./model/version.go
- @if [ "$(BUILD_ENTERPRISE)" = "true" ] && [ -d "$(ENTERPRISE_DIR)" ]; then \
- mv ./mattermost.go.bak ./mattermost.go; \
- mv ./config/config.json.bak ./config/config.json 2> /dev/null || true; \
- fi
-
-dist-local: | start-docker dist
+all: dist
-dist-travis: | travis-init build-container
+dist: | check-style test package
start-docker:
@echo Starting docker containers
@@ -66,107 +66,6 @@ start-docker:
sleep 10; \
fi
-build-server:
- @echo Building mattermost server
-
- rm -Rf $(DIST_ROOT)
- $(GO) clean $(GOFLAGS) -i ./...
-
- @echo GOFMT
- $(eval GOFMT_OUTPUT := $(shell gofmt -d -s api/ model/ store/ utils/ manualtesting/ mattermost.go 2>&1))
- @echo "$(GOFMT_OUTPUT)"
- @if [ ! "$(GOFMT_OUTPUT)" ]; then \
- echo "gofmt sucess"; \
- else \
- echo "gofmt failure"; \
- exit 1; \
- fi
-
- cp ./model/version.go ./model/version.go.bak
- sed -i'.make_mac_work' 's|_BUILD_NUMBER_|$(BUILD_NUMBER)|g' ./model/version.go
- sed -i'.make_mac_work' 's|_BUILD_DATE_|$(BUILD_DATE)|g' ./model/version.go
- sed -i'.make_mac_work' 's|_BUILD_HASH_|$(BUILD_HASH)|g' ./model/version.go
-
- @if [ "$(BUILD_ENTERPRISE)" = "true" ] && [ -d "$(ENTERPRISE_DIR)" ]; then \
- cp ./config/config.json ./config/config.json.bak; \
- jq -s '.[0] * .[1]' ./config/config.json $(ENTERPRISE_DIR)/config/enterprise-config-additions.json > config.json.tmp; \
- mv config.json.tmp ./config/config.json; \
- sed -e '/\/\/ENTERPRISE_IMPORTS/ {' -e 'r $(ENTERPRISE_DIR)/imports' -e 'd' -e '}' -i'.bak' mattermost.go; \
- sed -i'.make_mac_work' 's|_BUILD_ENTERPRISE_READY_|true|g' ./model/version.go; \
- else \
- sed -i'.make_mac_work' 's|_BUILD_ENTERPRISE_READY_|false|g' ./model/version.go; \
- fi
-
- rm ./model/version.go.make_mac_work
-
- $(GO) build $(GOFLAGS) ./...
- $(GO) generate $(GOFLAGS) ./...
- $(GO) install $(GOFLAGS) ./...
-
-package:
- @ echo Packaging mattermost
-
- mkdir -p $(DIST_PATH)/bin
- cp $(GOPATH)/bin/platform $(DIST_PATH)/bin
-
- cp -RL config $(DIST_PATH)/config
- cp -RL fonts $(DIST_PATH)/fonts
- touch $(DIST_PATH)/config/build.txt
- echo $(BUILD_NUMBER) | tee -a $(DIST_PATH)/config/build.txt
-
- mkdir -p $(DIST_PATH)/logs
-
- mkdir -p $(DIST_PATH)/webapp/dist
- cp -RL webapp/dist $(DIST_PATH)/webapp
-
- cp -RL templates $(DIST_PATH)
-
- cp -RL i18n $(DIST_PATH)
-
- cp build/MIT-COMPILED-LICENSE.md $(DIST_PATH)
- cp NOTICE.txt $(DIST_PATH)
- cp README.md $(DIST_PATH)
-
- mv $(DIST_PATH)/webapp/dist/bundle.js $(DIST_PATH)/webapp/dist/bundle-$(BUILD_NUMBER).js
- sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).js|g' $(DIST_PATH)/webapp/dist/root.html
- rm $(DIST_PATH)/webapp/dist/root.html.bak
-
- @if [ "$(BUILD_ENTERPRISE)" = "true" ] && [ -d "$(ENTERPRISE_DIR)" ]; then \
- sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv'; \
- fi
-
- tar -C dist -czf $(DIST_PATH).tar.gz mattermost
-
-build-client:
- mkdir -p webapp/dist/files
- cd webapp && make build
-
-go-test:
- $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s ./api || exit 1
- $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=12s ./model || exit 1
- $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./store || exit 1
- $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./utils || exit 1
- $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./web || exit 1
-
-test: | start-docker .prepare-go go-test
-
-travis-init:
- @echo Setting up enviroment for travis
-
- if [ "$(TRAVIS_DB)" = "postgres" ]; then \
- sed -i'.bak' 's|mysql|postgres|g' config/config.json; \
- sed -i'.bak2' 's|mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8|postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable\&connect_timeout=10|g' config/config.json; \
- fi
-
- if [ "$(TRAVIS_DB)" = "mysql" ]; then \
- sed -i'.bak' 's|mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8|mmuser:mostest@tcp(mysql:3306)/mattermost_test?charset=utf8mb4,utf8|g' config/config.json; \
- fi
-
-build-container:
- @echo Building in container
-
- cd .. && docker run -e TRAVIS_BUILD_NUMBER=$(TRAVIS_BUILD_NUMBER) --link mattermost-mysql:mysql --link mattermost-postgres:postgres -v `pwd`:/go/src/github.com/mattermost mattermost/builder:latest
-
stop-docker:
@echo Stopping docker containers
@@ -195,92 +94,144 @@ clean-docker:
docker rm -v mattermost-postgres > /dev/null; \
fi
-clean: stop-docker
- rm -Rf $(DIST_ROOT)
- go clean $(GOFLAGS) -i ./...
+check-style:
+ @echo Running GOFMT
+ $(eval GOFMT_OUTPUT := $(shell gofmt -d -s api/ model/ store/ utils/ manualtesting/ einterfaces/ mattermost.go 2>&1))
+ @echo "$(GOFMT_OUTPUT)"
+ @if [ ! "$(GOFMT_OUTPUT)" ]; then \
+ echo "gofmt sucess"; \
+ else \
+ echo "gofmt failure"; \
+ exit 1; \
+ fi
- cd webapp && make clean
+test: start-docker
+ $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s ./api || exit 1
+ $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=12s ./model || exit 1
+ $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./store || exit 1
+ $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./utils || exit 1
+ $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./web || exit 1
- rm -rf api/data
- rm -rf logs
+.prebuild:
+ @echo Preparation for running go code
+ go get $(GOFLAGS) github.com/tools/godep
- rm -rf Godeps/_workspace/pkg/
+ touch $@
- rm -f mattermost.log
- rm -f .prepare-go .prepare-jsx
+prepare-enterprise:
+ifeq ($(BUILD_ENTERPRISE_READY),true)
+ @echo Enterprise build selected, perparing
+ cp $(BUILD_ENTERPRISE_DIR)/imports.go .
+endif
-nuke: | clean clean-docker
- rm -rf data
+build: .prebuild prepare-enterprise
+ @echo Building mattermost server
-.prepare-go:
- @echo Preparation for running go code
- go get $(GOFLAGS) github.com/tools/godep
+ $(GO) clean $(GOFLAGS) -i ./...
+ $(GO) install $(GOFLAGS) $(GO_LINKER_FLAGS) ./...
- touch $@
+build-client:
+ @echo Building mattermost web app
-run: | start-docker run-server run-client
+ cd $(BUILD_WEBAPP_DIR) && make build
-run-server: .prepare-go
- @echo Starting go web server
- mkdir -p webapp/dist/files
- $(GO) run $(GOFLAGS) mattermost.go -config=config.json &
-run-client:
- @echo Starting client
+package: build build-client
+ @ echo Packaging mattermost
- cd webapp && make run
+ # Remove any old files
+ rm -Rf $(DIST_ROOT)
- @if [ "$(BUILD_ENTERPRISE)" = "true" ] && [ -d "$(ENTERPRISE_DIR)" ]; then \
- cp ./config/config.json ./config/config.json.bak; \
- jq -s '.[0] * .[1]' ./config/config.json $(ENTERPRISE_DIR)/config/enterprise-config-additions.json > config.json.tmp; \
- mv config.json.tmp ./config/config.json; \
- sed -e '/\/\/ENTERPRISE_IMPORTS/ {' -e 'r $(ENTERPRISE_DIR)/imports' -e 'd' -e '}' -i'.bak' mattermost.go; \
- sed -i'.bak' 's|_BUILD_ENTERPRISE_READY_|true|g' ./model/version.go; \
- else \
- sed -i'.bak' 's|_BUILD_ENTERPRISE_READY_|false|g' ./model/version.go; \
- fi
+ # Create needed directories
+ mkdir -p $(DIST_PATH)/bin
+ mkdir -p $(DIST_PATH)/logs
-stop: stop-server stop-client
- @if [ $(shell docker ps -a | grep -ci ${DOCKER_CONTAINER_NAME}) -eq 1 ]; then \
- echo removing dev docker container; \
- docker stop ${DOCKER_CONTAINER_NAME} > /dev/null; \
- docker rm -v ${DOCKER_CONTAINER_NAME} > /dev/null; \
- fi
+ # Copy binary
+ cp $(GOPATH)/bin/platform $(DIST_PATH)/bin
- @if [ "$(BUILD_ENTERPRISE)" = "true" ] && [ -d "$(ENTERPRISE_DIR)" ]; then \
- mv ./config/config.json.bak ./config/config.json 2> /dev/null || true; \
- mv ./mattermost.go.bak ./mattermost.go 2> /dev/null || true; \
- mv ./model/version.go.bak ./model/version.go 2> /dev/null || true; \
- fi
+ # Resource directories
+ cp -RL config $(DIST_PATH)
+ cp -RL fonts $(DIST_PATH)
+ cp -RL templates $(DIST_PATH)
+ cp -RL i18n $(DIST_PATH)
+
+ # Package webapp
+ mkdir -p $(DIST_PATH)/webapp/dist
+ cp -RL $(BUILD_WEBAPP_DIR)/dist $(DIST_PATH)/webapp
+ mv $(DIST_PATH)/webapp/dist/bundle.js $(DIST_PATH)/webapp/dist/bundle-$(BUILD_NUMBER).js
+ sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).js|g' $(DIST_PATH)/webapp/dist/root.html
+ rm $(DIST_PATH)/webapp/dist/root.html.bak
+
+ # Help files
+ifeq ($(BUILD_ENTERPRISE_READY),true)
+ cp $(BUILD_ENTERPRISE_DIR)/ENTERPRISE-EDITION-LICENSE.txt $(DIST_PATH)
+else
+ cp build/MIT-COMPILED-LICENSE.md $(DIST_PATH)
+endif
+ cp NOTICE.txt $(DIST_PATH)
+ cp README.md $(DIST_PATH)
+
+ # Create package
+ tar -C dist -czf $(DIST_PATH).tar.gz mattermost
+
+run-server: prepare-enterprise start-docker
+ @echo Running mattermost for development
+
+ mkdir -p $(BUILD_WEBAPP_DIR)/dist/files
+ $(GO) run $(GOFLAGS) $(GO_LINKER_FLAGS) *.go &
+
+run-client:
+ @echo Running mattermost client for development
+
+ cd $(BUILD_WEBAPP_DIR) && make run
+
+run: run-server run-client
stop-server:
- @for PID in $$(ps -ef | grep "go run [m]attermost.go" | awk '{ print $$2 }'); do \
+ @echo Stopping mattermost
+
+ @for PID in $$(ps -ef | grep "[g]o run" | awk '{ print $$2 }'); do \
echo stopping go $$PID; \
kill $$PID; \
done
- @for PID in $$(ps -ef | grep "go-build.*/[m]attermost" | awk '{ print $$2 }'); do \
+ @for PID in $$(ps -ef | grep "[g]o-build" | awk '{ print $$2 }'); do \
echo stopping mattermost $$PID; \
kill $$PID; \
done
stop-client:
- cd webapp && make stop
+ @echo Stopping mattermost client
-restart-server: stop-server run-server
+ cd $(BUILD_WEBAPP_DIR) && make stop
-setup-mac:
- echo $$(boot2docker ip 2> /dev/null) dockerhost | sudo tee -a /etc/hosts
-cleandb:
- @if [ $(shell docker ps -a | grep -ci mattermost-mysql) -eq 1 ]; then \
- docker stop mattermost-mysql > /dev/null; \
- docker rm -v mattermost-mysql > /dev/null; \
- fi
+stop: stop-server stop-client
+
+restart-server: | stop-server run-server
+
+restart-client: | stop-client run-client
+
+clean: stop-docker
+ @echo Cleaning
+
+ rm -Rf $(DIST_ROOT)
+ go clean $(GOFLAGS) -i ./...
+
+ cd $(BUILD_WEBAPP_DIR) && make clean
-docker-build: stop
- docker build -t ${DOCKERNAME} -f docker/local/Dockerfile .
+ rm -rf api/data
+ rm -rf logs
+
+ rm -rf Godeps/_workspace/pkg/
+
+ rm -f mattermost.log
+ rm -f .prepare-go
+
+nuke: clean clean-docker
+ @echo BOOM
-docker-run: docker-build
- docker run --name ${DOCKER_CONTAINER_NAME} -d --publish 8065:80 ${DOCKERNAME}
+ rm -rf data
+setup-mac:
+ echo $$(boot2docker ip 2> /dev/null) dockerhost | sudo tee -a /etc/hosts
diff --git a/api/webhook.go b/api/webhook.go
index c0f8ea506..fe1aa1175 100644
--- a/api/webhook.go
+++ b/api/webhook.go
@@ -4,11 +4,14 @@
package api
import (
+ "net/http"
+ "strings"
+
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
- "net/http"
)
func InitWebhook(r *mux.Router) {
@@ -23,6 +26,12 @@ func InitWebhook(r *mux.Router) {
sr.Handle("/outgoing/regen_token", ApiUserRequired(regenOutgoingHookToken)).Methods("POST")
sr.Handle("/outgoing/delete", ApiUserRequired(deleteOutgoingHook)).Methods("POST")
sr.Handle("/outgoing/list", ApiUserRequired(getOutgoingHooks)).Methods("GET")
+
+ sr.Handle("/{id:[A-Za-z0-9]+}", ApiAppHandler(incomingWebhook)).Methods("POST")
+
+ // Old route. Remove eventually.
+ mr := Srv.Router
+ mr.Handle("/hooks/{id:[A-Za-z0-9]+}", ApiAppHandler(incomingWebhook)).Methods("POST")
}
func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -330,3 +339,105 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request)
w.Write([]byte(result.Data.(*model.OutgoingWebhook).ToJson()))
}
}
+
+func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
+ c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.disabled.app_error", nil, "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ params := mux.Vars(r)
+ id := params["id"]
+
+ hchan := Srv.Store.Webhook().GetIncoming(id)
+
+ r.ParseForm()
+
+ var parsedRequest *model.IncomingWebhookRequest
+ contentType := r.Header.Get("Content-Type")
+ if strings.Split(contentType, "; ")[0] == "application/json" {
+ parsedRequest = model.IncomingWebhookRequestFromJson(r.Body)
+ } else {
+ parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload")))
+ }
+
+ if parsedRequest == nil {
+ c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.parse.app_error", nil, "")
+ return
+ }
+
+ text := parsedRequest.Text
+ if len(text) == 0 && parsedRequest.Attachments == nil {
+ c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.text.app_error", nil, "")
+ return
+ }
+
+ channelName := parsedRequest.ChannelName
+ webhookType := parsedRequest.Type
+
+ //attachments is in here for slack compatibility
+ if parsedRequest.Attachments != nil {
+ if len(parsedRequest.Props) == 0 {
+ parsedRequest.Props = make(model.StringInterface)
+ }
+ parsedRequest.Props["attachments"] = parsedRequest.Attachments
+ webhookType = model.POST_SLACK_ATTACHMENT
+ }
+
+ var hook *model.IncomingWebhook
+ if result := <-hchan; result.Err != nil {
+ c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.invalid.app_error", nil, "err="+result.Err.Message)
+ return
+ } else {
+ hook = result.Data.(*model.IncomingWebhook)
+ }
+
+ var channel *model.Channel
+ var cchan store.StoreChannel
+
+ if len(channelName) != 0 {
+ if channelName[0] == '@' {
+ if result := <-Srv.Store.User().GetByUsername(hook.TeamId, channelName[1:]); result.Err != nil {
+ c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message)
+ return
+ } else {
+ channelName = model.GetDMNameFromIds(result.Data.(*model.User).Id, hook.UserId)
+ }
+ } else if channelName[0] == '#' {
+ channelName = channelName[1:]
+ }
+
+ cchan = Srv.Store.Channel().GetByName(hook.TeamId, channelName)
+ } else {
+ cchan = Srv.Store.Channel().Get(hook.ChannelId)
+ }
+
+ overrideUsername := parsedRequest.Username
+ overrideIconUrl := parsedRequest.IconURL
+
+ if result := <-cchan; result.Err != nil {
+ c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message)
+ return
+ } else {
+ channel = result.Data.(*model.Channel)
+ }
+
+ pchan := Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId)
+
+ // create a mock session
+ c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false}
+
+ if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
+ c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "")
+ return
+ }
+
+ if _, err := CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil {
+ c.Err = err
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/plain")
+ w.Write([]byte("ok"))
+}
diff --git a/config/config.json b/config/config.json
index 1735ca293..65a61bb72 100644
--- a/config/config.json
+++ b/config/config.json
@@ -87,8 +87,8 @@
"InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
"SendPushNotifications": false,
- "PushNotificationContents": "generic",
- "PushNotificationServer": ""
+ "PushNotificationServer": "",
+ "PushNotificationContents": "generic"
},
"RateLimitSettings": {
"EnableRateLimiter": true,
@@ -146,4 +146,4 @@
"Directory": "./data/",
"EnableDaily": false
}
-}
+} \ No newline at end of file
diff --git a/i18n/en.json b/i18n/en.json
index 6ba877f56..3180e8dbc 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -3189,7 +3189,7 @@
},
{
"id": "store.sql_user.save.max_accounts.app_error",
- "translation": "This team has reached the maxmium number of allowed accounts. Contact your systems administrator to set a higher limit."
+ "translation": "This team has reached the maximum number of allowed accounts. Contact your systems administrator to set a higher limit."
},
{
"id": "store.sql_user.save.member_count.app_error",
diff --git a/i18n/pt.json b/i18n/pt.json
index c59eb896b..15bc42835 100644
--- a/i18n/pt.json
+++ b/i18n/pt.json
@@ -1016,6 +1016,10 @@
"translation": "Somente um administrador de equipe pode exportar os dados."
},
{
+ "id": "api.team.get_invite_info.not_open_team",
+ "translation": "Convite é inválido devido a este não ser de uma equipe aberta."
+ },
+ {
"id": "api.team.import_team.admin.app_error",
"translation": "Somente um administrador de equipe pode importar dados."
},
@@ -1704,6 +1708,26 @@
"translation": "Permissões inadequadas para re-gerar o token do webhook de saída"
},
{
+ "id": "ent.compliance.licence_disable.app_error",
+ "translation": "Funcionalidade Compliance desabilitada pela licença atual. Entre em contato com o administrador do sistema sobre como atualizar sua licença de empresa."
+ },
+ {
+ "id": "ent.compliance.run_failed.error",
+ "translation": "Falha na tarefa '{{.JobName}}' para exportar o compliance no '{{.FilePath}}'"
+ },
+ {
+ "id": "ent.compliance.run_finished.info",
+ "translation": "Exportação do compliance terminado para tarefa '{{.JobName}}' exportado {{.Count}} registros para '{{.FilePath}}'"
+ },
+ {
+ "id": "ent.compliance.run_limit.warning",
+ "translation": "Aviso exportação do compliance na tarefa '{{.JobName}}' retornando muitos registros truncando para 30.000 em '{{.FilePath}}'"
+ },
+ {
+ "id": "ent.compliance.run_started.info",
+ "translation": "Exportação de compliance tarefa '{{.JobName}}' iniciada no '{{.FilePath}}'"
+ },
+ {
"id": "ent.ldap.do_login.bind_admin_user.app_error",
"translation": "Não foi possível ligar ao servidor LDAP. Verifique BindUsername e BindPassword."
},
@@ -1841,7 +1865,7 @@
},
{
"id": "model.authorize.is_valid.create_at.app_error",
- "translation": "Deve-se criar em um tempo válido"
+ "translation": "Create deve ser um tempo válido"
},
{
"id": "model.authorize.is_valid.expires.app_error",
@@ -1869,7 +1893,7 @@
},
{
"id": "model.channel.is_valid.create_at.app_error",
- "translation": "Deve-se criar em um tempo válido"
+ "translation": "Create deve ser um tempo válido"
},
{
"id": "model.channel.is_valid.creator_id.app_error",
@@ -1901,7 +1925,7 @@
},
{
"id": "model.channel.is_valid.update_at.app_error",
- "translation": "Deve-se atualizar em um tempo válido"
+ "translation": "Update deve ser um tempo válido"
},
{
"id": "model.channel_member.is_valid.channel_id.app_error",
@@ -1933,7 +1957,7 @@
},
{
"id": "model.command.is_valid.create_at.app_error",
- "translation": "Deve-se criar em um tempo válido"
+ "translation": "Create deve ser um tempo válido"
},
{
"id": "model.command.is_valid.id.app_error",
@@ -1957,7 +1981,7 @@
},
{
"id": "model.command.is_valid.update_at.app_error",
- "translation": "Deve-se atualizar em um tempo válido"
+ "translation": "Update deve ser um tempo válido"
},
{
"id": "model.command.is_valid.url.app_error",
@@ -1972,6 +1996,30 @@
"translation": "ID de usuário inválido"
},
{
+ "id": "model.compliance.is_valid.create_at.app_error",
+ "translation": "Create deve ser um tempo válido"
+ },
+ {
+ "id": "model.compliance.is_valid.desc.app_error",
+ "translation": "Descrição inválida"
+ },
+ {
+ "id": "model.compliance.is_valid.end_at.app_error",
+ "translation": "Para precisa ser um tempo válido"
+ },
+ {
+ "id": "model.compliance.is_valid.id.app_error",
+ "translation": "ID inválida"
+ },
+ {
+ "id": "model.compliance.is_valid.start_at.app_error",
+ "translation": "De precisa ser um tempo válido"
+ },
+ {
+ "id": "model.compliance.is_valid.start_end_at.app_error",
+ "translation": "Para precisa ser maior que De"
+ },
+ {
"id": "model.config.is_valid.email_reset_salt.app_error",
"translation": "Senha inválida redefinir salt nas configurações de email. Deve ser de 32 caracteres ou mais."
},
@@ -2065,7 +2113,7 @@
},
{
"id": "model.incoming_hook.create_at.app_error",
- "translation": "Deve-se criar em um tempo válido"
+ "translation": "Create deve ser um tempo válido"
},
{
"id": "model.incoming_hook.id.app_error",
@@ -2077,7 +2125,7 @@
},
{
"id": "model.incoming_hook.update_at.app_error",
- "translation": "Deve-se atualizar em um tempo válido"
+ "translation": "Update deve ser um tempo válido"
},
{
"id": "model.incoming_hook.user_id.app_error",
@@ -2097,7 +2145,7 @@
},
{
"id": "model.oauth.is_valid.create_at.app_error",
- "translation": "Deve-se criar em um tempo válido"
+ "translation": "Create deve ser um tempo válido"
},
{
"id": "model.oauth.is_valid.creator_id.app_error",
@@ -2117,7 +2165,7 @@
},
{
"id": "model.oauth.is_valid.update_at.app_error",
- "translation": "Deve-se atualizar em um tempo válido"
+ "translation": "Update deve ser um tempo válido"
},
{
"id": "model.outgoing_hook.is_valid.callback.app_error",
@@ -2129,7 +2177,7 @@
},
{
"id": "model.outgoing_hook.is_valid.create_at.app_error",
- "translation": "Deve-se criar em um tempo válido"
+ "translation": "Create deve ser um tempo válido"
},
{
"id": "model.outgoing_hook.is_valid.id.app_error",
@@ -2145,7 +2193,7 @@
},
{
"id": "model.outgoing_hook.is_valid.update_at.app_error",
- "translation": "Deve-se atualizar em um tempo válido"
+ "translation": "Update deve ser um tempo válido"
},
{
"id": "model.outgoing_hook.is_valid.url.app_error",
@@ -2165,7 +2213,7 @@
},
{
"id": "model.post.is_valid.create_at.app_error",
- "translation": "Deve-se criar em um tempo válido"
+ "translation": "Create deve ser um tempo válido"
},
{
"id": "model.post.is_valid.filenames.app_error",
@@ -2209,7 +2257,7 @@
},
{
"id": "model.post.is_valid.update_at.app_error",
- "translation": "Deve-se atualizar em um tempo válido"
+ "translation": "Update deve ser um tempo válido"
},
{
"id": "model.post.is_valid.user_id.app_error",
@@ -2241,7 +2289,7 @@
},
{
"id": "model.team.is_valid.create_at.app_error",
- "translation": "Deve-se criar em um tempo válido"
+ "translation": "Create deve ser um tempo válido"
},
{
"id": "model.team.is_valid.domains.app_error",
@@ -2269,7 +2317,7 @@
},
{
"id": "model.team.is_valid.update_at.app_error",
- "translation": "Deve-se atualizar em um tempo válido"
+ "translation": "Update deve ser um tempo válido"
},
{
"id": "model.team.is_valid.url.app_error",
@@ -2289,7 +2337,7 @@
},
{
"id": "model.user.is_valid.create_at.app_error",
- "translation": "Deve-se criar em um tempo válido"
+ "translation": "Create deve ser um tempo válido"
},
{
"id": "model.user.is_valid.email.app_error",
@@ -2325,7 +2373,7 @@
},
{
"id": "model.user.is_valid.update_at.app_error",
- "translation": "Deve-se atualizar em um tempo válido"
+ "translation": "Update deve ser um tempo válido"
},
{
"id": "model.user.is_valid.username.app_error",
@@ -2704,6 +2752,14 @@
"translation": "Não foi possível atualizar o comando"
},
{
+ "id": "store.sql_compliance.get.finding.app_error",
+ "translation": "Encontramos um erro ao obter o relatório de compliance"
+ },
+ {
+ "id": "store.sql_compliance.save.saving.app_error",
+ "translation": "Encontramos um erro ao salvar o relatório de compliance"
+ },
+ {
"id": "store.sql_license.get.app_error",
"translation": "Encontramos um erro ao obter a licença"
},
@@ -2800,6 +2856,10 @@
"translation": "Não foi possível obter o número de posts"
},
{
+ "id": "store.sql_post.compliance_export.app_error",
+ "translation": "Não foi possível obter os posts para exportação compliance"
+ },
+ {
"id": "store.sql_post.delete.app_error",
"translation": "Não foi possível deletar o post"
},
@@ -3076,6 +3136,10 @@
"translation": "Não foi possível encontrar uma conta correspondente ao seu tipo de autenticação para esta equipe. Esta equipe pode exigir um convite do dono da equipe para participar."
},
{
+ "id": "store.sql_user.get_by_auth.other.app_error",
+ "translation": "Foi encontrado um erro ao tentar encontrar a conta pelo tipo de autenticação."
+ },
+ {
"id": "store.sql_user.get_by_username.app_error",
"translation": "Não foi possível encontrar uma conta correspondente ao seu usuário para esta equipe. Esta equipe pode exigir um convite do dono da equipe para participar."
},
@@ -3100,6 +3164,10 @@
"translation": "Não foi possível contar os usuários"
},
{
+ "id": "store.sql_user.get_unread_count.app_error",
+ "translation": "Não foi possível obter o número de mensagens não lidas para o usuário"
+ },
+ {
"id": "store.sql_user.missing_account.const",
"translation": "Não foi possível encontrar uma conta correspondente ao seu endereço de email para esta equipe. Esta equipe pode exigir um convite do dono da equipe para participar."
},
@@ -3590,9 +3658,5 @@
{
"id": "web.watcher_fail.error",
"translation": "Falha ao adicionar diretório observador %v"
- },
- {
- "id": "api.team.get_invite_info.not_open_team",
- "translation": "Convite é inválido devido a este não ser de uma equipe aberta."
}
-]
+] \ No newline at end of file
diff --git a/model/version.go b/model/version.go
index 737071934..b3950dcc9 100644
--- a/model/version.go
+++ b/model/version.go
@@ -28,10 +28,10 @@ var versions = []string{
}
var CurrentVersion string = versions[0]
-var BuildNumber = "_BUILD_NUMBER_"
-var BuildDate = "_BUILD_DATE_"
-var BuildHash = "_BUILD_HASH_"
-var BuildEnterpriseReady = "_BUILD_ENTERPRISE_READY_"
+var BuildNumber string
+var BuildDate string
+var BuildHash string
+var BuildEnterpriseReady string
var versionsWithoutHotFixes []string
func init() {
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index 68c22f7f6..401306862 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -38,11 +38,6 @@ func NewSqlPostStore(sqlStore *SqlStore) PostStore {
}
func (s SqlPostStore) UpgradeSchemaIfNeeded() {
- // ADDED for 1.3 REMOVE for 2.2
- s.RemoveColumnIfExists("Posts", "ImgCount")
-
- // ADDED for 1.3 REMOVE for 2.2
- s.GetMaster().Exec(`UPDATE Preferences SET Type = :NewType WHERE Type = :CurrentType`, map[string]string{"NewType": model.POST_JOIN_LEAVE, "CurrentType": "join_leave"})
}
func (s SqlPostStore) CreateIndexesIfNotExists() {
diff --git a/web/web_test.go b/web/web_test.go
index 8305ca34e..8dde5d747 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -4,7 +4,6 @@
package web
import (
- "net/http"
"net/url"
"strings"
"testing"
@@ -40,6 +39,7 @@ func TearDown() {
}
}
+/* Test disabled for now so we don't requrie the client to build. Maybe re-enable after client gets moved out.
func TestStatic(t *testing.T) {
Setup()
@@ -54,6 +54,7 @@ func TestStatic(t *testing.T) {
t.Fatalf("couldn't get static files %v", resp.StatusCode)
}
}
+*/
func TestGetAccessToken(t *testing.T) {
Setup()
diff --git a/webapp/action_creators/global_actions.jsx b/webapp/action_creators/global_actions.jsx
index 0280d5974..ab38532a6 100644
--- a/webapp/action_creators/global_actions.jsx
+++ b/webapp/action_creators/global_actions.jsx
@@ -10,6 +10,7 @@ const ActionTypes = Constants.ActionTypes;
import * as AsyncClient from 'utils/async_client.jsx';
import * as Client from 'utils/client.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as Websockets from './websocket_actions.jsx';
import * as I18n from 'i18n/i18n.jsx';
import en from 'i18n/en.json';
@@ -97,10 +98,21 @@ export function emitLoadMorePostsFocusedBottomEvent() {
AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE);
}
-export function emitPostRecievedEvent(post) {
+export function emitPostRecievedEvent(post, websocketMessageProps) {
+ if (ChannelStore.getCurrentId() === post.channel_id) {
+ if (window.isActive) {
+ AsyncClient.updateLastViewedAt();
+ } else {
+ AsyncClient.getChannel(post.channel_id);
+ }
+ } else {
+ AsyncClient.getChannel(post.channel_id);
+ }
+
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST,
- post
+ post,
+ websocketMessageProps
});
}
@@ -261,3 +273,21 @@ export function viewLoggedIn() {
// Clear pending posts (shouldn't have pending posts if we are loading)
PostStore.clearPendingPosts();
}
+
+var lastTimeTypingSent = 0;
+export function emitLocalUserTypingEvent(channelId, parentId) {
+ const t = Date.now();
+ if ((t - lastTimeTypingSent) > Constants.UPDATE_TYPING_MS) {
+ Websockets.sendMessage({channel_id: channelId, action: 'typing', props: {parent_id: parentId}, state: {}});
+ lastTimeTypingSent = t;
+ }
+}
+
+export function emitRemoteUserTypingEvent(channelId, userId, postParentId) {
+ AppDispatcher.handleViewAction({
+ type: Constants.ActionTypes.USER_TYPING,
+ channelId,
+ userId,
+ postParentId
+ });
+}
diff --git a/webapp/action_creators/websocket_actions.jsx b/webapp/action_creators/websocket_actions.jsx
new file mode 100644
index 000000000..55a76dbea
--- /dev/null
+++ b/webapp/action_creators/websocket_actions.jsx
@@ -0,0 +1,227 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import UserStore from 'stores/user_store.jsx';
+import PostStore from 'stores/post_store.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+import BrowserStore from 'stores/browser_store.jsx';
+import ErrorStore from 'stores/error_store.jsx';
+
+import * as Utils from 'utils/utils.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
+
+import Constants from 'utils/constants.jsx';
+const SocketEvents = Constants.SocketEvents;
+
+const MAX_WEBSOCKET_FAILS = 7;
+const WEBSOCKET_RETRY_TIME = 3000;
+
+var conn = null;
+var connectFailCount = 0;
+var pastFirstInit = false;
+
+export function initialize() {
+ if (window.WebSocket && !conn) {
+ let protocol = 'ws://';
+ if (window.location.protocol === 'https:') {
+ protocol = 'wss://';
+ }
+
+ const connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket';
+
+ if (connectFailCount === 0) {
+ console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console
+ }
+
+ conn = new WebSocket(connUrl);
+
+ conn.onopen = () => {
+ if (connectFailCount > 0) {
+ console.log('websocket re-established connection'); //eslint-disable-line no-console
+ AsyncClient.getChannels();
+ AsyncClient.getPosts(ChannelStore.getCurrentId());
+ }
+
+ if (pastFirstInit) {
+ ErrorStore.clearLastError();
+ ErrorStore.emitChange();
+ }
+
+ pastFirstInit = true;
+ connectFailCount = 0;
+ };
+
+ conn.onclose = () => {
+ conn = null;
+
+ if (connectFailCount === 0) {
+ console.log('websocket closed'); //eslint-disable-line no-console
+ }
+
+ connectFailCount = connectFailCount + 1;
+
+ if (connectFailCount > MAX_WEBSOCKET_FAILS) {
+ ErrorStore.storeLastError(Utils.localizeMessage('channel_loader.socketError', 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.'));
+ }
+
+ ErrorStore.setConnectionErrorCount(connectFailCount);
+ ErrorStore.emitChange();
+
+ setTimeout(
+ () => {
+ initialize();
+ },
+ WEBSOCKET_RETRY_TIME
+ );
+ };
+
+ conn.onerror = (evt) => {
+ if (connectFailCount <= 1) {
+ console.log('websocket error'); //eslint-disable-line no-console
+ console.log(evt); //eslint-disable-line no-console
+ }
+ };
+
+ conn.onmessage = (evt) => {
+ const msg = JSON.parse(evt.data);
+ handleMessage(msg);
+ };
+ }
+}
+
+function handleMessage(msg) {
+ // Let the store know we are online. This probably shouldn't be here.
+ UserStore.setStatus(msg.user_id, 'online');
+
+ switch (msg.action) {
+ case SocketEvents.POSTED:
+ case SocketEvents.EPHEMERAL_MESSAGE:
+ handleNewPostEvent(msg);
+ break;
+
+ case SocketEvents.POST_EDITED:
+ handlePostEditEvent(msg);
+ break;
+
+ case SocketEvents.POST_DELETED:
+ handlePostDeleteEvent(msg);
+ break;
+
+ case SocketEvents.NEW_USER:
+ handleNewUserEvent();
+ break;
+
+ case SocketEvents.USER_ADDED:
+ handleUserAddedEvent(msg);
+ break;
+
+ case SocketEvents.USER_REMOVED:
+ handleUserRemovedEvent(msg);
+ break;
+
+ case SocketEvents.CHANNEL_VIEWED:
+ handleChannelViewedEvent(msg);
+ break;
+
+ case SocketEvents.PREFERENCE_CHANGED:
+ handlePreferenceChangedEvent(msg);
+ break;
+
+ case SocketEvents.TYPING:
+ handleUserTypingEvent(msg);
+ break;
+
+ default:
+ }
+}
+
+export function sendMessage(msg) {
+ if (conn && conn.readyState === WebSocket.OPEN) {
+ conn.send(JSON.stringify(msg));
+ } else if (!conn || conn.readyState === WebSocket.Closed) {
+ conn = null;
+ this.initialize();
+ }
+}
+
+export function close() {
+ if (conn && conn.readyState === WebSocket.OPEN) {
+ conn.close();
+ }
+}
+
+function handleNewPostEvent(msg) {
+ const post = JSON.parse(msg.props.post);
+ GlobalActions.emitPostRecievedEvent(post, msg.props);
+}
+
+function handlePostEditEvent(msg) {
+ // Store post
+ const post = JSON.parse(msg.props.post);
+ PostStore.storePost(post);
+ PostStore.emitChange();
+
+ // Update channel state
+ if (ChannelStore.getCurrentId() === msg.channel_id) {
+ if (window.isActive) {
+ AsyncClient.updateLastViewedAt();
+ }
+ }
+}
+
+function handlePostDeleteEvent(msg) {
+ const post = JSON.parse(msg.props.post);
+ GlobalActions.emitPostDeletedEvent(post);
+}
+
+function handleNewUserEvent() {
+ AsyncClient.getProfiles();
+ AsyncClient.getChannelExtraInfo();
+}
+
+function handleUserAddedEvent(msg) {
+ if (ChannelStore.getCurrentId() === msg.channel_id) {
+ AsyncClient.getChannelExtraInfo();
+ }
+
+ if (UserStore.getCurrentId() === msg.user_id) {
+ AsyncClient.getChannel(msg.channel_id);
+ }
+}
+
+function handleUserRemovedEvent(msg) {
+ if (UserStore.getCurrentId() === msg.user_id) {
+ AsyncClient.getChannels();
+
+ if (msg.props.remover_id !== msg.user_id &&
+ msg.channel_id === ChannelStore.getCurrentId() &&
+ $('#removed_from_channel').length > 0) {
+ var sentState = {};
+ sentState.channelName = ChannelStore.getCurrent().display_name;
+ sentState.remover = UserStore.getProfile(msg.props.remover_id).username;
+
+ BrowserStore.setItem('channel-removed-state', sentState);
+ $('#removed_from_channel').modal('show');
+ }
+ } else if (ChannelStore.getCurrentId() === msg.channel_id) {
+ AsyncClient.getChannelExtraInfo();
+ }
+}
+
+function handleChannelViewedEvent(msg) {
+ // Useful for when multiple devices have the app open to different channels
+ if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) {
+ AsyncClient.getChannel(msg.channel_id);
+ }
+}
+
+function handlePreferenceChangedEvent(msg) {
+ const preference = JSON.parse(msg.props.preference);
+ GlobalActions.emitPreferenceChangedEvent(preference);
+}
+
+function handleUserTypingEvent(msg) {
+ GlobalActions.emitRemoteUserTypingEvent(msg.channel_id, msg.user_id, msg.props.parent_id);
+}
diff --git a/webapp/components/about_build_modal.jsx b/webapp/components/about_build_modal.jsx
index e2fefc44e..e73d842d0 100644
--- a/webapp/components/about_build_modal.jsx
+++ b/webapp/components/about_build_modal.jsx
@@ -24,7 +24,7 @@ export default class AboutBuildModal extends React.Component {
let title = (
<FormattedMessage
id='about.teamEditiont0'
- defaultMessage='Team Edition T0'
+ defaultMessage='Team Edition'
/>
);
@@ -33,14 +33,14 @@ export default class AboutBuildModal extends React.Component {
title = (
<FormattedMessage
id='about.teamEditiont1'
- defaultMessage='Team Edition T1'
+ defaultMessage='Enterprise Edition'
/>
);
if (license.IsLicensed === 'true') {
title = (
<FormattedMessage
id='about.enterpriseEditione1'
- defaultMessage='Enterprise Edition E1'
+ defaultMessage='Enterprise Edition'
/>
);
licensee = (
diff --git a/webapp/components/access_history_modal.jsx b/webapp/components/access_history_modal.jsx
index 94a10c97f..9c49c3879 100644
--- a/webapp/components/access_history_modal.jsx
+++ b/webapp/components/access_history_modal.jsx
@@ -2,7 +2,6 @@
// See License.txt for license information.
import $ from 'jquery';
-import ReactDOM from 'react-dom';
import {Modal} from 'react-bootstrap';
import LoadingScreen from './loading_screen.jsx';
import AuditTable from './audit_table.jsx';
@@ -36,11 +35,8 @@ class AccessHistoryModal extends React.Component {
}
onShow() {
AsyncClient.getAudits();
-
- if ($(window).width() > 768) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
- } else {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150);
+ if (!Utils.isMobile()) {
+ $('.modal-body').perfectScrollbar();
}
}
onHide() {
diff --git a/webapp/components/activity_log_modal.jsx b/webapp/components/activity_log_modal.jsx
index 9a4ff3ef2..f1dd4a26a 100644
--- a/webapp/components/activity_log_modal.jsx
+++ b/webapp/components/activity_log_modal.jsx
@@ -2,7 +2,6 @@
// See License.txt for license information.
import $ from 'jquery';
-import ReactDOM from 'react-dom';
import UserStore from 'stores/user_store.jsx';
import * as Client from 'utils/client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
@@ -56,11 +55,8 @@ export default class ActivityLogModal extends React.Component {
}
onShow() {
AsyncClient.getSessions();
-
- if ($(window).width() > 768) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
- } else {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150);
+ if (!Utils.isMobile()) {
+ $('.modal-body').perfectScrollbar();
}
}
onHide() {
diff --git a/webapp/components/admin_console/license_settings.jsx b/webapp/components/admin_console/license_settings.jsx
index 5aa0dba7e..ad310d8e0 100644
--- a/webapp/components/admin_console/license_settings.jsx
+++ b/webapp/components/admin_console/license_settings.jsx
@@ -105,36 +105,27 @@ class LicenseSettings extends React.Component {
let licenseType;
let licenseKey;
+ const issued = Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10)) + ' ' + Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true);
+ const startsAt = Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10));
+ const expiresAt = Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10));
+
if (global.window.mm_license.IsLicensed === 'true') {
- edition = (
- <FormattedMessage
- id='admin.license.enterpriseEdition'
- defaultMessage='Mattermost Enterprise Edition. Designed for enterprise-scale communication.'
- />
- );
+ // Note: DO NOT LOCALISE THESE STRINGS. Legally we can not since the license is in English.
+ edition = 'Mattermost Enterprise Edition. Enterprise features on this server have been unlocked with a license key and a valid subscription.';
licenseType = (
- <FormattedHTMLMessage
- id='admin.license.enterpriseType'
- values={{
- terms: global.window.mm_config.TermsOfServiceLink,
- name: global.window.mm_license.Name,
- company: global.window.mm_license.Company,
- users: global.window.mm_license.Users,
- issued: Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10)) + ' ' + Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true),
- start: Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10)),
- expires: Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10)),
- ldap: global.window.mm_license.LDAP
- }}
- defaultMessage='<div><p>This compiled release of Mattermost platform is provided under a <a href="http://mattermost.com" target="_blank">commercial license</a> from Mattermost, Inc. based on your subscription level and is subject to the <a href="{terms}" target="_blank">Terms of Service.</a></p>
- <p>Your subscription details are as follows:</p>
- Name: {name}<br />
- Company or organization name: {company}<br/>
- Number of users: {users}<br/>
- License issued: {issued}<br/>
- Start date of license: {start}<br/>
- Expiry date of license: {expires}<br/>
- LDAP: {ldap}<br/></div>'
- />
+ <div>
+ <p>
+ {'This software is offered under a commercial license.\n\nSee ENTERPRISE-EDITION-LICENSE.txt in your root install directory for details. See NOTICE.txt for information about open source software used in this system.\n\nYour subscription details are as follows:'}
+ </p>
+ {`Name: ${global.window.mm_license.Name}`}<br/>
+ {`Company or organization name: ${global.window.mm_license.Company}`}<br/>
+ {`Number of users: ${global.window.mm_license.Users}`}<br/>
+ {`License issued: ${issued}`}<br/>
+ {`Start date of license: ${startsAt}`}<br/>
+ {`Expiry date of license: ${expiresAt}`}<br/>
+ <br/>
+ {'See also '}<a href='https://about.mattermost.com/enterprise-edition-terms/'>{'Enterprise Edition Terms of Service'}</a>{' and '}<a href='https://about.mattermost.com/privacy/'>{'Privacy Policy.'}</a>
+ </div>
);
licenseKey = (
@@ -162,20 +153,15 @@ class LicenseSettings extends React.Component {
</div>
);
} else {
+ // Note: DO NOT LOCALISE THESE STRINGS. Legally we can not since the license is in English.
edition = (
- <FormattedMessage
- id='admin.license.teamEdition'
- defaultMessage='Mattermost Team Edition. Designed for teams from 5 to 50 users.'
- />
+ <p>
+ {'Mattermost Enterprise Edition. Unlock enterprise features in this software through the purchase of a subscription from '}
+ <a href='https://mattermost.com/'>{'https://mattermost.com/'}</a>
+ </p>
);
- licenseType = (
- <FormattedHTMLMessage
- id='admin.license.teamType'
- defaultMessage='<span><p>This compiled release of Mattermost platform is offered under an MIT license.</p>
- <p>See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.</p></span>'
- />
- );
+ licenseType = 'This software is offered under a commercial license.\n\nSee ENTERPRISE-EDITION-LICENSE.txt in your root install directory for details. See NOTICE.txt for information about open source software used in this system.';
let fileName;
if (this.state.fileName) {
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index 7cd713942..369fa2dbb 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -80,6 +80,7 @@ export default class ChannelHeader extends React.Component {
SearchStore.addSearchChangeListener(this.onListenerChange);
PreferenceStore.addChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
+ $('.sidebar--left .dropdown-menu').perfectScrollbar();
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
diff --git a/webapp/components/channel_invite_button.jsx b/webapp/components/channel_invite_button.jsx
new file mode 100644
index 000000000..e4af9f9ce
--- /dev/null
+++ b/webapp/components/channel_invite_button.jsx
@@ -0,0 +1,79 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import * as Client from 'utils/client.jsx';
+
+import {FormattedMessage} from 'react-intl';
+import SpinnerButton from 'components/spinner_button.jsx';
+
+export default class ChannelInviteButton extends React.Component {
+ static get propTypes() {
+ return {
+ user: React.PropTypes.object.isRequired,
+ channel: React.PropTypes.object.isRequired,
+ onInviteError: React.PropTypes.func.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleClick = this.handleClick.bind(this);
+
+ this.state = {
+ addingUser: false
+ };
+ }
+
+ handleClick() {
+ if (this.state.addingUser) {
+ return;
+ }
+
+ this.setState({
+ addingUser: true
+ });
+
+ const data = {
+ user_id: this.props.user.id
+ };
+
+ Client.addChannelMember(
+ this.props.channel.id,
+ data,
+ () => {
+ this.setState({
+ addingUser: false
+ });
+
+ this.props.onInviteError(null);
+ AsyncClient.getChannelExtraInfo();
+ },
+ (err) => {
+ this.setState({
+ addingUser: false
+ });
+
+ this.props.onInviteError(err);
+ }
+ );
+ }
+
+ render() {
+ return (
+ <SpinnerButton
+ onClick={this.handleClick}
+ spinning={this.state.addingUser}
+ >
+ <i className='glyphicon glyphicon-envelope'/>
+ <FormattedMessage
+ id='channel_invite.add'
+ defaultMessage=' Add'
+ />
+ </SpinnerButton>
+ );
+ }
+}
diff --git a/webapp/components/channel_invite_modal.jsx b/webapp/components/channel_invite_modal.jsx
index dfb0d4187..c7c1906a5 100644
--- a/webapp/components/channel_invite_modal.jsx
+++ b/webapp/components/channel_invite_modal.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import $ from 'jquery';
+import ChannelInviteButton from './channel_invite_button.jsx';
import FilteredUserList from './filtered_user_list.jsx';
import LoadingScreen from './loading_screen.jsx';
@@ -9,7 +10,6 @@ import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
-import * as Client from 'utils/client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import {FormattedMessage} from 'react-intl';
@@ -23,9 +23,8 @@ export default class ChannelInviteModal extends React.Component {
super(props);
this.onListenerChange = this.onListenerChange.bind(this);
- this.handleInvite = this.handleInvite.bind(this);
this.getStateFromStores = this.getStateFromStores.bind(this);
- this.createInviteButton = this.createInviteButton.bind(this);
+ this.handleInviteError = this.handleInviteError.bind(this);
this.state = this.getStateFromStores();
}
@@ -120,36 +119,16 @@ export default class ChannelInviteModal extends React.Component {
this.setState(newState);
}
}
- handleInvite(user) {
- const data = {
- user_id: user.id
- };
-
- Client.addChannelMember(
- this.props.channel.id,
- data,
- () => {
- this.setState({inviteError: null});
- AsyncClient.getChannelExtraInfo();
- },
- (err) => {
- this.setState({inviteError: err.message});
- }
- );
- }
- createInviteButton({user}) {
- return (
- <a
- onClick={this.handleInvite.bind(this, user)}
- className='btn btn-sm btn-primary'
- >
- <i className='glyphicon glyphicon-envelope'/>
- <FormattedMessage
- id='channel_invite.add'
- defaultMessage=' Add'
- />
- </a>
- );
+ handleInviteError(err) {
+ if (err) {
+ this.setState({
+ inviteError: err.message
+ });
+ } else {
+ this.setState({
+ inviteError: null
+ });
+ }
}
render() {
var inviteError = null;
@@ -169,7 +148,11 @@ export default class ChannelInviteModal extends React.Component {
<FilteredUserList
style={{maxHeight}}
users={this.state.nonmembers}
- actions={[this.createInviteButton]}
+ actions={[ChannelInviteButton]}
+ actionProps={{
+ channel: this.props.channel,
+ onInviteError: this.handleInviteError
+ }}
/>
);
}
diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx
index 0aeb70c57..177f282d3 100644
--- a/webapp/components/create_comment.jsx
+++ b/webapp/components/create_comment.jsx
@@ -6,7 +6,6 @@ import ReactDOM from 'react-dom';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as Client from 'utils/client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
-import SocketStore from 'stores/socket_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PostDeletedModal from './post_deleted_modal.jsx';
@@ -17,6 +16,7 @@ import MsgTyping from './msg_typing.jsx';
import FileUpload from './file_upload.jsx';
import FilePreview from './file_preview.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
import Constants from 'utils/constants.jsx';
@@ -196,11 +196,7 @@ class CreateComment extends React.Component {
}
}
- const t = Date.now();
- if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
- SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}});
- this.lastTime = t;
- }
+ GlobalActions.emitLocalUserTypingEvent(this.props.channelId, this.props.rootId);
}
handleUserInput(messageText) {
let draft = PostStore.getCommentDraft(this.props.rootId);
diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx
index 36bfbf22d..e5e99debd 100644
--- a/webapp/components/create_post.jsx
+++ b/webapp/components/create_post.jsx
@@ -19,7 +19,6 @@ import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
-import SocketStore from 'stores/socket_store.jsx';
import Constants from 'utils/constants.jsx';
@@ -213,11 +212,7 @@ class CreatePost extends React.Component {
}
}
- const t = Date.now();
- if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
- SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}});
- this.lastTime = t;
- }
+ GlobalActions.emitLocalUserTypingEvent(this.state.channelId, '');
}
handleUserInput(messageText) {
this.setState({messageText});
diff --git a/webapp/components/filtered_user_list.jsx b/webapp/components/filtered_user_list.jsx
index 1e4afd2be..bd6c49714 100644
--- a/webapp/components/filtered_user_list.jsx
+++ b/webapp/components/filtered_user_list.jsx
@@ -114,6 +114,7 @@ class FilteredUserList extends React.Component {
<UserList
users={users}
actions={this.props.actions}
+ actionProps={this.props.actionProps}
/>
</div>
</div>
@@ -123,13 +124,15 @@ class FilteredUserList extends React.Component {
FilteredUserList.defaultProps = {
users: [],
- actions: []
+ actions: [],
+ actionProps: {}
};
FilteredUserList.propTypes = {
intl: intlShape.isRequired,
users: React.PropTypes.arrayOf(React.PropTypes.object),
actions: React.PropTypes.arrayOf(React.PropTypes.func),
+ actionProps: React.PropTypes.object,
style: React.PropTypes.object
};
diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx
index d567183ac..1f8fd6133 100644
--- a/webapp/components/invite_member_modal.jsx
+++ b/webapp/components/invite_member_modal.jsx
@@ -1,7 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
import ReactDOM from 'react-dom';
import * as utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
@@ -176,12 +175,6 @@ class InviteMemberModal extends React.Component {
});
}
- componentDidUpdate(prevProps, prevState) {
- if (!prevState.show && this.state.show) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
- }
- }
-
addInviteFields() {
var count = this.state.idCount + 1;
var inviteIds = this.state.inviteIds;
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index 6d35ff8c2..c6f7b50b1 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -5,12 +5,13 @@ import $ from 'jquery';
import * as AsyncClient from 'utils/async_client.jsx';
import * as GlobalActions from 'action_creators/global_actions.jsx';
import UserStore from 'stores/user_store.jsx';
-import SocketStore from 'stores/socket_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
+import BrowserStore from 'stores/browser_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import ErrorBar from 'components/error_bar.jsx';
+import * as Websockets from 'action_creators/websocket_actions.jsx';
import {browserHistory} from 'react-router';
@@ -66,11 +67,6 @@ export default class LoggedIn extends React.Component {
}
}
}
- onSocketChange(msg) {
- if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
- UserStore.setStatus(msg.user_id, 'online');
- }
- }
componentWillMount() {
// Emit view action
GlobalActions.viewLoggedIn();
@@ -78,8 +74,8 @@ export default class LoggedIn extends React.Component {
// Listen for user
UserStore.addChangeListener(this.onUserChanged);
- // Add listner for socker store
- SocketStore.addChangeListener(this.onSocketChange);
+ // Initalize websockets
+ Websockets.initialize();
// Get all statuses regularally. (Soon to be switched to websocket)
this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL);
@@ -89,7 +85,7 @@ export default class LoggedIn extends React.Component {
// when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
// make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
- if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
+ if (BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
return;
}
@@ -99,7 +95,7 @@ export default class LoggedIn extends React.Component {
if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
// make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
- if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
+ if (BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
return;
}
@@ -178,7 +174,7 @@ export default class LoggedIn extends React.Component {
$(window).off('focus');
$(window).off('blur');
- SocketStore.removeChangeListener(this.onSocketChange);
+ Websockets.close();
UserStore.removeChangeListener(this.onUserChanged);
$('body').off('click.userpopover');
diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx
index 57cac7229..d1446059d 100644
--- a/webapp/components/more_direct_channels.jsx
+++ b/webapp/components/more_direct_channels.jsx
@@ -5,9 +5,9 @@ import {Modal} from 'react-bootstrap';
import FilteredUserList from './filtered_user_list.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
-import loadingGif from 'images/load.gif';
import {FormattedMessage} from 'react-intl';
+import SpinnerButton from 'components/spinner_button.jsx';
import React from 'react';
@@ -83,26 +83,16 @@ export default class MoreDirectChannels extends React.Component {
}
createJoinDirectChannelButton({user}) {
- if (this.state.loadingDMChannel === user.id) {
- return (
- <img
- className='channel-loading-gif'
- src={loadingGif}
- />
- );
- }
-
return (
- <button
- type='button'
- className='btn btn-primary btn-message'
+ <SpinnerButton
+ spinning={this.state.loadingDMChannel === user.id}
onClick={this.handleShowDirectChannel.bind(this, user)}
>
<FormattedMessage
id='more_direct_channels.message'
defaultMessage='Message'
/>
- </button>
+ </SpinnerButton>
);
}
diff --git a/webapp/components/msg_typing.jsx b/webapp/components/msg_typing.jsx
index b1781623c..b2d414287 100644
--- a/webapp/components/msg_typing.jsx
+++ b/webapp/components/msg_typing.jsx
@@ -1,21 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import SocketStore from 'stores/socket_store.jsx';
-import UserStore from 'stores/user_store.jsx';
+import UserTypingStore from 'stores/user_typing_store.jsx';
-import Constants from 'utils/constants.jsx';
-
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
-
-const SocketEvents = Constants.SocketEvents;
-
-const holders = defineMessages({
- someone: {
- id: 'msg_typing.someone',
- defaultMessage: 'Someone'
- }
-});
+import {FormattedMessage} from 'react-intl';
import React from 'react';
@@ -23,69 +11,40 @@ class MsgTyping extends React.Component {
constructor(props) {
super(props);
- this.onChange = this.onChange.bind(this);
+ this.onTypingChange = this.onTypingChange.bind(this);
this.updateTypingText = this.updateTypingText.bind(this);
this.componentWillReceiveProps = this.componentWillReceiveProps.bind(this);
- this.typingUsers = {};
this.state = {
text: ''
};
}
- componentDidMount() {
- SocketStore.addChangeListener(this.onChange);
+ componentWillMount() {
+ UserTypingStore.addChangeListener(this.onTypingChange);
+ this.onTypingChange();
+ }
+
+ componentWillUnmount() {
+ UserTypingStore.removeChangeListener(this.onTypingChange);
}
componentWillReceiveProps(nextProps) {
if (this.props.channelId !== nextProps.channelId) {
- for (const u in this.typingUsers) {
- if (!this.typingUsers.hasOwnProperty(u)) {
- continue;
- }
-
- clearTimeout(this.typingUsers[u]);
- }
- this.typingUsers = {};
- this.setState({text: ''});
+ this.updateTypingText(UserTypingStore.getUsersTyping(nextProps.channelId, nextProps.parentId));
}
}
- componentWillUnmount() {
- SocketStore.removeChangeListener(this.onChange);
+ onTypingChange() {
+ this.updateTypingText(UserTypingStore.getUsersTyping(this.props.channelId, this.props.parentId));
}
- onChange(msg) {
- let username = this.props.intl.formatMessage(holders.someone);
- if (msg.action === SocketEvents.TYPING &&
- this.props.channelId === msg.channel_id &&
- this.props.parentId === msg.props.parent_id) {
- if (UserStore.hasProfile(msg.user_id)) {
- username = UserStore.getProfile(msg.user_id).username;
- }
-
- if (this.typingUsers[username]) {
- clearTimeout(this.typingUsers[username]);
- }
-
- this.typingUsers[username] = setTimeout(function myTimer(user) {
- delete this.typingUsers[user];
- this.updateTypingText();
- }.bind(this, username), Constants.UPDATE_TYPING_MS);
-
- this.updateTypingText();
- } else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) {
- if (UserStore.hasProfile(msg.user_id)) {
- username = UserStore.getProfile(msg.user_id).username;
- }
- clearTimeout(this.typingUsers[username]);
- delete this.typingUsers[username];
- this.updateTypingText();
+ updateTypingText(typingUsers) {
+ if (!typingUsers) {
+ return;
}
- }
- updateTypingText() {
- const users = Object.keys(this.typingUsers);
+ const users = Object.keys(typingUsers);
let text = '';
switch (users.length) {
case 0:
@@ -129,9 +88,8 @@ class MsgTyping extends React.Component {
}
MsgTyping.propTypes = {
- intl: intlShape.isRequired,
channelId: React.PropTypes.string,
parentId: React.PropTypes.string
};
-export default injectIntl(MsgTyping); \ No newline at end of file
+export default MsgTyping;
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index fb3b25957..520f05ed0 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -8,6 +8,7 @@ import MessageWrapper from './message_wrapper.jsx';
import NotifyCounts from './notify_counts.jsx';
import ChannelInfoModal from './channel_info_modal.jsx';
import ChannelInviteModal from './channel_invite_modal.jsx';
+import ChannelMembersModal from './channel_members_modal.jsx';
import ChannelNotificationsModal from './channel_notifications_modal.jsx';
import DeleteChannelModal from './delete_channel_modal.jsx';
import RenameChannelModal from './rename_channel_modal.jsx';
@@ -433,6 +434,7 @@ export default class Navbar extends React.Component {
var editChannelHeaderModal = null;
var editChannelPurposeModal = null;
let renameChannelModal = null;
+ let channelMembersModal = null;
if (channel) {
popoverContent = (
@@ -523,6 +525,14 @@ export default class Navbar extends React.Component {
channel={channel}
/>
);
+
+ channelMembersModal = (
+ <ChannelMembersModal
+ show={this.state.showMembersModal}
+ onModalDismissed={() => this.setState({showMembersModal: false})}
+ channel={channel}
+ />
+ );
}
var collapseButtons = this.createCollapseButtons(currentId);
@@ -556,6 +566,7 @@ export default class Navbar extends React.Component {
{editChannelHeaderModal}
{editChannelPurposeModal}
{renameChannelModal}
+ {channelMembersModal}
</div>
);
}
diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx
index 7d048019c..819c7f590 100644
--- a/webapp/components/popover_list_members.jsx
+++ b/webapp/components/popover_list_members.jsx
@@ -1,6 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import $ from 'jquery';
+
import UserStore from 'stores/user_store.jsx';
import {Popover, Overlay} from 'react-bootstrap';
import * as Utils from 'utils/utils.jsx';
@@ -20,6 +22,10 @@ export default class PopoverListMembers extends React.Component {
this.closePopover = this.closePopover.bind(this);
}
+ componentDidUpdate() {
+ $('.member-list__popover .popover-content').perfectScrollbar();
+ }
+
componentWillMount() {
this.setState({showPopover: false});
}
diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx
index 29986d415..de99eb37d 100644
--- a/webapp/components/rhs_comment.jsx
+++ b/webapp/components/rhs_comment.jsx
@@ -5,7 +5,6 @@ import ReactDOM from 'react-dom';
import PostStore from 'stores/post_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserProfile from './user_profile.jsx';
-import UserStore from 'stores/user_store.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
@@ -97,8 +96,8 @@ class RhsComment extends React.Component {
return '';
}
- var isOwner = UserStore.getCurrentId() === post.user_id;
- var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles);
+ const isOwner = this.props.currentUser.id === post.user_id;
+ const isAdmin = Utils.isAdmin(this.props.currentUser.roles);
var dropdownContents = [];
@@ -193,11 +192,11 @@ class RhsComment extends React.Component {
var post = this.props.post;
var currentUserCss = '';
- if (UserStore.getCurrentId() === post.user_id) {
+ if (this.props.currentUser === post.user_id) {
currentUserCss = 'current--user';
}
- var timestamp = UserStore.getCurrentUser().update_at;
+ var timestamp = this.props.currentUser.update_at;
let loading;
let postClass = '';
@@ -305,7 +304,8 @@ RhsComment.defaultProps = {
RhsComment.propTypes = {
intl: intlShape.isRequired,
post: React.PropTypes.object,
- user: React.PropTypes.object
+ user: React.PropTypes.object.isRequired,
+ currentUser: React.PropTypes.object.isRequired
};
export default injectIntl(RhsComment);
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index a2c7ee7f8..1aa4a555f 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -55,8 +55,8 @@ export default class RhsRootPost extends React.Component {
render() {
const post = this.props.post;
const user = this.props.user;
- var isOwner = user.id === post.user_id;
- var isAdmin = Utils.isAdmin(user.roles);
+ var isOwner = this.props.currentUser.id === post.user_id;
+ var isAdmin = Utils.isAdmin(this.props.currentUser.roles);
var timestamp = UserStore.getProfile(post.user_id).update_at;
var channel = ChannelStore.get(post.channel_id);
@@ -286,5 +286,6 @@ RhsRootPost.defaultProps = {
RhsRootPost.propTypes = {
post: React.PropTypes.object.isRequired,
user: React.PropTypes.object.isRequired,
+ currentUser: React.PropTypes.object.isRequired,
commentCount: React.PropTypes.number
};
diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread.jsx
index cc900f8e7..f0324d7ce 100644
--- a/webapp/components/rhs_thread.jsx
+++ b/webapp/components/rhs_thread.jsx
@@ -46,6 +46,9 @@ export default class RhsThread extends React.Component {
window.addEventListener('resize', this.handleResize);
this.mounted = true;
+ if (!Utils.isMobile()) {
+ $('.sidebar--right .post-right__scroll').perfectScrollbar();
+ }
}
componentDidUpdate() {
if ($('.post-right__scroll')[0]) {
@@ -130,7 +133,7 @@ export default class RhsThread extends React.Component {
}
// sort failed posts to bottom, followed by pending, and then regular posts
- postsArray.sort(function postSort(a, b) {
+ postsArray.sort((a, b) => {
if ((a.state === Constants.POST_LOADING || a.state === Constants.POST_FAILED) && (b.state !== Constants.POST_LOADING && b.state !== Constants.POST_FAILED)) {
return 1;
}
@@ -182,24 +185,26 @@ export default class RhsThread extends React.Component {
post={selected}
commentCount={postsArray.length}
user={profile}
+ currentUser={this.props.currentUser}
/>
<div className='post-right-comments-container'>
- {postsArray.map(function mapPosts(comPost) {
- let p;
- if (UserStore.getCurrentId() === comPost.user_id) {
- p = UserStore.getCurrentUser();
- } else {
- p = profiles[comPost.user_id];
- }
- return (
- <Comment
- ref={comPost.id}
- key={comPost.id + 'commentKey'}
- post={comPost}
- user={p}
- />
- );
- })}
+ {postsArray.map((comPost) => {
+ let p;
+ if (UserStore.getCurrentId() === comPost.user_id) {
+ p = UserStore.getCurrentUser();
+ } else {
+ p = profiles[comPost.user_id];
+ }
+ return (
+ <Comment
+ ref={comPost.id}
+ key={comPost.id + 'commentKey'}
+ post={comPost}
+ user={p}
+ currentUser={this.props.currentUser}
+ />
+ );
+ })}
</div>
<div className='post-create__container'>
<CreateComment
@@ -221,5 +226,6 @@ RhsThread.defaultProps = {
RhsThread.propTypes = {
fromSearch: React.PropTypes.string,
- isMentionSearch: React.PropTypes.bool
+ isMentionSearch: React.PropTypes.bool,
+ currentUser: React.PropTypes.object.isRequired
};
diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx
index 7619e41cd..c5baf50ef 100644
--- a/webapp/components/search_results.jsx
+++ b/webapp/components/search_results.jsx
@@ -61,6 +61,9 @@ export default class SearchResults extends React.Component {
UserStore.addChangeListener(this.onUserChange);
this.resize();
window.addEventListener('resize', this.handleResize);
+ if (!Utils.isMobile()) {
+ $('.sidebar--right .search-items-container').perfectScrollbar();
+ }
}
shouldComponentUpdate(nextProps, nextState) {
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index 49ae1bec6..0e1b7dd0e 100644
--- a/webapp/components/sidebar.jsx
+++ b/webapp/components/sidebar.jsx
@@ -93,12 +93,12 @@ export default class Sidebar extends React.Component {
const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
const directChannels = [];
- for (const preference of preferences) {
- if (preference.value !== 'true') {
+ for (const [name, value] of preferences) {
+ if (value !== 'true') {
continue;
}
- const teammateId = preference.name;
+ const teammateId = name;
let directChannel = channels.find(Utils.isDirectChannelForUser.bind(null, teammateId));
@@ -163,6 +163,9 @@ export default class Sidebar extends React.Component {
componentDidUpdate() {
this.updateTitle();
this.updateUnreadIndicators();
+ if (!Utils.isMobile()) {
+ $('.sidebar--left .nav-pills__container').perfectScrollbar();
+ }
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
@@ -239,11 +242,10 @@ export default class Sidebar extends React.Component {
if (!this.isLeaving.get(channel.id)) {
this.isLeaving.set(channel.id, true);
- const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'false');
-
- // bypass AsyncClient since we've already saved the updated preferences
- Client.savePreferences(
- [preference],
+ AsyncClient.savePreference(
+ Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
+ channel.teammate_id,
+ 'false',
() => {
this.isLeaving.set(channel.id, false);
},
diff --git a/webapp/components/sidebar_right.jsx b/webapp/components/sidebar_right.jsx
index 1b3286963..a2e3914f3 100644
--- a/webapp/components/sidebar_right.jsx
+++ b/webapp/components/sidebar_right.jsx
@@ -8,6 +8,7 @@ import SearchResults from './search_results.jsx';
import RhsThread from './rhs_thread.jsx';
import SearchStore from 'stores/search_store.jsx';
import PostStore from 'stores/post_store.jsx';
+import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
const SIDEBAR_SCROLL_DELAY = 500;
@@ -22,33 +23,38 @@ export default class SidebarRight extends React.Component {
this.onSelectedChange = this.onSelectedChange.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
+ this.onUserChange = this.onUserChange.bind(this);
this.onShowSearch = this.onShowSearch.bind(this);
this.doStrangeThings = this.doStrangeThings.bind(this);
- this.state = this.getStateFromStores();
- }
- getStateFromStores() {
- return {
- search_visible: SearchStore.getSearchResults() != null,
- post_right_visible: PostStore.getSelectedPost() != null,
- is_mention_search: SearchStore.getIsMentionSearch()
+ this.state = {
+ searchVisible: !!SearchStore.getSearchResults(),
+ isMentionSearch: SearchStore.getIsMentionSearch(),
+ postRightVisible: !!PostStore.getSelectedPost(),
+ fromSearch: false,
+ currentUser: UserStore.getCurrentUser()
};
}
componentDidMount() {
SearchStore.addSearchChangeListener(this.onSearchChange);
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
SearchStore.addShowSearchListener(this.onShowSearch);
+ UserStore.addChangeListener(this.onUserChange);
this.doStrangeThings();
}
componentWillUnmount() {
SearchStore.removeSearchChangeListener(this.onSearchChange);
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
SearchStore.removeShowSearchListener(this.onShowSearch);
+ UserStore.removeChangeListener(this.onUserChange);
+ }
+ shouldComponentUpdate(nextProps, nextState) {
+ return !Utils.areObjectsEqual(nextState, this.state);
}
componentWillUpdate(nextProps, nextState) {
- const isOpen = this.state.search_visible || this.state.post_right_visible;
- const willOpen = nextState.search_visible || nextState.post_right_visible;
+ const isOpen = this.state.searchVisible || this.state.postRightVisible;
+ const willOpen = nextState.searchVisible || nextState.postRightVisible;
if (!isOpen && willOpen) {
setTimeout(() => PostStore.jumpPostsViewSidebarOpen(), SIDEBAR_SCROLL_DELAY);
@@ -66,7 +72,7 @@ export default class SidebarRight extends React.Component {
$('.sidebar--right').addClass('move--left');
//$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
- if (this.state.search_visible || this.state.post_right_visible) {
+ if (this.state.searchVisible || this.state.postRightVisible) {
if (windowWidth > 960) {
velocity($('.inner-wrap'), {marginRight: sidebarRightWidth}, {duration: 500, easing: 'easeOutSine'});
velocity($('.sidebar--right'), {translateX: 0}, {duration: 500, easing: 'easeOutSine'});
@@ -98,35 +104,40 @@ export default class SidebarRight extends React.Component {
this.doStrangeThings();
}
onSelectedChange(fromSearch) {
- var newState = this.getStateFromStores(fromSearch);
- newState.from_search = fromSearch;
- if (!Utils.areObjectsEqual(newState, this.state)) {
- this.setState(newState);
- }
+ this.setState({
+ postRightVisible: !!PostStore.getSelectedPost(),
+ fromSearch
+ });
}
onSearchChange() {
- var newState = this.getStateFromStores();
- if (!Utils.areObjectsEqual(newState, this.state)) {
- this.setState(newState);
- }
+ this.setState({
+ searchVisible: !!SearchStore.getSearchResults(),
+ isMentionSearch: SearchStore.getIsMentionSearch()
+ });
+ }
+ onUserChange() {
+ this.setState({
+ currentUser: UserStore.getCurrentUser()
+ });
}
onShowSearch() {
- if (!this.state.search_visible) {
+ if (!this.state.searchVisible) {
this.setState({
- search_visible: true
+ searchVisible: true
});
}
}
render() {
- var content = '';
+ let content = null;
- if (this.state.search_visible) {
- content = <SearchResults isMentionSearch={this.state.is_mention_search}/>;
- } else if (this.state.post_right_visible) {
+ if (this.state.searchVisible) {
+ content = <SearchResults isMentionSearch={this.state.isMentionSearch}/>;
+ } else if (this.state.postRightVisible) {
content = (
<RhsThread
- fromSearch={this.state.from_search}
- isMentionSearch={this.state.is_mention_search}
+ fromSearch={this.state.fromSearch}
+ isMentionSearch={this.state.isMentionSearch}
+ currentUser={this.state.currentUser}
/>
);
}
diff --git a/webapp/components/spinner_button.jsx b/webapp/components/spinner_button.jsx
new file mode 100644
index 000000000..fcc9af8cd
--- /dev/null
+++ b/webapp/components/spinner_button.jsx
@@ -0,0 +1,48 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import loadingGif from 'images/load.gif';
+
+export default class SpinnerButton extends React.Component {
+ static get propTypes() {
+ return {
+ children: React.PropTypes.node,
+ spinning: React.PropTypes.bool.isRequired,
+ onClick: React.PropTypes.func
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleClick = this.handleClick.bind(this);
+ }
+
+ handleClick(e) {
+ if (this.props.onClick) {
+ this.props.onClick(e);
+ }
+ }
+
+ render() {
+ if (this.props.spinning) {
+ return (
+ <img
+ className='spinner-button__gif'
+ src={loadingGif}
+ />
+ );
+ }
+
+ return (
+ <button
+ onClick={this.handleClick}
+ className='btn btn-sm btn-primary'
+ >
+ {this.props.children}
+ </button>
+ );
+ }
+}
diff --git a/webapp/components/team_settings_modal.jsx b/webapp/components/team_settings_modal.jsx
index 7dbbd680a..c19787993 100644
--- a/webapp/components/team_settings_modal.jsx
+++ b/webapp/components/team_settings_modal.jsx
@@ -5,6 +5,7 @@ import $ from 'jquery';
import ReactDOM from 'react-dom';
import SettingsSidebar from './settings_sidebar.jsx';
import TeamSettings from './team_settings.jsx';
+import * as Utils from 'utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
@@ -49,9 +50,16 @@ class TeamSettingsModal extends React.Component {
$('.modal-dialog.display--content').removeClass('display--content');
}, 500);
});
+
+ if (!Utils.isMobile()) {
+ $('.settings-modal .settings-content').perfectScrollbar();
+ }
}
updateTab(tab) {
this.setState({activeTab: tab, activeSection: ''});
+ if (!Utils.isMobile()) {
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
+ }
}
updateSection(section) {
this.setState({activeSection: section});
diff --git a/webapp/components/tutorial/tutorial_intro_screens.jsx b/webapp/components/tutorial/tutorial_intro_screens.jsx
index 5db45523e..734842cad 100644
--- a/webapp/components/tutorial/tutorial_intro_screens.jsx
+++ b/webapp/components/tutorial/tutorial_intro_screens.jsx
@@ -36,17 +36,22 @@ export default class TutorialIntroScreens extends React.Component {
Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL));
- let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ let step = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 0);
- const newValue = (parseInt(preference.value, 10) + 1).toString();
-
- preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue);
- AsyncClient.savePreferences([preference]);
+ AsyncClient.savePreference(
+ Preferences.TUTORIAL_STEP,
+ UserStore.getCurrentId(),
+ step + 1
+ );
}
skipTutorial(e) {
e.preventDefault();
- const preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), '999');
- AsyncClient.savePreferences([preference]);
+
+ AsyncClient.savePreference(
+ Preferences.TUTORIAL_STEP,
+ UserStore.getCurrentId(),
+ 999
+ );
}
createScreen() {
switch (this.state.currentScreen) {
diff --git a/webapp/components/tutorial/tutorial_tip.jsx b/webapp/components/tutorial/tutorial_tip.jsx
index ab49d4b04..d93fff1b1 100644
--- a/webapp/components/tutorial/tutorial_tip.jsx
+++ b/webapp/components/tutorial/tutorial_tip.jsx
@@ -29,12 +29,13 @@ export default class TutorialTip extends React.Component {
this.setState({show});
if (!show && this.state.currentScreen >= this.props.screens.length - 1) {
- let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ let step = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 0);
- const newValue = (parseInt(preference.value, 10) + 1).toString();
-
- preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue);
- AsyncClient.savePreferences([preference]);
+ AsyncClient.savePreference(
+ Preferences.TUTORIAL_STEP,
+ UserStore.getCurrentId(),
+ step + 1
+ );
}
}
handleNext() {
diff --git a/webapp/components/user_list.jsx b/webapp/components/user_list.jsx
index 2140158e6..3652723be 100644
--- a/webapp/components/user_list.jsx
+++ b/webapp/components/user_list.jsx
@@ -18,16 +18,22 @@ export default class UserList extends React.Component {
key={user.id}
user={user}
actions={this.props.actions}
+ actionProps={this.props.actionProps}
/>
);
});
} else {
content = (
- <div key='no-users-found'>
- <FormattedMessage
- id='user_list.notFound'
- defaultMessage='No users found :('
- />
+ <div
+ key='no-users-found'
+ className='no-channel-message'
+ >
+ <p className='primary-message'>
+ <FormattedMessage
+ id='user_list.notFound'
+ defaultMessage='No users found :('
+ />
+ </p>
</div>
);
}
@@ -42,10 +48,12 @@ export default class UserList extends React.Component {
UserList.defaultProps = {
users: [],
- actions: []
+ actions: [],
+ actionProps: {}
};
UserList.propTypes = {
users: React.PropTypes.arrayOf(React.PropTypes.object),
- actions: React.PropTypes.arrayOf(React.PropTypes.func)
+ actions: React.PropTypes.arrayOf(React.PropTypes.func),
+ actionProps: React.PropTypes.object
};
diff --git a/webapp/components/user_list_row.jsx b/webapp/components/user_list_row.jsx
index ed3a29a61..f6fd91688 100644
--- a/webapp/components/user_list_row.jsx
+++ b/webapp/components/user_list_row.jsx
@@ -6,7 +6,7 @@ import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
-export default function UserListRow({user, actions}) {
+export default function UserListRow({user, actions, actionProps}) {
const nameFormat = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', '');
let name = user.username;
@@ -21,6 +21,7 @@ export default function UserListRow({user, actions}) {
<Action
key={index.toString()}
user={user}
+ {...actionProps}
/>
);
});
@@ -56,10 +57,12 @@ export default function UserListRow({user, actions}) {
}
UserListRow.defaultProps = {
- actions: []
+ actions: [],
+ actionProps: {}
};
UserListRow.propTypes = {
user: React.PropTypes.object.isRequired,
- actions: React.PropTypes.arrayOf(React.PropTypes.func)
+ actions: React.PropTypes.arrayOf(React.PropTypes.func),
+ actionProps: React.PropTypes.object
};
diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx
index 7c496f57b..4fcdc9a41 100644
--- a/webapp/components/user_settings/user_settings_advanced.jsx
+++ b/webapp/components/user_settings/user_settings_advanced.jsx
@@ -1,11 +1,12 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as Client from 'utils/client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import Constants from 'utils/constants.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
+import UserStore from 'stores/user_store.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
@@ -68,25 +69,27 @@ class AdvancedSettingsDisplay extends React.Component {
const preReleaseFeaturesKeys = Object.keys(PreReleaseFeatures);
const advancedSettings = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS);
const settings = {
- send_on_ctrl_enter: PreferenceStore.getPreference(
+ send_on_ctrl_enter: PreferenceStore.get(
Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
'send_on_ctrl_enter',
- {value: 'false'}
- ).value
+ 'false'
+ )
};
let enabledFeatures = 0;
- advancedSettings.forEach((setting) => {
- preReleaseFeaturesKeys.forEach((key) => {
+ for (const [name, value] of advancedSettings) {
+ for (const key of preReleaseFeaturesKeys) {
const feature = PreReleaseFeatures[key];
- if (setting.name === Constants.FeatureTogglePrefix + feature.label) {
- settings[setting.name] = setting.value;
- if (setting.value === 'true') {
- enabledFeatures++;
+
+ if (name === Constants.FeatureTogglePrefix + feature.label) {
+ settings[name] = value;
+
+ if (value === 'true') {
+ enabledFeatures += 1;
}
}
- });
- });
+ }
+ }
this.state = {preReleaseFeatures: PreReleaseFeatures, settings, preReleaseFeaturesKeys, enabledFeatures};
}
@@ -124,20 +127,21 @@ class AdvancedSettingsDisplay extends React.Component {
handleSubmit(settings) {
const preferences = [];
+ const userId = UserStore.getCurrentId();
+ // this should be refactored so we can actually be certain about what type everything is
(Array.isArray(settings) ? settings : [settings]).forEach((setting) => {
- preferences.push(
- PreferenceStore.setPreference(
- Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
- setting,
- String(this.state.settings[setting])
- )
- );
+ preferences.push({
+ user_id: userId,
+ category: Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ name: setting,
+ value: this.state.settings[setting]
+ });
});
- Client.savePreferences(preferences,
+ AsyncClient.savePreferences(
+ preferences,
() => {
- PreferenceStore.emitChange();
this.updateSection('');
},
(err) => {
diff --git a/webapp/components/user_settings/user_settings_display.jsx b/webapp/components/user_settings/user_settings_display.jsx
index 3299588f7..e56156049 100644
--- a/webapp/components/user_settings/user_settings_display.jsx
+++ b/webapp/components/user_settings/user_settings_display.jsx
@@ -6,24 +6,22 @@ import SettingItemMax from '../setting_item_max.jsx';
import ManageLanguages from './manage_languages.jsx';
import ThemeSetting from './user_settings_theme.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
+import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as I18n from 'i18n/i18n.jsx';
import Constants from 'utils/constants.jsx';
+const Preferences = Constants.Preferences;
-import {savePreferences} from 'utils/client.jsx';
import {FormattedMessage} from 'react-intl';
function getDisplayStateFromStores() {
- const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'});
- const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'username'});
- const selectedFont = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', {value: Constants.DEFAULT_FONT});
-
return {
- militaryTime: militaryTime.value,
- nameFormat: nameFormat.value,
- selectedFont: selectedFont.value
+ militaryTime: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', 'false'),
+ nameFormat: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'username'),
+ selectedFont: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT)
};
}
@@ -44,13 +42,29 @@ export default class UserSettingsDisplay extends React.Component {
this.state = getDisplayStateFromStores();
}
handleSubmit() {
- const timePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime);
- const namePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', this.state.nameFormat);
- const fontPreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', this.state.selectedFont);
+ const userId = UserStore.getCurrentId();
+
+ const timePreference = {
+ user_id: userId,
+ category: Preferences.CATEGORY_DISPLAY_SETTINGS,
+ name: 'use_military_time',
+ value: this.state.militaryTime
+ };
+ const namePreference = {
+ user_id: userId,
+ category: Preferences.CATEGORY_DISPLAY_SETTINGS,
+ name: 'name_format',
+ value: this.state.nameFormat
+ };
+ const fontPreference = {
+ user_id: userId,
+ category: Preferences.CATEGORY_DISPLAY_SETTINGS,
+ name: 'selected_font',
+ value: this.state.selectedFont
+ };
- savePreferences([timePreference, namePreference, fontPreference],
+ AsyncClient.savePreferences([timePreference, namePreference, fontPreference],
() => {
- PreferenceStore.emitChange();
this.updateSection('');
},
(err) => {
diff --git a/webapp/components/user_settings/user_settings_modal.jsx b/webapp/components/user_settings/user_settings_modal.jsx
index bd1df6ea5..d1c1f0fe2 100644
--- a/webapp/components/user_settings/user_settings_modal.jsx
+++ b/webapp/components/user_settings/user_settings_modal.jsx
@@ -64,7 +64,6 @@ class UserSettingsModal extends React.Component {
constructor(props) {
super(props);
- this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
this.handleHidden = this.handleHidden.bind(this);
this.handleCollapse = this.handleCollapse.bind(this);
@@ -95,24 +94,13 @@ class UserSettingsModal extends React.Component {
}
componentDidMount() {
- if (this.props.show) {
- this.handleShow();
- }
UserStore.addChangeListener(this.onUserChanged);
}
- componentDidUpdate(prevProps) {
- if (this.props.show && !prevProps.show) {
- this.handleShow();
- }
+ componentDidUpdate() {
UserStore.removeChangeListener(this.onUserChanged);
- }
-
- handleShow() {
- if ($(window).width() > 768) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
- } else {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50);
+ if (!Utils.isMobile()) {
+ $('.settings-modal .modal-body').perfectScrollbar();
}
}
@@ -222,6 +210,10 @@ class UserSettingsModal extends React.Component {
active_section: ''
});
}
+
+ if (!Utils.isMobile()) {
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
+ }
}
updateSection(section, skipConfirm) {
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 9b44f9abd..d9411df07 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -1,12 +1,12 @@
{
"about.close": "Close",
"about.date": "Build Date:",
- "about.enterpriseEditione1": "Enterprise Edition E1",
+ "about.enterpriseEditione1": "Enterprise Edition",
"about.hash": "Build Hash:",
"about.licensed": "Licensed by:",
"about.number": "Build Number:",
- "about.teamEditiont0": "Team Edition T0",
- "about.teamEditiont1": "Team Edition T1",
+ "about.teamEditiont0": "Team Edition",
+ "about.teamEditiont1": "Enterprise Edition",
"about.title": "About Mattermost",
"about.version": "Version:",
"access_history.title": "Access History",
@@ -230,15 +230,9 @@
"admin.ldap.usernameAttrTitle": "Username Attribute:",
"admin.licence.keyMigration": "If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, <a href=\"http://mattermost.com\" target=\"_blank\">disable all Enterprise Edition features on this server</a>. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.",
"admin.license.chooseFile": "Choose File",
- "admin.license.edition": "Edition: ",
- "admin.license.enterpriseEdition": "Mattermost Enterprise Edition. Designed for enterprise-scale communication.",
- "admin.license.enterpriseType": "<div><p>This compiled release of Mattermost platform is provided under a <a href=\"http://mattermost.com\" target=\"_blank\">commercial license</a> from Mattermost, Inc. based on your subscription level and is subject to the <a href=\"{terms}\" target=\"_blank\">Terms of Service.</a></p><p>Your subscription details are as follows:</p>Name: {name}<br />Company or organization name: {company}<br/>Number of users: {users}<br/>License issued: {issued}<br/>Start date of license: {start}<br/>Expiry date of license: {expires}<br/>LDAP: {ldap}<br/></div>",
- "admin.license.key": "License Key: ",
"admin.license.keyRemove": "Remove Enterprise License and Downgrade Server",
"admin.license.noFile": "No file uploaded",
"admin.license.removing": "Removing License...",
- "admin.license.teamEdition": "Mattermost Team Edition. Designed for teams from 5 to 50 users.",
- "admin.license.teamType": "<span><p>This compiled release of Mattermost platform is offered under an MIT license.</p><p>See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.</p></span>",
"admin.license.title": "Edition and License",
"admin.license.type": "License: ",
"admin.license.upload": "Upload",
@@ -416,6 +410,8 @@
"admin.support.emailTitle": "Support email:",
"admin.support.helpDesc": "Link to help documentation from team site main menu. Typically not changed unless your organization chooses to create custom documentation.",
"admin.support.helpTitle": "Help link:",
+ "admin.support.noteDescription": "If linking to an external site, URLs should begin with http:// or https://.",
+ "admin.support.noteTitle": "Note:",
"admin.support.privacyDesc": "Link to Privacy Policy available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.",
"admin.support.privacyTitle": "Privacy Policy link:",
"admin.support.problemDesc": "Link to help documentation from team site main menu. By default this points to the peer-to-peer troubleshooting forum where users can search for, find and request help with technical issues.",
@@ -425,8 +421,6 @@
"admin.support.termsDesc": "Link to Terms of Service available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.",
"admin.support.termsTitle": "Terms of Service link:",
"admin.support.title": "Legal and Support Settings",
- "admin.support.noteTitle": "Note:",
- "admin.support.noteDescription": "If linking to an external site, URLs should begin with http:// or https://.",
"admin.system_analytics.activeUsers": "Active Users With Posts",
"admin.system_analytics.title": "the System",
"admin.system_analytics.totalPosts": "Total Posts",
diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json
index 606b4376d..8852dd3c6 100644
--- a/webapp/i18n/es.json
+++ b/webapp/i18n/es.json
@@ -230,15 +230,9 @@
"admin.ldap.usernameAttrTitle": "Atributo Usuario:",
"admin.licence.keyMigration": "Si estás migrando servidores es posible que necesites remover tu licencia de este servidor para poder instalarlo en un servidor nuevo. Para empezar, <a href=\"http://mattermost.com\" target=\"_blank\">deshabilita todas las características de la Edición Enterprise de este servidor</a>. Esta operación habilitará la opción para remover la licencia y degradar este servidor de la Edición Enterprise a la Edición Team.",
"admin.license.chooseFile": "Escoger Archivo",
- "admin.license.edition": "Edición: ",
- "admin.license.enterpriseEdition": "Mattermost Edición Enterprise. Diseñada para comunicación de escala empresarial.",
- "admin.license.enterpriseType": "<div><p>Esta versión compilada de la plataforma de Mattermost es provista bajo una <a href=\"http://mattermost.com\" target=\"_blank\">licencia comercial</a> de Mattermost, Inc. en función en su nivel de subscripción y bajo los <a href=\"{terms}\" target=\"_blank\">Términos del Servicio.</a></p><p>Los detalles de tu subscripción son los siguientes:</p>Nombre: {name}<br />Nombre compañía u organización: {company}<br/>Cantidad de usuarios: {users}<br/>Licencia emitida: {issued}<br/>Fecha de inicio: {start}<br/>Fecha de expiración: {expires}<br/>LDAP: {ldap}<br/></div>",
- "admin.license.key": "Llave de la Licencia: ",
"admin.license.keyRemove": "Remover la Licencia Enterprise y Degradar el Servidor",
"admin.license.noFile": "No se subió ningún archivo",
"admin.license.removing": "Removiendo Licencia...",
- "admin.license.teamEdition": "Mattermost Edición Team. Diseñado para equipos desde 5 hasta 50 usuarios.",
- "admin.license.teamType": "<span><p>Esta versión compilada de la plataforma de Mattermost es proporcionada bajo la licencia MIT.</p><p>Lea MIT-COMPILED-LICENSE.txt en el directorio raíz de la instalación para más detalles. Lea NOTICES.txt para información sobre software libre utilizado en este sistema.</p></span>",
"admin.license.title": "Edición y Licencia",
"admin.license.type": "Licencia: ",
"admin.license.upload": "Subir",
@@ -416,6 +410,8 @@
"admin.support.emailTitle": "Correo electrónico de Soporte:",
"admin.support.helpDesc": "Enlace con la documentación de ayuda para el equipo desde el menú principal. Normalmente no cambia a menos que tu organización decida crear una documentación personalizada.",
"admin.support.helpTitle": "Enlace de Ayuda:",
+ "admin.support.noteDescription": "Si se enlaza a un sitio externo, las URLs deben comenzar con http:// o https://.",
+ "admin.support.noteTitle": "Nota:",
"admin.support.privacyDesc": "Enlace para las políticas de Privacidad disponible para los usuarios en versión de escritorio y movil. Al dejarlo en blanco esconderá la opción que muestra el aviso.",
"admin.support.privacyTitle": "Enlace de políticas de Privacidad:",
"admin.support.problemDesc": "Enlace con la documentación de ayuda para el equipo desde el menú principal. Como predeterminado esto apunta a un foro de ayuda donde los usuarios pueden buscar, encontrar y solicitar ayuda sobre temas técnicos.",
diff --git a/webapp/i18n/pt.json b/webapp/i18n/pt.json
index 17ffe1b16..41d3bbc1c 100644
--- a/webapp/i18n/pt.json
+++ b/webapp/i18n/pt.json
@@ -1,12 +1,12 @@
{
"about.close": "Fechar",
"about.date": "Data De Criação:",
- "about.enterpriseEditione1": "Enterprise Edition E1",
+ "about.enterpriseEditione1": "Enterprise Edition",
"about.hash": "Hash de Compilação:",
"about.licensed": "Licenciado pela:",
"about.number": "O Número De Compilação:",
- "about.teamEditiont0": "Team Edition T0",
- "about.teamEditiont1": "Team Edition T1",
+ "about.teamEditiont0": "Team Edition",
+ "about.teamEditiont1": "Enterprise Edition",
"about.title": "Sobre o Mattermost",
"about.version": "Versão:",
"access_history.title": "Histórico de Acesso",
@@ -24,6 +24,39 @@
"activity_log_modal.iphoneNativeApp": "App Nativo para iPhone",
"admin.audits.reload": "Recarregar",
"admin.audits.title": "Atividade de Usuário",
+ "admin.compliance.directoryDescription": "Diretório o qual os relatórios compliance são gravados, Se estiver em branco, será usado ./data/.",
+ "admin.compliance.directoryExample": "Ex \"./data/\"",
+ "admin.compliance.directoryTitle": "Localização do Diretório de Compliance:",
+ "admin.compliance.enableDailyTitle": "Ativar Relatório Diário:",
+ "admin.compliance.enableDesc": "Quando verdadeiro, Mattermost irá gerar um relatório diário de compliance.",
+ "admin.compliance.enableTitle": "Ativar Compliance:",
+ "admin.compliance.false": "falso",
+ "admin.compliance.noLicense": "<h4 class=\"banner__heading\">Nota:</h4><p>Compliance é um recurso empresarial. Sua licença atual não suporta Compliance. Clique <a href=\"http://mattermost.com\" target=\"_blank\">aqui</a> para informações e preços da licença empresarial.</p>",
+ "admin.compliance.save": "Salvar",
+ "admin.compliance.saving": "Salvando Config...",
+ "admin.compliance.title": "Configurações Compliance",
+ "admin.compliance.true": "verdadeiro",
+ "admin.compliance_reports.desc": "Nome da Tarefa:",
+ "admin.compliance_reports.desc_placeholder": "Ex \"Audit 445 for HR\"",
+ "admin.compliance_reports.emails": "Emails:",
+ "admin.compliance_reports.emails_placeholder": "Ex \"bill@example.com, bob@example.com\"",
+ "admin.compliance_reports.from": "De:",
+ "admin.compliance_reports.from_placeholder": "Ex \"2016-03-11\"",
+ "admin.compliance_reports.keywords": "Palavras-chave:",
+ "admin.compliance_reports.keywords_placeholder": "Ex \"diminuir estoque\"",
+ "admin.compliance_reports.reload": "Recarregar",
+ "admin.compliance_reports.run": "Executar",
+ "admin.compliance_reports.title": "Relatórios Compliance",
+ "admin.compliance_reports.to": "Para:",
+ "admin.compliance_reports.to_placeholder": "Ex \"2016-03-15\"",
+ "admin.compliance_table.desc": "Descrição",
+ "admin.compliance_table.download": "Download",
+ "admin.compliance_table.params": "Parâmetros",
+ "admin.compliance_table.records": "Registros",
+ "admin.compliance_table.status": "Status",
+ "admin.compliance_table.timestamp": "Timestamp",
+ "admin.compliance_table.type": "Tipo",
+ "admin.compliance_table.userId": "Solicitado Por",
"admin.email.allowEmailSignInDescription": "Quando verdadeiro, Mattermost permite aos usuários fazer login usando o e-mail e senha.",
"admin.email.allowEmailSignInTitle": "Permitir Login Com E-mail: ",
"admin.email.allowSignupDescription": "Quando verdadeiro, Mattermost permite a criação de equipe e conta de inscrição através de e-mail e senha. Este valor deve ser falso somente quando você deseja limitar a entrada para o single-sign-on service como OAuth ou LDAP.",
@@ -42,6 +75,8 @@
"admin.email.emailSettings": "Configuração do e-mail",
"admin.email.emailSuccess": "Nenhum erro foram relatados durante o envio de um e-mail. Por favor verifique a sua caixa de entrada para se certificar.",
"admin.email.false": "falso",
+ "admin.email.fullPushNotification": "Enviar trecho de mensagem",
+ "admin.email.genericPushNotification": "Enviar descrição genérica com nomes do usuário e canal",
"admin.email.inviteSaltDescription": "32-caracteres salt adicionados a assinatura de convites por e-mail. Aleatoriamente gerados na instalação. Click \"Re-Gerar\" para criar um novo salt.",
"admin.email.inviteSaltExample": "Ex \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"",
"admin.email.inviteSaltTitle": "Salt Convite:",
@@ -56,6 +91,8 @@
"admin.email.passwordSaltDescription": "32-caracteres de salt adicionado para assinar o redefinição de senha de e-mails. Gerada aleatoriamente na instalação. Clique em \"Re-Gerar\" para criar novos salt.",
"admin.email.passwordSaltExample": "Ex \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"",
"admin.email.passwordSaltTitle": "Salt Reset Senha:",
+ "admin.email.pushContentDesc": "Selecionar \"Enviar descrição genérica com nomes de usuário e canal\" fornece envio de notificações com mensagens genéricas, inclui nomes de usuários e canais mas não específica detalhes do texto da mensagem.<br /><br />Selecionar \"Enviar trecho da mensagem\" envia trechos da mensagem que desencadearam as notificações e pode incluir informações confidenciais enviadas na mensagem. Se o seu serviço de Envio de Notificação está fora de um firewall, é ALTAMENTE RECOMENDADO que está opção somente seja usada com o protocolo \"https\" para encriptar a conexão.",
+ "admin.email.pushContentTitle": "Enviar Notificação Contendo:",
"admin.email.pushDesc": "Normalmente definida como verdadeiro na produção. Quando verdadeiro, Mattermost tenta enviar notificações no iOS e Android, através do servidor de notificação.",
"admin.email.pushServerDesc": "Localização do serviço de notificação push Mattermost você pode configurar por trás do firewall usando https://github.com/mattermost/push-proxy. Para testar, você pode usar http://push-test.mattermost.com, que liga à amostra do app Mattermost iOS na Apple AppStore pública. Por favor, não use o serviço de teste para implantações de produção.",
"admin.email.pushServerEx": "Ex: \"http://push-test.mattermost.com\"",
@@ -326,6 +363,7 @@
"admin.service.webhooksTitle": "Ativar Webhooks Entrada: ",
"admin.sidebar.addTeamSidebar": "Adicionar equipe do menu lateral",
"admin.sidebar.audits": "Conformidade e Auditoria",
+ "admin.sidebar.compliance": "Configurações Compliance",
"admin.sidebar.email": "Configuração do e-mail",
"admin.sidebar.file": "Configurações do arquivo",
"admin.sidebar.gitlab": "Configurações GitLab",
@@ -387,6 +425,8 @@
"admin.support.termsDesc": "Link para os Termos de Serviço para os usuários no desktop ou móvel. Deixando este espaço em branco irá esconder a opção de exibir um aviso.",
"admin.support.termsTitle": "Link Termos do Serviço:",
"admin.support.title": "Configurações jurídico e apoio",
+ "admin.support.noteTitle": "Note:",
+ "admin.support.noteDescription": "If linking to an external site, URLs should begin with http:// or https://.",
"admin.system_analytics.activeUsers": "Usuários Ativos com Postagens",
"admin.system_analytics.title": "o Sistema",
"admin.system_analytics.totalPosts": "Total Posts",
@@ -660,13 +700,13 @@
"email_signup.emailError": "Por favor introduza um endereço de e-mail válido",
"email_signup.find": "Encontrar minhas equipes",
"email_verify.almost": "{siteName}: Você está quase pronto",
+ "email_verify.failed": " Falha ao enviar verificação por email.",
"email_verify.notVerifiedBody": "Por favor verifique seu endereço de email. Verifique por um email em sua caixa de entrada.",
- "email_verify.verifyFailed": "Falha ao verificar seu email.",
"email_verify.resend": "Re-enviar Email",
"email_verify.sent": " Verificação de email enviado.",
- "email_verify.failed": " Falha ao enviar verificação por email.",
"email_verify.verified": "{siteName} Email Verificado",
"email_verify.verifiedBody": "<p>Seu email foi verificado! Clique <a href={url}>aqui</a> para login.</p>",
+ "email_verify.verifyFailed": "Falha ao verificar seu email.",
"error_bar.preview_mode": "Modo de visualização: Notificações por E-mail não foram configuradas",
"file_attachment.download": "Download",
"file_info_preview.size": "Tamanho ",
@@ -758,9 +798,9 @@
"login.noAccount": "Não tem uma conta? ",
"login.on": "no {siteName}",
"login.or": "ou",
+ "login.session_expired": " Sua sessão expirou. Por favor faça login novamente.",
"login.signTo": "Login em:",
"login.verified": " Email Verificado",
- "login.session_expired": " Sua sessão expirou. Por favor faça login novamente.",
"login_email.badTeam": "Nome ruim de equipe",
"login_email.email": "E-mail",
"login_email.emailReq": "Um email é necessário",
@@ -1102,6 +1142,7 @@
"user.settings.advance.preReleaseTitle": "Visualizar recursos de pré-lançamento",
"user.settings.advance.sendDesc": "Se habilitado 'Enter' insere uma nova linha e 'Ctrl + Enter' envia a mensagem.",
"user.settings.advance.sendTitle": "Enviar mensagens Ctrl + Enter",
+ "user.settings.advance.slashCmd_autocmp": "Ativar aplicação externa para autocompletar comandos slash",
"user.settings.advance.title": "Configurações Avançadas",
"user.settings.cmds.add": "Adicionar",
"user.settings.cmds.add_desc": "Criar comandos slash para enviar eventos para integrações externas e receber uma resposta. Por exemplo digitando `/patient Joe Smith` poderia trazer de volta os resultados de pesquisa a partir do seu sistema de gestão de registos internos de saúde para o nome “Joe Smith”. Por favor veja <a href=\"http://docs.mattermost.com/developer/slash-commands.html\">Documentação comandos Slash</a> para detalhes e instruções. Ver todos os comandos slash configurados nesta equipe abaixo.",
@@ -1130,6 +1171,7 @@
"user.settings.cmds.request_type_desc": "O tipo de solicitação do comando emitido para a URL requisitada.",
"user.settings.cmds.request_type_get": "GET",
"user.settings.cmds.request_type_post": "POST",
+ "user.settings.cmds.slashCmd_autocmp": "Ativar aplicação externa para autocompletar",
"user.settings.cmds.token": "Token: ",
"user.settings.cmds.trigger": "Comando Palavra Gatilho: ",
"user.settings.cmds.trigger_desc": "Exemplos: /patient, /client, /employee Reserved: /echo, /join, /logout, /me, /shrug",
@@ -1313,4 +1355,4 @@
"web.footer.terms": "Termos",
"web.header.back": "Voltar",
"web.root.singup_info": "Toda comunicação em um só lugar, pesquisável e acessível em qualquer lugar"
-}
+} \ No newline at end of file
diff --git a/webapp/package.json b/webapp/package.json
index 464083bd4..6f50962a4 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -11,6 +11,7 @@
"fastclick": "1.0.6",
"flux": "2.1.1",
"highlight.js": "9.2.0",
+ "intl": "1.1.0",
"jasny-bootstrap": "3.1.3",
"jquery": "2.2.1",
"keymirror": "0.1.1",
@@ -53,8 +54,8 @@
"webpack": "webpack/webpack#master"
},
"scripts": {
- "check": "eslint --ext \".jsx\" --ignore-pattern node_modules --quiet .",
- "build": "webpack",
- "run": "webpack --progress --watch"
+ "check": "eslint --ext \".jsx\" --ignore-pattern node_modules --quiet .",
+ "build": "webpack",
+ "run": "webpack --progress --watch"
}
}
diff --git a/webapp/root.jsx b/webapp/root.jsx
index 2ce220f1d..63fbb4422 100644
--- a/webapp/root.jsx
+++ b/webapp/root.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import $ from 'jquery';
+require('perfect-scrollbar/jquery')($);
import 'bootstrap/dist/css/bootstrap.css';
import 'jasny-bootstrap/dist/css/jasny-bootstrap.css';
@@ -24,11 +25,11 @@ import Sidebar from 'components/sidebar.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
-import SocketStore from 'stores/socket_store.jsx';
import ErrorStore from 'stores/error_store.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import SignupTeam from 'components/signup_team.jsx';
import * as Client from 'utils/client.jsx';
+import * as Websockets from 'action_creators/websocket_actions.jsx';
import * as GlobalActions from 'action_creators/global_actions.jsx';
import SignupTeamConfirm from 'components/signup_team_confirm.jsx';
import SignupUserComplete from 'components/signup_user_complete.jsx';
@@ -101,29 +102,43 @@ function preRenderSetup(callwhendone) {
// Do Nothing
};
+ // Make sure the websockets close
$(window).on('beforeunload',
() => {
- if (window.SocketStore) {
- SocketStore.close();
- }
+ Websockets.close();
}
);
- addLocaleData(enLocaleData);
- addLocaleData(esLocaleData);
- addLocaleData(ptLocaleData);
+ function afterIntl() {
+ addLocaleData(enLocaleData);
+ addLocaleData(esLocaleData);
+ addLocaleData(ptLocaleData);
- $.when(d1, d2).done(callwhendone);
+ $.when(d1, d2).done(callwhendone);
+ }
+
+ if (global.Intl) {
+ afterIntl();
+ } else {
+ require.ensure([
+ 'intl',
+ 'intl/locale-data/jsonp/en.js',
+ 'intl/locale-data/jsonp/es.js',
+ 'intl/locale-data/jsonp/pt.js'
+ ], (require) => {
+ require('intl');
+ require('intl/locale-data/jsonp/en.js');
+ require('intl/locale-data/jsonp/es.js');
+ require('intl/locale-data/jsonp/pt.js');
+ afterIntl();
+ });
+ }
}
function preLoggedIn(nextState, replace, callback) {
const d1 = Client.getAllPreferences(
(data) => {
- if (!data) {
- return;
- }
-
- PreferenceStore.setPreferences(data);
+ PreferenceStore.setPreferencesFromServer(data);
},
(err) => {
AsyncClient.dispatchError(err, 'getAllPreferences');
@@ -179,6 +194,7 @@ function onLoggedOut(nextState) {
BrowserStore.signalLogout();
BrowserStore.clear();
ErrorStore.clearLastError();
+ PreferenceStore.clear();
},
() => {
browserHistory.push('/' + teamName + '/login');
diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss
index 94378aabe..4e2049857 100644
--- a/webapp/sass/components/_modal.scss
+++ b/webapp/sass/components/_modal.scss
@@ -9,6 +9,7 @@
}
.modal-body {
+ max-height: calc(90vh - 62px);
overflow: auto;
padding: 20px 15px;
diff --git a/webapp/sass/components/_spinner-button.scss b/webapp/sass/components/_spinner-button.scss
new file mode 100644
index 000000000..e683ad4f4
--- /dev/null
+++ b/webapp/sass/components/_spinner-button.scss
@@ -0,0 +1,8 @@
+@charset 'UTF-8';
+
+.spinner-button__gif {
+ height: 15px;
+ margin-top: 2px;
+ width: 15px;
+}
+
diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss
index 9b631d00f..e75d2556b 100644
--- a/webapp/sass/layout/_headers.scss
+++ b/webapp/sass/layout/_headers.scss
@@ -134,6 +134,10 @@
@include alpha-property(background, $black, .1);
}
+ a {
+ text-decoration: none;
+ }
+
.user__name {
color: $white;
}
diff --git a/webapp/sass/layout/_navigation.scss b/webapp/sass/layout/_navigation.scss
index 87e4b4d27..3daf6e56b 100644
--- a/webapp/sass/layout/_navigation.scss
+++ b/webapp/sass/layout/_navigation.scss
@@ -89,7 +89,7 @@
&.info-popover {
@include background-size(100% 100%);
- background: url('../images/info__icon.png');
+ background-image: url('../images/info__icon.png');
cursor: pointer;
height: 19px;
position: relative;
diff --git a/webapp/sass/layout/_sidebar-left.scss b/webapp/sass/layout/_sidebar-left.scss
index 4c65d0a65..3a5e74570 100644
--- a/webapp/sass/layout/_sidebar-left.scss
+++ b/webapp/sass/layout/_sidebar-left.scss
@@ -1,7 +1,6 @@
@charset 'UTF-8';
.sidebar--left {
- background: #fafafa;
border-right: $border-gray;
height: 100%;
left: 0;
diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss
index 0b47e5ab6..7e53713c0 100644
--- a/webapp/sass/routes/_admin-console.scss
+++ b/webapp/sass/routes/_admin-console.scss
@@ -10,12 +10,8 @@
width: 100%;
}
- .row {
- margin: 0;
- }
-
h3 {
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid alpha-color($black, .1);
font-weight: 600;
margin: 1em 0;
padding-bottom: .5em;
@@ -75,11 +71,18 @@
width: 17px;
}
+ &.divider {
+ @include alpha-property(background, $black, .1);
+ }
+
> a {
&:hover,
- &.active,
&:focus {
- background-color: #eaeaea;
+ @include alpha-property(background, $black, .1);
+ }
+
+ &.active {
+ background-color: transparent;
}
}
diff --git a/webapp/stores/browser_store.jsx b/webapp/stores/browser_store.jsx
index 66190f6a2..bba146e38 100644
--- a/webapp/stores/browser_store.jsx
+++ b/webapp/stores/browser_store.jsx
@@ -8,6 +8,8 @@ function getPrefix() {
return global.window.mm_current_user_id + '_';
}
+ console.log('BrowserStore tried to operate without user present'); //eslint-disable-line no-console
+
return 'unknown_';
}
@@ -34,46 +36,35 @@ class BrowserStoreClass {
}
checkVersion() {
- var currentVersion = sessionStorage.getItem('storage_version');
+ var currentVersion = this.getGlobalItem('storage_version');
if (currentVersion !== global.window.mm_config.Version) {
- sessionStorage.clear();
+ this.clearAll();
try {
- sessionStorage.setItem('storage_version', global.window.mm_config.Version);
+ this.setGlobalItem('storage_version', global.window.mm_config.Version);
} catch (e) {
// Do nothing
}
}
}
- getItem(name, defaultValue) {
- var result = null;
- try {
- result = JSON.parse(sessionStorage.getItem(getPrefix() + name));
- } catch (err) {
- result = null;
- }
-
- if (result === null && typeof defaultValue !== 'undefined') {
- result = defaultValue;
- }
-
- return result;
+ setItem(name, value) {
+ this.setGlobalItem(getPrefix() + name, value);
}
- setItem(name, value) {
- sessionStorage.setItem(getPrefix() + name, JSON.stringify(value));
+ getItem(name, defaultValue) {
+ return this.getGlobalItem(getPrefix() + name, defaultValue);
}
removeItem(name) {
- sessionStorage.removeItem(getPrefix() + name);
+ this.removeGlobalItem(getPrefix() + name);
}
setGlobalItem(name, value) {
try {
if (this.isLocalStorageSupported()) {
- localStorage.setItem(getPrefix() + name, JSON.stringify(value));
+ localStorage.setItem(name, JSON.stringify(value));
} else {
- sessionStorage.setItem(getPrefix() + name, JSON.stringify(value));
+ sessionStorage.setItem(name, JSON.stringify(value));
}
} catch (err) {
console.log('An error occurred while setting local storage, clearing all props'); //eslint-disable-line no-console
@@ -87,9 +78,9 @@ class BrowserStoreClass {
var result = null;
try {
if (this.isLocalStorageSupported()) {
- result = JSON.parse(localStorage.getItem(getPrefix() + name));
+ result = JSON.parse(localStorage.getItem(name));
} else {
- result = JSON.parse(sessionStorage.getItem(getPrefix() + name));
+ result = JSON.parse(sessionStorage.getItem(name));
}
} catch (err) {
result = null;
@@ -104,18 +95,18 @@ class BrowserStoreClass {
removeGlobalItem(name) {
if (this.isLocalStorageSupported()) {
- localStorage.removeItem(getPrefix() + name);
+ localStorage.removeItem(name);
} else {
- sessionStorage.removeItem(getPrefix() + name);
+ sessionStorage.removeItem(name);
}
}
getLastServerVersion() {
- return sessionStorage.getItem('last_server_version');
+ return this.getGlobalItem('last_server_version');
}
setLastServerVersion(version) {
- sessionStorage.setItem('last_server_version', version);
+ this.setGlobalItem('last_server_version', version);
}
signalLogout() {
@@ -185,6 +176,7 @@ class BrowserStoreClass {
const logoutId = sessionStorage.getItem('__logout__');
sessionStorage.clear();
+ localStorage.clear();
if (logoutId) {
sessionStorage.setItem('__logout__', logoutId);
@@ -222,4 +214,3 @@ class BrowserStoreClass {
var BrowserStore = new BrowserStoreClass();
export default BrowserStore;
-window.BrowserStore = BrowserStore;
diff --git a/webapp/stores/error_store.jsx b/webapp/stores/error_store.jsx
index 776375a82..7c695a335 100644
--- a/webapp/stores/error_store.jsx
+++ b/webapp/stores/error_store.jsx
@@ -35,15 +35,15 @@ class ErrorStoreClass extends EventEmitter {
}
getLastError() {
- return BrowserStore.getItem('last_error');
+ return BrowserStore.getGlobalItem('last_error');
}
storeLastError(error) {
- BrowserStore.setItem('last_error', error);
+ BrowserStore.setGlobalItem('last_error', error);
}
getConnectionErrorCount() {
- var count = BrowserStore.getItem('last_error_conn');
+ var count = BrowserStore.getGlobalItem('last_error_conn');
if (count == null) {
return 0;
@@ -53,12 +53,12 @@ class ErrorStoreClass extends EventEmitter {
}
setConnectionErrorCount(count) {
- BrowserStore.setItem('last_error_conn', count);
+ BrowserStore.setGlobalItem('last_error_conn', count);
}
clearLastError() {
- BrowserStore.removeItem('last_error');
- BrowserStore.removeItem('last_error_conn');
+ BrowserStore.removeGlobalItem('last_error');
+ BrowserStore.removeGlobalItem('last_error_conn');
}
}
diff --git a/webapp/stores/notificaiton_store.jsx b/webapp/stores/notificaiton_store.jsx
new file mode 100644
index 000000000..70caffeb6
--- /dev/null
+++ b/webapp/stores/notificaiton_store.jsx
@@ -0,0 +1,98 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import EventEmitter from 'events';
+import Constants from 'utils/constants.jsx';
+import UserStore from './user_store.jsx';
+import ChannelStore from './channel_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'change';
+
+class NotificationStoreClass extends EventEmitter {
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ handleRecievedPost(post, msgProps) {
+ // Send desktop notification
+ if ((UserStore.getCurrentId() !== post.user_id || post.props.from_webhook === 'true') && !Utils.isSystemMessage(post)) {
+ let mentions = [];
+ if (msgProps.mentions) {
+ mentions = JSON.parse(msgProps.mentions);
+ }
+
+ const channel = ChannelStore.get(post.channel_id);
+ const user = UserStore.getCurrentUser();
+ const member = ChannelStore.getMember(post.channel_id);
+
+ let notifyLevel = member && member.notify_props ? member.notify_props.desktop : 'default';
+ if (notifyLevel === 'default') {
+ notifyLevel = user.notify_props.desktop;
+ }
+
+ if (notifyLevel === 'none') {
+ return;
+ } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== Constants.DM_CHANNEL) {
+ return;
+ }
+
+ let username = Utils.localizeMessage('channel_loader.someone', 'Someone');
+ if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') {
+ username = post.props.override_username;
+ } else if (UserStore.hasProfile(post.user_id)) {
+ username = UserStore.getProfile(post.user_id).username;
+ }
+
+ let title = Utils.localizeMessage('channel_loader.posted', 'Posted');
+ if (channel) {
+ title = channel.display_name;
+ }
+
+ let notifyText = post.message.replace(/\n+/g, ' ');
+ if (notifyText.length > 50) {
+ notifyText = notifyText.substring(0, 49) + '...';
+ }
+
+ if (notifyText.length === 0) {
+ if (msgProps.image) {
+ Utils.notifyMe(title, username + Utils.localizeMessage('channel_loader.uploadedImage', ' uploaded an image'), channel);
+ } else if (msgProps.otherFile) {
+ Utils.notifyMe(title, username + Utils.localizeMessage('channel_loader.uploadedFile', ' uploaded a file'), channel);
+ } else {
+ Utils.notifyMe(title, username + Utils.localizeMessage('channel_loader.something', ' did something new'), channel);
+ }
+ } else {
+ Utils.notifyMe(title, username + Utils.localizeMessage('channel_loader.wrote', ' wrote: ') + notifyText, channel);
+ }
+ if (!user.notify_props || user.notify_props.desktop_sound === 'true') {
+ Utils.ding();
+ }
+ }
+ }
+}
+
+var NotificationStore = new NotificationStoreClass();
+
+NotificationStore.dispatchToken = AppDispatcher.register((payload) => {
+ const action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECEIVED_POST:
+ NotificationStore.handleRecievedPost(action.post, action.webspcketMessageProps);
+ NotificationStore.emitChange();
+ break;
+ }
+});
+
+export default NotificationStore;
diff --git a/webapp/stores/preference_store.jsx b/webapp/stores/preference_store.jsx
index df77f0d51..fcfd1c426 100644
--- a/webapp/stores/preference_store.jsx
+++ b/webapp/stores/preference_store.jsx
@@ -4,143 +4,80 @@
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import BrowserStore from './browser_store.jsx';
import EventEmitter from 'events';
-import UserStore from 'stores/user_store.jsx';
const CHANGE_EVENT = 'change';
-function getPreferenceKey(category, name) {
- return `${category}-${name}`;
-}
-
-function getPreferenceKeyForModel(preference) {
- return `${preference.category}-${preference.name}`;
-}
-
class PreferenceStoreClass extends EventEmitter {
constructor() {
super();
- this.getAllPreferences = this.getAllPreferences.bind(this);
- this.get = this.get.bind(this);
- this.getBool = this.getBool.bind(this);
- this.getInt = this.getInt.bind(this);
- this.getPreference = this.getPreference.bind(this);
- this.getCategory = this.getCategory.bind(this);
- this.getPreferencesWhere = this.getPreferencesWhere.bind(this);
- this.setAllPreferences = this.setAllPreferences.bind(this);
- this.setPreference = this.setPreference.bind(this);
-
- this.emitChange = this.emitChange.bind(this);
- this.addChangeListener = this.addChangeListener.bind(this);
- this.removeChangeListener = this.removeChangeListener.bind(this);
-
this.handleEventPayload = this.handleEventPayload.bind(this);
this.dispatchToken = AppDispatcher.register(this.handleEventPayload);
+
+ this.preferences = new Map();
}
- getAllPreferences() {
- return new Map(BrowserStore.getItem('preferences', []));
+ getKey(category, name) {
+ return `${category}--${name}`;
}
get(category, name, defaultValue = '') {
- const preference = this.getAllPreferences().get(getPreferenceKey(category, name));
+ const key = this.getKey(category, name);
- if (!preference) {
+ if (!this.preferences.has(key)) {
return defaultValue;
}
- return preference.value || defaultValue;
+ return this.preferences.get(key);
}
getBool(category, name, defaultValue = false) {
- const preference = this.getAllPreferences().get(getPreferenceKey(category, name));
+ const key = this.getKey(category, name);
- if (!preference) {
+ if (!this.preferences.has(key)) {
return defaultValue;
}
- // prevent a non-false default value from being returned instead of an actual false value
- if (preference.value === 'false') {
- return false;
- }
-
- return (preference.value !== 'false') || defaultValue;
+ return this.preferences.get(key) !== 'false';
}
getInt(category, name, defaultValue = 0) {
- const preference = this.getAllPreferences().get(getPreferenceKey(category, name));
+ const key = this.getKey(category, name);
- if (!preference) {
+ if (!this.preferences.has(key)) {
return defaultValue;
}
- // prevent a non-zero default value from being returned instead of an actual 0 value
- if (preference.value === '0') {
- return 0;
- }
-
- return parseInt(preference.value, 10) || defaultValue;
- }
-
- getPreference(category, name, defaultValue = {}) {
- return this.getAllPreferences().get(getPreferenceKey(category, name)) || defaultValue;
+ return parseInt(this.preferences.get(key), 10);
}
getCategory(category) {
- return this.getPreferencesWhere((preference) => (preference.category === category));
- }
+ const prefix = category + '--';
- getPreferencesWhere(pred) {
- const all = this.getAllPreferences();
- const preferences = [];
+ const preferences = new Map();
- for (const [, preference] of all) {
- if (pred(preference)) {
- preferences.push(preference);
+ for (const [key, value] of this.preferences) {
+ if (key.startsWith(prefix)) {
+ preferences.set(key.substring(prefix.length), value);
}
}
return preferences;
}
- setAllPreferences(preferences) {
- // note that we store the preferences as an array of key-value pairs so that we can deserialize
- // it as a proper Map instead of an object
- BrowserStore.setItem('preferences', [...preferences]);
- }
-
setPreference(category, name, value) {
- const preferences = this.getAllPreferences();
-
- const key = getPreferenceKey(category, name);
- let preference = preferences.get(key);
-
- if (!preference) {
- preference = {
- user_id: UserStore.getCurrentId(),
- category,
- name
- };
- }
- preference.value = value;
-
- preferences.set(key, preference);
-
- this.setAllPreferences(preferences);
-
- return preference;
+ this.preferences.set(this.getKey(category, name), value);
}
- setPreferences(newPreferences) {
- const preferences = this.getAllPreferences();
-
+ setPreferencesFromServer(newPreferences) {
for (const preference of newPreferences) {
- preferences.set(getPreferenceKeyForModel(preference), preference);
+ this.setPreference(preference.category, preference.name, preference.value);
}
+ }
- this.setAllPreferences(preferences);
+ clear() {
+ this.preferences.clear();
}
emitChange() {
@@ -166,7 +103,7 @@ class PreferenceStoreClass extends EventEmitter {
break;
}
case ActionTypes.RECEIVED_PREFERENCES:
- this.setPreferences(action.preferences);
+ this.setPreferencesFromServer(action.preferences);
this.emitChange();
break;
}
diff --git a/webapp/stores/socket_store.jsx b/webapp/stores/socket_store.jsx
deleted file mode 100644
index 5d6302743..000000000
--- a/webapp/stores/socket_store.jsx
+++ /dev/null
@@ -1,343 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import UserStore from './user_store.jsx';
-import PostStore from './post_store.jsx';
-import ChannelStore from './channel_store.jsx';
-import BrowserStore from './browser_store.jsx';
-import ErrorStore from './error_store.jsx';
-import EventEmitter from 'events';
-
-import * as Utils from 'utils/utils.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
-
-import Constants from 'utils/constants.jsx';
-const SocketEvents = Constants.SocketEvents;
-
-const CHANGE_EVENT = 'change';
-
-var conn;
-
-class SocketStoreClass extends EventEmitter {
- constructor() {
- super();
-
- this.initialize = this.initialize.bind(this);
- this.emitChange = this.emitChange.bind(this);
- this.addChangeListener = this.addChangeListener.bind(this);
- this.removeChangeListener = this.removeChangeListener.bind(this);
- this.sendMessage = this.sendMessage.bind(this);
- this.close = this.close.bind(this);
-
- this.failCount = 0;
- this.isInitialize = false;
-
- this.translations = this.getDefaultTranslations();
-
- this.initialize();
- }
-
- initialize() {
- if (!UserStore.getCurrentId()) {
- return;
- }
-
- this.setMaxListeners(0);
-
- if (window.WebSocket && !conn) {
- var protocol = 'ws://';
- if (window.location.protocol === 'https:') {
- protocol = 'wss://';
- }
-
- var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket';
-
- if (this.failCount === 0) {
- console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console
- }
-
- conn = new WebSocket(connUrl);
-
- conn.onopen = () => {
- if (this.failCount > 0) {
- console.log('websocket re-established connection'); //eslint-disable-line no-console
- AsyncClient.getChannels();
- AsyncClient.getPosts(ChannelStore.getCurrentId());
- }
-
- if (this.isInitialize) {
- ErrorStore.clearLastError();
- ErrorStore.emitChange();
- }
-
- this.isInitialize = true;
- this.failCount = 0;
- };
-
- conn.onclose = () => {
- conn = null;
-
- if (this.failCount === 0) {
- console.log('websocket closed'); //eslint-disable-line no-console
- }
-
- this.failCount = this.failCount + 1;
-
- if (this.failCount > 7) {
- ErrorStore.storeLastError({message: this.translations.socketError});
- }
-
- ErrorStore.setConnectionErrorCount(this.failCount);
- ErrorStore.emitChange();
-
- setTimeout(
- () => {
- this.initialize();
- },
- 3000
- );
- };
-
- conn.onerror = (evt) => {
- if (this.failCount <= 1) {
- console.log('websocket error'); //eslint-disable-line no-console
- console.log(evt); //eslint-disable-line no-console
- }
- };
-
- conn.onmessage = (evt) => {
- const msg = JSON.parse(evt.data);
- this.handleMessage(msg);
- this.emitChange(msg);
- };
- }
- }
-
- emitChange(msg) {
- this.emit(CHANGE_EVENT, msg);
- }
-
- addChangeListener(callback) {
- this.on(CHANGE_EVENT, callback);
- }
-
- removeChangeListener(callback) {
- this.removeListener(CHANGE_EVENT, callback);
- }
-
- handleMessage(msg) {
- switch (msg.action) {
- case SocketEvents.POSTED:
- case SocketEvents.EPHEMERAL_MESSAGE:
- handleNewPostEvent(msg, this.translations);
- break;
-
- case SocketEvents.POST_EDITED:
- handlePostEditEvent(msg);
- break;
-
- case SocketEvents.POST_DELETED:
- handlePostDeleteEvent(msg);
- break;
-
- case SocketEvents.NEW_USER:
- handleNewUserEvent();
- break;
-
- case SocketEvents.USER_ADDED:
- handleUserAddedEvent(msg);
- break;
-
- case SocketEvents.USER_REMOVED:
- handleUserRemovedEvent(msg);
- break;
-
- case SocketEvents.CHANNEL_VIEWED:
- handleChannelViewedEvent(msg);
- break;
-
- case SocketEvents.PREFERENCE_CHANGED:
- handlePreferenceChangedEvent(msg);
- break;
-
- default:
- }
- }
-
- sendMessage(msg) {
- if (conn && conn.readyState === WebSocket.OPEN) {
- conn.send(JSON.stringify(msg));
- } else if (!conn || conn.readyState === WebSocket.Closed) {
- conn = null;
- this.initialize();
- }
- }
-
- setTranslations(messages) {
- 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();
- }
- }
-}
-
-function handleNewPostEvent(msg, translations) {
- // Store post
- const post = JSON.parse(msg.props.post);
- GlobalActions.emitPostRecievedEvent(post);
-
- // Update channel state
- if (ChannelStore.getCurrentId() === msg.channel_id) {
- 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);
- }
-
- // Send desktop notification
- if ((UserStore.getCurrentId() !== msg.user_id || post.props.from_webhook === 'true') && !Utils.isSystemMessage(post)) {
- const msgProps = msg.props;
-
- let mentions = [];
- if (msgProps.mentions) {
- mentions = JSON.parse(msg.props.mentions);
- }
-
- const channel = ChannelStore.get(msg.channel_id);
- const user = UserStore.getCurrentUser();
- const member = ChannelStore.getMember(msg.channel_id);
-
- let notifyLevel = member && member.notify_props ? member.notify_props.desktop : 'default';
- if (notifyLevel === 'default') {
- notifyLevel = user.notify_props.desktop;
- }
-
- if (notifyLevel === 'none') {
- return;
- } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== Constants.DM_CHANNEL) {
- return;
- }
-
- let username = translations.someone;
- if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') {
- username = post.props.override_username;
- } else if (UserStore.hasProfile(msg.user_id)) {
- username = UserStore.getProfile(msg.user_id).username;
- }
-
- let title = translations.posted;
- if (channel) {
- title = channel.display_name;
- }
-
- let notifyText = post.message.replace(/\n+/g, ' ');
- if (notifyText.length > 50) {
- notifyText = notifyText.substring(0, 49) + '...';
- }
-
- if (notifyText.length === 0) {
- if (msgProps.image) {
- Utils.notifyMe(title, username + translations.uploadedImage, channel);
- } else if (msgProps.otherFile) {
- Utils.notifyMe(title, username + translations.uploadedFile, channel);
- } else {
- Utils.notifyMe(title, username + translations.something, channel);
- }
- } else {
- Utils.notifyMe(title, username + translations.wrote + notifyText, channel);
- }
- if (!user.notify_props || user.notify_props.desktop_sound === 'true') {
- Utils.ding();
- }
- }
-}
-
-function handlePostEditEvent(msg) {
- // Store post
- const post = JSON.parse(msg.props.post);
- PostStore.storePost(post);
- PostStore.emitChange();
-
- // Update channel state
- if (ChannelStore.getCurrentId() === msg.channel_id) {
- if (window.isActive) {
- AsyncClient.updateLastViewedAt();
- }
- }
-}
-
-function handlePostDeleteEvent(msg) {
- const post = JSON.parse(msg.props.post);
- GlobalActions.emitPostDeletedEvent(post);
-}
-
-function handleNewUserEvent() {
- AsyncClient.getProfiles();
- AsyncClient.getChannelExtraInfo();
-}
-
-function handleUserAddedEvent(msg) {
- if (ChannelStore.getCurrentId() === msg.channel_id) {
- AsyncClient.getChannelExtraInfo();
- }
-
- if (UserStore.getCurrentId() === msg.user_id) {
- AsyncClient.getChannel(msg.channel_id);
- }
-}
-
-function handleUserRemovedEvent(msg) {
- if (UserStore.getCurrentId() === msg.user_id) {
- AsyncClient.getChannels();
-
- if (msg.props.remover_id !== msg.user_id &&
- msg.channel_id === ChannelStore.getCurrentId() &&
- $('#removed_from_channel').length > 0) {
- var sentState = {};
- sentState.channelName = ChannelStore.getCurrent().display_name;
- sentState.remover = UserStore.getProfile(msg.props.remover_id).username;
-
- BrowserStore.setItem('channel-removed-state', sentState);
- $('#removed_from_channel').modal('show');
- }
- } else if (ChannelStore.getCurrentId() === msg.channel_id) {
- AsyncClient.getChannelExtraInfo();
- }
-}
-
-function handleChannelViewedEvent(msg) {
- // Useful for when multiple devices have the app open to different channels
- if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) {
- AsyncClient.getChannel(msg.channel_id);
- }
-}
-
-function handlePreferenceChangedEvent(msg) {
- const preference = JSON.parse(msg.props.preference);
- GlobalActions.emitPreferenceChangedEvent(preference);
-}
-
-var SocketStore = new SocketStoreClass();
-
-export default SocketStore;
-window.SocketStore = SocketStore;
diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx
index 98cc2f3f1..4213e6e8c 100644
--- a/webapp/stores/user_store.jsx
+++ b/webapp/stores/user_store.jsx
@@ -16,7 +16,10 @@ const CHANGE_EVENT_STATUSES = 'change_statuses';
class UserStoreClass extends EventEmitter {
constructor() {
super();
- this.profileCache = null;
+ this.profiles = {};
+ this.statuses = {};
+ this.sessions = {};
+ this.audits = {};
this.currentUserId = '';
}
@@ -135,11 +138,7 @@ class UserStoreClass extends EventEmitter {
}
getProfiles() {
- if (this.profileCache !== null) {
- return this.profileCache;
- }
-
- return BrowserStore.getItem('profiles', {});
+ return this.profiles;
}
getActiveOnlyProfiles(skipCurrent) {
@@ -171,47 +170,38 @@ class UserStoreClass extends EventEmitter {
}
saveProfile(profile) {
- var ps = this.getProfiles();
- ps[profile.id] = profile;
- this.profileCache = ps;
- BrowserStore.setItem('profiles', ps);
+ this.profiles[profile.id] = profile;
}
saveProfiles(profiles) {
const currentId = this.getCurrentId();
- if (this.profileCache) {
- const currentUser = this.profileCache[currentId];
- if (currentUser) {
- if (currentId in profiles) {
- delete profiles[currentId];
- }
-
- this.profileCache = profiles;
- this.profileCache[currentId] = currentUser;
- } else {
- this.profileCache = profiles;
+ const currentUser = this.profiles[currentId];
+ if (currentUser) {
+ if (currentId in this.profiles) {
+ delete this.profiles[currentId];
}
+
+ this.profiles = profiles;
+ this.profiles[currentId] = currentUser;
} else {
- this.profileCache = profiles;
+ this.profiles = profiles;
}
-
- BrowserStore.setItem('profiles', profiles);
}
setSessions(sessions) {
- BrowserStore.setItem('sessions', sessions);
+ this.sessions = sessions;
}
getSessions() {
- return BrowserStore.getItem('sessions', {loading: true});
+ return this.sessions;
}
setAudits(audits) {
- BrowserStore.setItem('audits', audits);
+ this.audits = audits;
}
getAudits() {
- return BrowserStore.getItem('audits', {loading: true});
+ return this.audits;
}
getCurrentMentionKeys() {
@@ -252,7 +242,7 @@ class UserStoreClass extends EventEmitter {
}
pSetStatuses(statuses) {
- BrowserStore.setItem('statuses', statuses);
+ this.statuses = statuses;
}
setStatus(userId, status) {
@@ -263,7 +253,7 @@ class UserStoreClass extends EventEmitter {
}
getStatuses() {
- return BrowserStore.getItem('statuses', {});
+ return this.statuses;
}
getStatus(id) {
diff --git a/webapp/stores/user_typing_store.jsx b/webapp/stores/user_typing_store.jsx
new file mode 100644
index 000000000..ab0a9af1d
--- /dev/null
+++ b/webapp/stores/user_typing_store.jsx
@@ -0,0 +1,108 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import UserStore from 'stores/user_store.jsx';
+import EventEmitter from 'events';
+import * as Utils from 'utils/utils.jsx';
+
+import Constants from 'utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'change';
+
+class UserTypingStoreClass extends EventEmitter {
+ constructor() {
+ super();
+
+ // All typeing users by channel
+ // this.typingUsers.[channelId+postParentId].user if present then user us typing
+ // Value is timeout to remove user
+ this.typingUsers = {};
+ }
+
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ usernameFromId(userId) {
+ let username = Utils.localizeMessage('msg_typing.someone', 'Someone');
+ if (UserStore.hasProfile(userId)) {
+ username = UserStore.getProfile(userId).username;
+ }
+ return username;
+ }
+
+ userTyping(channelId, userId, postParentId) {
+ const username = this.usernameFromId(userId);
+
+ // Key representing a location where users can type
+ const loc = channelId + postParentId;
+
+ // Create entry
+ if (!this.typingUsers[loc]) {
+ this.typingUsers[loc] = {};
+ }
+
+ // If we already have this user, clear it's timeout to be deleted
+ if (this.typingUsers[loc][username]) {
+ clearTimeout(this.typingUsers[loc][username].timeout);
+ }
+
+ // Set the user and a timeout to remove it
+ this.typingUsers[loc][username] = setTimeout(() => {
+ delete this.typingUsers[loc][username];
+ if (this.typingUsers[loc] === {}) {
+ delete this.typingUsers[loc];
+ }
+ this.emitChange();
+ }, Constants.UPDATE_TYPING_MS);
+ this.emitChange();
+ }
+
+ getUsersTyping(channelId, postParentId) {
+ // Key representing a location where users can type
+ const loc = channelId + postParentId;
+
+ return this.typingUsers[loc];
+ }
+
+ userPosted(userId, channelId, postParentId) {
+ const username = this.usernameFromId(userId);
+ const loc = channelId + postParentId;
+
+ if (this.typingUsers[loc]) {
+ clearTimeout(this.typingUsers[loc][username]);
+ delete this.typingUsers[loc][username];
+ if (this.typingUsers[loc] === {}) {
+ delete this.typingUsers[loc];
+ }
+ this.emitChange();
+ }
+ }
+}
+
+var UserTypingStore = new UserTypingStoreClass();
+
+UserTypingStore.dispatchToken = AppDispatcher.register((payload) => {
+ var action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECEIVED_POST:
+ UserTypingStore.userPosted(action.post.user_id, action.post.channel_id, action.post.parent_id);
+ break;
+ case ActionTypes.USER_TYPING:
+ UserTypingStore.userTyping(action.channelId, action.userId, action.postParentId);
+ break;
+ }
+});
+
+export default UserTypingStore;
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index d3f91bb0e..6140fd9e0 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -673,9 +673,9 @@ export function getStatuses() {
const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
const teammateIds = [];
- for (const preference of preferences) {
- if (preference.value === 'true') {
- teammateIds.push(preference.name);
+ for (const [name, value] of preferences) {
+ if (value === 'true') {
+ teammateIds.push(name);
}
}
@@ -756,6 +756,17 @@ export function getAllPreferences() {
);
}
+export function savePreference(category, name, value, success, error) {
+ const preference = {
+ user_id: UserStore.getCurrentId(),
+ category,
+ name,
+ value
+ };
+
+ savePreferences([preference], success, error);
+}
+
export function savePreferences(preferences, success, error) {
client.savePreferences(
preferences,
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index a4aa7604c..859348c73 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -83,6 +83,8 @@ export default {
SHOW_SEARCH: null,
+ USER_TYPING: null,
+
TOGGLE_IMPORT_THEME_MODAL: null,
TOGGLE_INVITE_MEMBER_MODAL: null,
TOGGLE_DELETE_POST_MODAL: null,
@@ -429,6 +431,21 @@ export default {
uiName: 'Mention Highlight Link'
},
{
+ group: 'linkAndButtonElements',
+ id: 'linkColor',
+ uiName: 'Link Color'
+ },
+ {
+ group: 'linkAndButtonElements',
+ id: 'buttonBg',
+ uiName: 'Button BG'
+ },
+ {
+ group: 'linkAndButtonElements',
+ id: 'buttonColor',
+ uiName: 'Button Text'
+ },
+ {
group: 'centerChannelElements',
id: 'codeTheme',
uiName: 'Code Theme',
@@ -458,21 +475,6 @@ export default {
iconURL: monokaiIcon
}
]
- },
- {
- group: 'linkAndButtonElements',
- id: 'linkColor',
- uiName: 'Link Color'
- },
- {
- group: 'linkAndButtonElements',
- id: 'buttonBg',
- uiName: 'Button BG'
- },
- {
- group: 'linkAndButtonElements',
- id: 'buttonColor',
- uiName: 'Button Text'
}
],
DEFAULT_CODE_THEME: 'github',
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index 95a0f99d5..1379455ca 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -314,7 +314,13 @@ export function getTimestamp() {
// extracts links not styled by Markdown
export function extractLinks(text) {
- const links = [];
+ text; // eslint-disable-line no-unused-expressions
+ Autolinker; // eslint-disable-line no-unused-expressions
+
+ // skip this operation because autolinker is having issues
+ return [];
+
+ /*const links = [];
let inText = text;
// strip out code blocks
@@ -348,7 +354,7 @@ export function extractLinks(text) {
}
);
- return links;
+ return links;*/
}
export function escapeRegExp(string) {
@@ -681,7 +687,7 @@ export function applyTheme(theme) {
}
if (theme.centerChannelBg) {
- changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .suggestion-list__content, .modal .modal-content, .modal .modal-back', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .suggestion-list__content, .modal .modal-content', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);