summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--.travis.yml66
-rw-r--r--CONTRIBUTING.md3
-rw-r--r--Makefile296
-rw-r--r--api/context.go2
-rw-r--r--api/post.go36
-rw-r--r--doc/README.md20
-rw-r--r--doc/developer/API.md12
-rw-r--r--doc/developer/Setup.md4
-rw-r--r--doc/help/Markdown.md180
-rw-r--r--doc/help/README.md18
-rw-r--r--doc/help/Team-Settings.md16
-rw-r--r--doc/help/system-console/Team-Statistics.md2
-rw-r--r--doc/integrations/Single-Sign-On/Gitlab.md2
-rw-r--r--doc/process/documentation-guidelines.md11
-rw-r--r--doc/process/overview.md143
-rw-r--r--doc/usage/Markdown.md179
-rw-r--r--web/react/.eslintrc4
-rw-r--r--web/react/components/center_panel.jsx33
-rw-r--r--web/react/components/channel_header.jsx27
-rw-r--r--web/react/components/channel_invite_modal.jsx13
-rw-r--r--web/react/components/channel_loader.jsx4
-rw-r--r--web/react/components/channel_members_modal.jsx14
-rw-r--r--web/react/components/create_post.jsx11
-rw-r--r--web/react/components/delete_post_modal.jsx9
-rw-r--r--web/react/components/edit_channel_header_modal.jsx126
-rw-r--r--web/react/components/edit_channel_modal.jsx150
-rw-r--r--web/react/components/edit_post_modal.jsx4
-rw-r--r--web/react/components/get_link_modal.jsx144
-rw-r--r--web/react/components/get_team_invite_link_modal.jsx45
-rw-r--r--web/react/components/invite_member_modal.jsx36
-rw-r--r--web/react/components/navbar.jsx56
-rw-r--r--web/react/components/navbar_dropdown.jsx9
-rw-r--r--web/react/components/post.jsx34
-rw-r--r--web/react/components/post_focus_view.jsx110
-rw-r--r--web/react/components/post_info.jsx75
-rw-r--r--web/react/components/posts_view.jsx91
-rw-r--r--web/react/components/posts_view_container.jsx129
-rw-r--r--web/react/components/rhs_comment.jsx4
-rw-r--r--web/react/components/rhs_root_post.jsx84
-rw-r--r--web/react/components/rhs_thread.jsx2
-rw-r--r--web/react/components/search_results_item.jsx34
-rw-r--r--web/react/components/sidebar.jsx4
-rw-r--r--web/react/components/sidebar_right_menu.jsx17
-rw-r--r--web/react/components/toggle_modal_button.jsx17
-rw-r--r--web/react/dispatcher/event_helpers.jsx106
-rw-r--r--web/react/package.json3
-rw-r--r--web/react/pages/channel.jsx28
-rw-r--r--web/react/pages/home.jsx3
-rw-r--r--web/react/stores/channel_store.jsx86
-rw-r--r--web/react/stores/modal_store.jsx1
-rw-r--r--web/react/stores/post_store.jsx509
-rw-r--r--web/react/stores/socket_store.jsx34
-rw-r--r--web/react/stores/team_store.jsx11
-rw-r--r--web/react/utils/async_client.jsx346
-rw-r--r--web/react/utils/channel_intro_mssages.jsx78
-rw-r--r--web/react/utils/client.jsx51
-rw-r--r--web/react/utils/constants.jsx9
-rw-r--r--web/react/utils/markdown.jsx47
-rw-r--r--web/react/utils/utils.jsx30
-rw-r--r--web/sass-files/sass/partials/_markdown.scss28
-rw-r--r--web/sass-files/sass/partials/_navbar.scss3
-rw-r--r--web/sass-files/sass/partials/_post.scss30
-rw-r--r--web/templates/channel.html13
-rw-r--r--web/templates/head.html1
-rw-r--r--web/web.go191
66 files changed, 2351 insertions, 1539 deletions
diff --git a/.gitignore b/.gitignore
index fc9076e69..50cdca100 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,9 +6,11 @@ dist
npm-debug.log
web/static/js/bundle*.js
+web/static/js/bundle*.js.map
web/static/js/libs*.js
-model/version.go
-model/version.go.bak
+
+# Build Targets
+.prepare
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
diff --git a/.travis.yml b/.travis.yml
index 1af940d21..7e54d3335 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,42 +1,23 @@
-language: go
-go:
-- 1.4.2
-- 1.5.1
+language: generic
+sudo: required
+services:
+- docker
env:
- TRAVIS_DB=mysql
- TRAVIS_DB=postgres
before_install:
-- gem install compass
-- sudo apt-get update -qq
-- sudo apt-get remove mysql-common mysql-server-5.5 mysql-server-core-5.5 mysql-client-5.5
- mysql-client-core-5.5
-- sudo apt-get autoremove
-- sudo apt-get install libaio1
-- wget -O mysql-5.6.17.deb http://dev.mysql.com/get/Downloads/MySQL-5.6/mysql-5.6.17-debian6.0-x86_64.deb
-- sudo dpkg -i mysql-5.6.17.deb
-- sudo cp /opt/mysql/server-5.6/support-files/mysql.server /etc/init.d/mysql.server
-- sudo ln -s /opt/mysql/server-5.6/bin/* /usr/bin/
-- sudo sed -i'' 's/table_cache/table_open_cache/' /etc/mysql/my.cnf
-- sudo sed -i'' 's/log_slow_queries/slow_query_log/' /etc/mysql/my.cnf
-- sudo sed -i'' 's/basedir[^=]\+=.*$/basedir = \/opt\/mysql\/server-5.6/' /etc/mysql/my.cnf
-- sudo /etc/init.d/mysql.server start
-- sudo pip install mkdocs
-install:
-- export PATH=$PATH:$HOME/gopath/bin
-- go get github.com/tools/godep
-#- godep restore
-before_script:
-- mysql -e "CREATE DATABASE IF NOT EXISTS mattermost_test ;" -uroot
-- mysql -e "CREATE USER 'mmuser'@'%' IDENTIFIED BY 'mostest' ;" -uroot
-- mysql -e "GRANT ALL ON mattermost_test.* TO 'mmuser'@'%' ;" -uroot
-- psql -c "create database mattermost_test ;" -U postgres
-- psql -c "create user mmuser with password 'mostest' ;" -U postgres
-- psql -c 'grant all privileges on database "mattermost_test" to mmuser ;' -U postgres
-services:
-- redis-server
+ - 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
+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
@@ -47,10 +28,9 @@ deploy:
on:
repo: mattermost/platform
tags: true
- go: 1.4.2
condition: $TRAVIS_DB = mysql
-# S3 deploy for master docker image. (latest compiled bits)
+# S3 deploy for latest master
- provider: s3
access_key_id: AKIAJCO3KJYEGWJIKDIQ
secret_access_key:
@@ -64,22 +44,4 @@ deploy:
on:
repo: mattermost/platform
branch: master
- go: 1.4.2
- condition: $TRAVIS_DB = mysql
-
-# S3 deploy for documentation
- - 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: docs.mattermost.org
- local_dir: documentation-html
- acl: public_read
- region: us-east-1
- skip_cleanup: true
- detect_encoding: true
- on:
- repo: mattermost/platform
- branch: master
- go: 1.4.2
condition: $TRAVIS_DB = mysql
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bd571efed..8af1d9efb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -36,6 +36,9 @@ git checkout -b <branch name>
Please review the ["Fast, Obvious, Forgiving" experience design principles](http://www.mattermost.org/design-principles/) for Mattermost and check that your feature meets the criteria. Also, for any changes to user interface or help text, please read the changes out loud, as a quick and easy way to catch any inconsitencies
+3. For new server-side funcitonality, please include test cases that verify the code performs as you have intended
+
+
## Submitting a Pull Request
diff --git a/Makefile b/Makefile
index 573036f06..b3875ffc7 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: all test clean build install run stop cover dist cleandb travis docker
+.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 stop setup-mac cleandb docker-build docker-run
GOPATH ?= $(GOPATH:)
GOFLAGS ?= $(GOFLAGS:)
@@ -21,33 +21,51 @@ endif
DIST_ROOT=dist
DIST_PATH=$(DIST_ROOT)/mattermost
-DIST_RESULTS=$(DIST_ROOT)/results
-BENCH=.
TESTS=.
DOCKERNAME ?= mm-dev
DOCKER_CONTAINER_NAME ?= mm-test
-all: travis
+all: dist-local
-travis:
- @echo building for travis
+dist: | build-server build-client go-test package
+ mv ./model/version.go.bak ./model/version.go
- if [ "$(TRAVIS_DB)" = "postgres" ]; then \
- sed -i'.bak' 's|mysql|postgres|g' config/config.json; \
- sed -i'.bak' 's|mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8|postgres://mmuser:mostest@dockerhost:5432/mattermost_test?sslmode=disable\&connect_timeout=10|g' config/config.json; \
+dist-local: | start-docker dist
+
+dist-travis: | travis-init build-container
+
+start-docker:
+ @echo Starting docker containers
+
+ @if [ $(shell docker ps -a | grep -ci mattermost-mysql) -eq 0 ]; then \
+ echo starting mattermost-mysql; \
+ docker run --name mattermost-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mostest \
+ -e MYSQL_USER=mmuser -e MYSQL_PASSWORD=mostest -e MYSQL_DATABASE=mattermost_test -d mysql:5.7 > /dev/null; \
+ elif [ $(shell docker ps | grep -ci mattermost-mysql) -eq 0 ]; then \
+ echo restarting mattermost-mysql; \
+ docker start mattermost-mysql > /dev/null; \
fi
- rm -Rf $(DIST_ROOT)
- @$(GO) clean $(GOFLAGS) -i ./...
+ @if [ $(shell docker ps -a | grep -ci mattermost-postgres) -eq 0 ]; then \
+ echo starting mattermost-postgres; \
+ docker run --name mattermost-postgres -p 5432:5432 -e POSTGRES_USER=mmuser -e POSTGRES_PASSWORD=mostest \
+ -d postgres:9.4 > /dev/null; \
+ sleep 10; \
+ elif [ $(shell docker ps | grep -ci mattermost-postgres) -eq 0 ]; then \
+ echo restarting mattermost-postgres; \
+ docker start mattermost-postgres > /dev/null; \
+ sleep 10; \
+ fi
- @cd web/react/ && npm install
- cd web/react/ && npm run build-libs
+build-server:
+ @echo Building mattermost server
- @echo Checking for style guide compliance
- cd web/react && $(ESLINT) --ext ".jsx" --ignore-pattern node_modules --quiet .
- @echo Running gofmt
+ 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 \
@@ -57,20 +75,17 @@ travis:
exit 1; \
fi
- @sed -i'.bak' 's|_BUILD_NUMBER_|$(BUILD_NUMBER)|g' ./model/version.go
- @sed -i'.bak' 's|_BUILD_DATE_|$(BUILD_DATE)|g' ./model/version.go
- @sed -i'.bak' 's|_BUILD_HASH_|$(BUILD_HASH)|g' ./model/version.go
+ 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
+ rm ./model/version.go.make_mac_work
- @$(GO) build $(GOFLAGS) ./...
- @$(GO) install $(GOFLAGS) ./...
+ $(GO) build $(GOFLAGS) ./...
+ $(GO) install $(GOFLAGS) ./...
- @mkdir -p logs
-
- @$(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
+package:
+ @ echo Packaging mattermost
mkdir -p $(DIST_PATH)/bin
cp $(GOPATH)/bin/platform $(DIST_PATH)/bin
@@ -81,13 +96,9 @@ travis:
mkdir -p $(DIST_PATH)/logs
- mkdir -p web/static/js
- cd web/react && npm run build
-
- cd web/sass-files && compass compile -e production --force
-
mkdir -p $(DIST_PATH)/web/static/js
cp -L web/static/js/*.min.js $(DIST_PATH)/web/static/js/
+ cp -L web/static/js/*.min.js.map $(DIST_PATH)/web/static/js/
cp -RL web/static/config $(DIST_PATH)/web/static
cp -RL web/static/css $(DIST_PATH)/web/static
cp -RL web/static/fonts $(DIST_PATH)/web/static
@@ -106,87 +117,76 @@ travis:
mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js
mv $(DIST_PATH)/web/static/js/libs.min.js $(DIST_PATH)/web/static/js/libs-$(BUILD_NUMBER).min.js
- @sed -i'.bak' 's|react-0.14.0.js|react-0.14.0.min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|react-dom-0.14.0.js|react-dom-0.14.0.min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|react-bootstrap-0.27.1.js|react-bootstrap-0.27.1.min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
+ sed -i'.bak' 's|react-0.14.0.js|react-0.14.0.min.js|g' $(DIST_PATH)/web/templates/head.html
+ sed -i'.bak' 's|react-dom-0.14.0.js|react-dom-0.14.0.min.js|g' $(DIST_PATH)/web/templates/head.html
+ sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html
+ sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
+ sed -i'.bak' 's|react-bootstrap-0.27.1.js|react-bootstrap-0.27.1.min.js|g' $(DIST_PATH)/web/templates/head.html
+ sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html
+ sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
+ sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
rm $(DIST_PATH)/web/templates/*.bak
- mv doc/README.md doc/index.md
- mkdocs build --strict
- cp -r documentation-html $(DIST_PATH)/documentation-html
-
tar -C dist -czf $(DIST_PATH).tar.gz mattermost
- rm -r $(DIST_PATH)
-build:
- @$(GO) build $(GOFLAGS) ./...
+build-client:
+ @echo Building mattermost web client
-install:
- @go get $(GOFLAGS) github.com/tools/godep
+ cd web/react/ && npm install
- @if [ $(shell docker ps -a | grep -ci mattermost-mysql) -eq 0 ]; then \
- echo starting mattermost-mysql; \
- docker run --name mattermost-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mostest \
- -e MYSQL_USER=mmuser -e MYSQL_PASSWORD=mostest -e MYSQL_DATABASE=mattermost_test -d mysql > /dev/null; \
- elif [ $(shell docker ps | grep -ci mattermost-mysql) -eq 0 ]; then \
- echo restarting mattermost-mysql; \
- docker start mattermost-mysql > /dev/null; \
- fi
+ @echo Checking for style guide compliance
- @cd web/react/ && npm install
- @cd web/react/ && npm run build-libs
+ @echo ESLint...
+ cd web/react && $(ESLINT) --ext ".jsx" --ignore-pattern node_modules --quiet .
-check: install
- @echo Running ESLint...
- -cd web/react && $(ESLINT) --ext ".jsx" --ignore-pattern node_modules .
- @echo Running 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; \
+ cd web/react/ && npm run build-libs
+
+ mkdir -p web/static/js
+ cd web/react && npm run build
+
+ cd web/sass-files && compass compile -e production --force
+
+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 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'.bak' '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
-test: install
- @mkdir -p logs
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./api || exit 1
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s ./model || exit 1
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./store || exit 1
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./utils || exit 1
- @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=600s ./web || exit 1
+ 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
-benchmark: install
- @mkdir -p logs
- @$(GO) test $(GOFLAGS) -test.v -run=NO_TESTS -bench=$(BENCH) ./api || exit 1
+build-container:
+ @echo Building in container
-cover: install
- rm -Rf $(DIST_RESULTS)
- mkdir -p $(DIST_RESULTS)
+ docker run --link mattermost-mysql:mysql --link mattermost-postgres:postgres -v `pwd`:/go/src/github.com/mattermost/platform mattermost/builder:latest
- @$(GO) test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/api.cover.out github.com/mattermost/platform/api
- @$(GO) test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/model.cover.out github.com/mattermost/platform/model
- @$(GO) test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/store.cover.out github.com/mattermost/platform/store
- @$(GO) test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/utils.cover.out github.com/mattermost/platform/utils
- @$(GO) test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/web.cover.out github.com/mattermost/platform/web
+stop-docker:
+ @echo Stopping docker containers
- cd $(DIST_RESULTS) && \
- echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \
- awk '{if($$1 != last) {print $$0;last=$$1}}' >> coverage.out
+ @if [ $(shell docker ps -a | grep -ci mattermost-mysql) -eq 1 ]; then \
+ echo stopping mattermost-mysql; \
+ docker stop mattermost-mysql > /dev/null; \
+ fi
- cd $(DIST_RESULTS) && $(GO) tool cover -html=coverage.out -o=coverage.html
+ @if [ $(shell docker ps -a | grep -ci mattermost-postgres) -eq 1 ]; then \
+ echo stopping mattermost-postgres; \
+ docker stop mattermost-postgres > /dev/null; \
+ fi
- rm -f $(DIST_RESULTS)/*.cover.out
-
-clean:
- rm -Rf $(DIST_ROOT)
- @$(GO) clean $(GOFLAGS) -i ./...
+clean-docker:
+ @echo Removing docker containers
@if [ $(shell docker ps -a | grep -ci mattermost-mysql) -eq 1 ]; then \
echo stopping mattermost-mysql; \
@@ -194,29 +194,55 @@ clean:
docker rm -v mattermost-mysql > /dev/null; \
fi
+ @if [ $(shell docker ps -a | grep -ci mattermost-postgres) -eq 1 ]; then \
+ echo stopping mattermost-postgres; \
+ docker stop mattermost-postgres > /dev/null; \
+ docker rm -v mattermost-postgres > /dev/null; \
+ fi
+
+clean: stop-docker
+ rm -Rf $(DIST_ROOT)
+ go clean $(GOFLAGS) -i ./...
+
rm -rf web/react/node_modules
rm -f web/static/js/bundle*.js
+ rm -f web/static/js/bundle*.js.map
rm -f web/static/js/libs*.js
rm -f web/static/css/styles.css
- rm -rf data/*
- rm -rf api/data/*
- rm -rf logs/*
+ rm -rf api/data
+ rm -rf logs
+ rm -rf web/sass-files/.sass-cache
rm -rf Godeps/_workspace/pkg/
+ rm -f mattermost.log
+ rm -f .prepare
+
+nuke: | clean clean-docker
+ rm -rf data
+
+.prepare:
+ @echo Preparation for run step
+
+ go get $(GOFLAGS) github.com/tools/godep
+
+ cd web/react/ && npm install
+ cd web/react/ && npm run build-libs
+
+ touch $@
-run: install
+run: start-docker .prepare
mkdir -p web/static/js
- @echo starting react processor
- @cd web/react && npm start &
+ @echo Starting react processor
+ cd web/react && npm start &
- @echo starting go web server
- @$(GO) run $(GOFLAGS) mattermost.go -config=config.json &
+ @echo Starting go web server
+ $(GO) run $(GOFLAGS) mattermost.go -config=config.json &
- @echo starting compass watch
- @cd web/sass-files && compass watch &
+ @echo Starting compass watch
+ cd web/sass-files && compass watch &
stop:
@for PID in $$(ps -ef | grep [c]ompass | awk '{ print $$2 }'); do \
@@ -248,60 +274,6 @@ cleandb:
docker stop mattermost-mysql > /dev/null; \
docker rm -v mattermost-mysql > /dev/null; \
fi
-dist: install
-
- @sed -i'.bak' 's|_BUILD_NUMBER_|$(BUILD_NUMBER)|g' ./model/version.go
- @sed -i'.bak' 's|_BUILD_DATE_|$(BUILD_DATE)|g' ./model/version.go
- @sed -i'.bak' 's|_BUILD_HASH_|$(BUILD_HASH)|g' ./model/version.go
-
- @$(GO) build $(GOFLAGS) -i ./...
- @$(GO) install $(GOFLAGS) ./...
-
- mkdir -p $(DIST_PATH)/bin
- cp $(GOPATH)/bin/platform $(DIST_PATH)/bin
-
- cp -RL config $(DIST_PATH)/config
- touch $(DIST_PATH)/config/build.txt
- echo $(BUILD_NUMBER) | tee -a $(DIST_PATH)/config/build.txt
-
- mkdir -p $(DIST_PATH)/logs
-
- mkdir -p web/static/js
- cd web/react && npm run build
-
- cd web/sass-files && compass compile -e production --force
-
- mkdir -p $(DIST_PATH)/web/static/js
- cp -L web/static/js/*.min.js $(DIST_PATH)/web/static/js/
- cp -RL web/static/config $(DIST_PATH)/web/static
- cp -RL web/static/css $(DIST_PATH)/web/static
- cp -RL web/static/fonts $(DIST_PATH)/web/static
- cp -RL web/static/help $(DIST_PATH)/web/static
- cp -RL web/static/images $(DIST_PATH)/web/static
- cp -RL web/static/js/jquery-dragster $(DIST_PATH)/web/static/js/
- cp -RL web/templates $(DIST_PATH)/web
-
- mkdir -p $(DIST_PATH)/api
- cp -RL api/templates $(DIST_PATH)/api
-
- cp build/MIT-COMPILED-LICENSE.md $(DIST_PATH)
- cp NOTICE.txt $(DIST_PATH)
- cp README.md $(DIST_PATH)
-
- mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js
- mv $(DIST_PATH)/web/static/js/libs.min.js $(DIST_PATH)/web/static/js/libs-$(BUILD_NUMBER).min.js
-
- @sed -i'.bak' 's|react-0.14.0.js|react-0.14.0.min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|react-dom-0.14.0.js|react-dom-0.14.0.min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|react-bootstrap-0.27.1.js|react-bootstrap-0.27.1.min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
- @sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
- rm $(DIST_PATH)/web/templates/*.bak
-
- tar -C dist -czf $(DIST_PATH).tar.gz mattermost
docker-build: stop
docker build -t ${DOCKERNAME} -f docker/local/Dockerfile .
diff --git a/api/context.go b/api/context.go
index a5d4169cb..a6f9bc1e1 100644
--- a/api/context.go
+++ b/api/context.go
@@ -37,6 +37,8 @@ type Page struct {
ClientCfg map[string]string
User *model.User
Team *model.Team
+ Channel *model.Channel
+ PostID string
SessionTokenIndex int64
}
diff --git a/api/post.go b/api/post.go
index 0860fd299..ca99eb15b 100644
--- a/api/post.go
+++ b/api/post.go
@@ -23,6 +23,7 @@ func InitPost(r *mux.Router) {
l4g.Debug("Initializing post api routes")
r.Handle("/posts/search", ApiUserRequired(searchPosts)).Methods("GET")
+ r.Handle("/posts/{post_id}", ApiUserRequired(getPostById)).Methods("GET")
sr := r.PathPrefix("/channels/{id:[A-Za-z0-9]+}").Subrouter()
sr.Handle("/create", ApiUserRequired(createPost)).Methods("POST")
@@ -767,6 +768,41 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
+func getPostById(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+
+ postId := params["post_id"]
+ if len(postId) != 26 {
+ c.SetInvalidParam("getPostById", "postId")
+ return
+ }
+
+ if result := <-Srv.Store.Post().Get(postId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ list := result.Data.(*model.PostList)
+
+ if len(list.Order) != 1 {
+ c.Err = model.NewAppError("getPostById", "Unable to get post", "")
+ return
+ }
+ post := list.Posts[list.Order[0]]
+
+ cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId)
+ if !c.HasPermissionsToChannel(cchan, "getPostById") {
+ return
+ }
+
+ if HandleEtag(list.Etag(), w, r) {
+ return
+ }
+
+ w.Header().Set(model.HEADER_ETAG_SERVER, list.Etag())
+ w.Write([]byte(list.ToJson()))
+ }
+}
+
func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
diff --git a/doc/README.md b/doc/README.md
index d062dee65..15d1d731b 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -51,22 +51,4 @@ Procedures for upgrading the Mattermost server
## Help
-_Note: End user help documentation is a new feature being completed for the v1.2 release. The materials below are work in progress._
-
-- Getting Started
- - [Sign-in](help/Sign-in.md)
-
-- User Interface
- - Main Menu
- - [Team Settings ](https://github.com/mattermost/platform/blob/help-docs-update/doc/help/Team-Settings.md)
- - [General Settings](https://github.com/mattermost/platform/blob/help-docs-update/doc/help/Team-Settings.md#general)
- - [Slack Import](https://github.com/mattermost/platform/blob/help-docs-update/doc/help/Team-Settings.md#import-from-slack-beta)
- - [Manage Members](help/Manage-Members.md)
- - Messaging
- - [Mattermost Markdown Formatting](usage/Markdown.md)
- - [Search](help/Search.md)
-
-- System Console
- - Team
- - [Team Statistics](help/system-console/Team-Statistics.md)
-
+See [End User Help](help/README.md).
diff --git a/doc/developer/API.md b/doc/developer/API.md
index e5e5db2ba..1be3669ab 100644
--- a/doc/developer/API.md
+++ b/doc/developer/API.md
@@ -33,3 +33,15 @@ Mattermost drivers offer access to the Mattermost web service API in different l
### [Golang Driver](https://github.com/mattermost/platform/blob/master/model/client.go)
[client.go](https://github.com/mattermost/platform/blob/master/model/client.go) - This is a RESTful driver connecting with the Golang-based webservice of Mattermost and is used by unit tests.
+
+## Building API Integration
+
+If you're building a deep integration with Mattermost, for example a mobile native client, and there is a driver available to support the programming language you are using, it's best to use the driver available to access the [Mattermost Web Service APIs](API-Web-Service.md).
+
+If no driver is available for the programming language of your choice, you can view the [Golang Driver](https://github.com/mattermost/platform/blob/master/model/client.go) source code to understand how it exercises the Web Service API. You can also learn more by reviewing open source projects that use the Web Service API, like [matterircd](https://github.com/42wim/matterircd).
+
+There are a wide range of [installation guides](www.mattermost.org/installation/) for setting up your own Mattermost server on which to develop and test your integrations.
+
+
+
+
diff --git a/doc/developer/Setup.md b/doc/developer/Setup.md
index e78d4dff2..882aac530 100644
--- a/doc/developer/Setup.md
+++ b/doc/developer/Setup.md
@@ -11,7 +11,7 @@ Developer Machine Setup
`docker-machine ip dev`
3. Add a line to your /etc/hosts that goes `<Docker IP> dockerhost`
4. Run `docker-machine env dev` and copy the export statements to your ~/.bash_profile
-2. Download Go (version 1.4.2 or 1.5.1. Final release bits are built with 1.4.2) from http://golang.org/dl/
+2. Download Go 1.5.1 from http://golang.org/dl/
3. Set up your Go workspace
1. `mkdir ~/go`
2. Add the following to your ~/.bash_profile
@@ -53,7 +53,7 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org
`127.0.0.1 dockerhost`
3. Install build essentials
1. `apt-get install build-essential`
-4. Download Go (version 1.4.2 or 1.5.1. Final release bits are built with 1.4.2) from http://golang.org/dl/
+4. Download Go 1.5.1 from http://golang.org/dl/
5. Set up your Go workspace and add Go to the PATH
1. `mkdir ~/go`
2. Add the following to your ~/.bashrc
diff --git a/doc/help/Markdown.md b/doc/help/Markdown.md
new file mode 100644
index 000000000..1befed8d4
--- /dev/null
+++ b/doc/help/Markdown.md
@@ -0,0 +1,180 @@
+# Markdown Help
+
+Markdown makes it easy to format messages. Type a message as you normally would, and use these rules to render it with special formatting.
+
+## Text Style:
+
+You can use either `_` or `*` around a word to make it italic. Use two to make it bold.
+
+* `_italics_` renders as _italics_
+* `**bold**` renders as **bold**
+* `**_bold-italic_**` renders as **_bold-italics_**
+* `~~strikethrough~~` renders as ~~strikethrough~~
+
+## Code Block:
+
+Create a code block by indenting each line by four spaces, or by placing ``` on the line above and below your code.
+
+Example:
+
+ ```
+ code block
+ ```
+
+Renders as:
+```
+code block
+```
+
+### Syntax Highlighting
+
+To add syntax highlighting, type the language to be highlighted after the ``` at the beginning of the code block.
+
+Supported languages are:
+`diff, apache, makefile, http, json, markdown, javascript, css, nginx, objectivec, python, xml, perl, bash, php, coffee (CoffeeScript), cs (C#), cpp (C++), sql, go, ruby, java, ini, latex`
+
+Example:
+
+ ``` go
+ package main
+ import "fmt"
+ func main() {
+ fmt.Println("Hello, 世界")
+ }
+ ```
+
+Renders as:
+``` go
+package main
+import "fmt"
+func main() {
+ fmt.Println("Hello, 世界")
+}
+```
+
+## In-line Code:
+
+Create in-line monospaced font by surrounding it with backticks.
+```
+`monospace`
+```
+Renders as: `monospace`.
+
+## Links:
+
+Create labeled links by putting the desired text in square brackets and the associated link in normal brackets.
+
+`[Check out Mattermost!](www.mattermost.com)`
+
+Renders as: [Check out Mattermost!](www.mattermost.com)
+
+## In-line Images
+
+Create in-line images using an `!` followed by the alt text in square brackets and the link in normal brackets. Add hover text by placing it in quotes after the link.
+```
+![alt text](link "hover text")
+
+and
+
+[![Build Status](https://travis-ci.org/mattermost/platform.svg?branch=master)](https://travis-ci.org/mattermost/platform) [![Github](https://assets-cdn.github.com/favicon.ico)](https://github.com/mattermost/platform)
+```
+Renders as:
+
+![alt text](link "hover text")
+
+and
+
+[![Build Status](https://travis-ci.org/mattermost/platform.svg?branch=master)](https://travis-ci.org/mattermost/platform) [![Github](https://assets-cdn.github.com/favicon.ico)](https://github.com/mattermost/platform)
+
+## Emojis
+
+Check out a full list of emojis [here](http://www.emoji-cheat-sheet.com/).
+
+```
+:smile: :+1: :sheep:
+```
+Renders as:
+:smile: :+1: :sheep:
+
+## Lines:
+
+Create a line by using three `*`, `_`, or `-`.
+
+`***` renders as:
+***
+
+## Block quotes:
+
+Create block quotes using `>`.
+
+`> block quotes` renders as:
+> block quotes
+
+## Lists:
+
+Create a list by using `*` or `-` as bullets. Indent a bullet point by adding two spaces in front of it.
+```
+* list item one
+* list item two
+ * item two sub-point
+```
+Renders as:
+* list item one
+* list item two
+ * item two sub-point
+
+Make it an ordered list by using numbers instead:
+```
+1. Item one
+2. Item two
+```
+Renders as:
+1. Item one
+2. Item two
+
+## Tables:
+
+Create a table by placing a dashed line under the header row and separating the columns with a pipe `|`. (The columns don’t need to line up exactly for it to work). Choose how to align table columns by including colons `:` within the header row.
+```
+| Left-Aligned | Center Aligned | Right Aligned |
+| :------------ |:---------------:| -----:|
+| Left column 1 | this text | $100 |
+| Left column 2 | is | $10 |
+| Left column 3 | centered | $1 |
+```
+
+Renders as:
+
+| Left-Aligned | Center Aligned | Right Aligned |
+| :------------ |:---------------:| -----:|
+| Left column 1 | this text | $100 |
+| Left column 2 | is | $10 |
+| Left column 3 | centered | $1 |
+
+## Headings:
+
+Make a heading by typing # and a space before your title. For smaller headings, use more #’s.
+```
+# Large heading
+## Smaller heading
+### Even smaller heading
+```
+Renders as:
+# Large Heading
+## Smaller Heading
+### Even smaller heading
+
+Alternatively, for the large heading you can underline the text using `===`. For the smaller heading you can underline using `---`
+```
+Large Heading
+=============
+
+Smaller Heading
+--------------
+```
+Renders as:
+Large Heading
+=============
+
+Smaller Heading
+--------------
diff --git a/doc/help/README.md b/doc/help/README.md
new file mode 100644
index 000000000..3b3c1709b
--- /dev/null
+++ b/doc/help/README.md
@@ -0,0 +1,18 @@
+## Help
+
+- Getting Started
+ - [Sign-in](Sign-in.md)
+
+- User Interface
+ - Main Menu
+ - [Team Settings ](https://github.com/mattermost/platform/blob/help-docs-update/doc/help/Team-Settings.md)
+ - [General Settings](https://github.com/mattermost/platform/blob/help-docs-update/doc/help/Team-Settings.md#general)
+ - [Slack Import](https://github.com/mattermost/platform/blob/help-docs-update/doc/help/Team-Settings.md#import-from-slack-beta)
+ - [Manage Members](Manage-Members.md)
+ - Messaging
+ - [Mattermost Markdown Formatting](help/Markdown.md)
+ - [Search](Search.md)
+
+- System Console
+ - Team
+ - [Team Statistics](system-console/Team-Statistics.md)
diff --git a/doc/help/Team-Settings.md b/doc/help/Team-Settings.md
index 7e6cf5dd5..7c8665565 100644
--- a/doc/help/Team-Settings.md
+++ b/doc/help/Team-Settings.md
@@ -1,6 +1,6 @@
## Team Settings
-The Team Settings menu offers Team Administrators, Team Owners and System Administrators to adjust settings applying to a specific team.
+The Team Settings menu offers Team Administrators, Team Owners and System Administrators the ability to adjust settings applied to a specific team.
The following settings are found in a Team Site from the **Three-Dot** menu at the top of the left sidebar under **Team Settings**.
@@ -14,7 +14,7 @@ Your **Team Name** is displayed on the sign-in page, and in the top of the left-
#### Allow anyone to sign-up from login page
-Setting this option to **Yes** a link to the account creation page is included on the sign-in page of this team.
+Setting this option to **Yes** shows a link to the account creation page on the sign-in page of this team.
Team Administrators would set this to **Yes** when they:
1. Operate on a closed network and want to make sign-up easy.
@@ -22,7 +22,7 @@ Team Administrators would set this to **Yes** when they:
3. Operate on the open internet and want to allow anyone to sign-up.
Team Administrators would set this to **No** when they:
- 1. Operate on the open internet and want a small, private team that is email-invite-only
+ 1. Operate on the open internet and want a small, private team that is email-invite-only.
#### Include this team in the Team Directory
@@ -34,7 +34,7 @@ Team Administrators would set this to **Yes** when they:
3. Operate on the open internet and want to allow anyone to sign-up to their team from the Home Page of the Mattermost server.
Team Administrators would set this to **No** when they:
- 1. Operate on the open internet and want a small, private team that is email-invite-only
+ 1. Operate on the open internet and want a small, private team that is email-invite-only.
#### Invite Code
@@ -48,15 +48,15 @@ When allowing anyone to sign-up from the login page, the **Invite Code** is used
The Slack Import feature in Mattermost is in "Beta" and focus is on supporting migration of teams of less than 100 registered users. To use:
-1. Generate a Slack "Export" file from **Slack > Team Settings > Import/Export Data > Export > Start Export**
+1. Generate a Slack "Export" file from **Slack > Team Settings > Import/Export Data > Export > Start Export**.
2. In Mattermost go to **Team Settings > Import > Import from Slack**. _Team Owner_ or _Team Administrator_ role is required to access this menu option.
3. Click **Select file** to upload Slack export file and click **Import**.
-4. Emails and usernames from Slack are used to create new Mattermost accounts
+4. Emails and usernames from Slack are used to create new Mattermost accounts.
-5. Slack users can activate their new Mattermost accounts by using Mattermost's Password Reset screen with their email addresses from Slack to set new passwords for their Mattermost accounts
+5. Slack users can activate their new Mattermost accounts by using Mattermost's Password Reset screen with their email addresses from Slack to set new passwords for their Mattermost accounts.
6. Once logged in, the Mattermost users will have access to previous Slack messages in the public channels imported from Slack.
@@ -67,4 +67,4 @@ The Slack Import feature in Mattermost is in "Beta" and focus is on supporting m
- Newly added markdown suppport in Slack's Posts 2.0 feature announced on September 28, 2015 is not yet supported.
- Slack does not export files or images your team has stored in Slack's database. Mattermost will provide links to the location of your assets in Slack's web UI.
- Slack does not export any content from private groups or direct messages that your team has stored in Slack's database.
-- In Beta, Slack accounts with username or email address collisions with existing Mattermost accounts will not import and mentions do not resolve as Mattermost usernames (still shows Slack ID). No pre-check or roll-back is currently offered.
+- In Beta, Slack accounts with usernames or email addresses identical to existing Mattermost accounts will not import and mentions do not resolve as Mattermost usernames (still shows Slack ID). No pre-check or roll-back is currently offered.
diff --git a/doc/help/system-console/Team-Statistics.md b/doc/help/system-console/Team-Statistics.md
index eef7b8346..5e4bcb5c5 100644
--- a/doc/help/system-console/Team-Statistics.md
+++ b/doc/help/system-console/Team-Statistics.md
@@ -1,6 +1,6 @@
# Team Statistics
-Statistics on users, posts and channels are tracked for each team are viewable under **System Console** > **Teams** > **Statistics**.
+Statistics on users, posts and channels are tracked for each team and are viewable under **System Console** > **Teams** > **Statistics**.
## Total Users
The total number of accounts created, including both active and inactive accounts.
diff --git a/doc/integrations/Single-Sign-On/Gitlab.md b/doc/integrations/Single-Sign-On/Gitlab.md
index 1242fd13e..f0acc0e66 100644
--- a/doc/integrations/Single-Sign-On/Gitlab.md
+++ b/doc/integrations/Single-Sign-On/Gitlab.md
@@ -9,7 +9,7 @@ Follow these steps to configure Mattermost to use GitLab as a single-sign-on (SS
(Note: If your GitLab instance is set up to use SSL, your URIs must begin with https://. Otherwise, use http://).
-3. Submit the application and copy the given _Id_ and _Secret_ into the appropriate _SSOSettings_ fields in config/config.json
+3. Submit the application and copy the given _Id_ and _Secret_ into the appropriate _GitLabSettings_ fields in config/config.json
4. Also in config/config.json, set _Enable_ to `true` for the _gitlab_ section, leave _Scope_ blank and use the following for the endpoints:
* _AuthEndpoint_: `https://<your-gitlab-url>/oauth/authorize` (example https://example.com/oauth/authorize)
diff --git a/doc/process/documentation-guidelines.md b/doc/process/documentation-guidelines.md
index e75bb3169..cbfeec850 100644
--- a/doc/process/documentation-guidelines.md
+++ b/doc/process/documentation-guidelines.md
@@ -86,14 +86,21 @@ For readability and clear layout, end a sub-section heading with a colon
##### Correct:
----
+
Service Based:
+
- [AWS Elastic Beanstalk Setup](https://github.com/mattermost/platform/blob/master/doc/install/Amazon-Elastic-Beanstalk.md)
+
----
##### Incorrect:
----
-Optional
-- [Community Guide for Production Debian Setup](https://github.com/mattermost/platform/blob/master/doc/install/Production-Debian.md)
+
+Service Based
+
+- [AWS Elastic Beanstalk Setup](https://github.com/mattermost/platform/blob/master/doc/install/Amazon-Elastic-Beanstalk.md)
+
+
----
### One instruction per line
diff --git a/doc/process/overview.md b/doc/process/overview.md
new file mode 100644
index 000000000..8109163d9
--- /dev/null
+++ b/doc/process/overview.md
@@ -0,0 +1,143 @@
+# Development Process Overview
+
+This document describes the process through which feedback and design discussions flow into community systems, then into tickets, then into merge requests, then into monthly releases based on the purpose of the product.
+
+## Purpose
+
+The core offer for users of Mattermost is:
+
+- **All your team communication in one place, searchable and accessible anywhere.**
+
+The design is successful if 100% of team members use Mattermost for internal communications, and are largely off of email and propreitary SaaS products that lock-in user data as part of their business model.
+
+See [Mattermost scope statement](http://www.mattermost.org/vision/#mattermost-teams-v1) for more details.
+
+## Community Systems
+
+The process for managing bugs, feature ideas, troubleshooting, and general discussions are different, so different systems are used to best support each process. Each system ties into Mattermost through notifications to internal channels, so the core team and key contributors can keep up-to-date with community feedback across all systems throughout the day.
+
+Systems include:
+
+### Feature Idea Forum
+
+A forum for filing, upvoting and discussing feature ideas. Reviewed monthly by the core team as part of the planning process for new releases.
+
+See [Contributing Feature Ideas](http://www.mattermost.org/feature-requests/) for more details on how to use this system.
+
+_Note: If you want to promote an idea filed in the feature idea forum, or if you are out of votes and want to find like-minded colleagues to vote for you, consider posting to the [Feature Idea Discussion ](https://forum.mattermost.org/t/how-to-use-feature-idea-discussion/63/1) category in the General Forum._
+
+
+### Troubleshooting Forum
+
+A system for peer-to-peer support of installation and configuration questions.
+
+See [Troubleshooting Forum](https://forum.mattermost.org/t/about-the-trouble-shooting-category/150/1).
+
+
+### GitHub Issues
+
+A system used by Mattermost for reporting bugs with clear statements on repro steps and expected behavior.
+
+See [Filing Issues](http://www.mattermost.org/filing-issues/) for details on how to use this system.
+
+For feature ideas, troubleshooting, or general questions, we ask your help to use the appropriate [Community System](https://github.com/mattermost/platform/blob/master/doc/process/overview.md#community-systems).
+
+### GitHub Pull Requests
+
+A system for submitting merge requests for changes to Mattermost. See [Merge Requests](https://github.com/mattermost/platform/blob/master/doc/process/overview.md#merge-requests) section below.
+
+### General Forum
+
+A general, peer-to-peer discussion forum with topics organized by category for general questions, trouble shooting, design feedback requests, and FAQs. Monitored and moderated by core team, which is also active on the forum.
+
+Read more about the [General Forum](https://forum.mattermost.org/t/welcome-to-mattermost-community-discussion/8).
+
+### Primary Research
+
+Core team members and key contributors may discuss Mattermost directly with users in a range of systems outside those listed here--in-person meetings, video-conference, usability testing, Twitter, email, etc. Those notes are shared in various Mattermost channels to inform designs.
+
+## Tickets
+
+Mattermost priorities are managed in Jira tickets, which are created by the core team via feedback from community systems as well as through the planning processes.
+
+### Triage
+
+On non-holiday weekdays new tickets are reviewed in a process called "triage", and assigned a Fix Version of "backlog", indicating the ticket has enough specificity that it can be assigned to a developer to be completed.
+
+By default, all tickets are created as internal-only, and the triage process reviews them for sufficient specifity and abscense of sensitive information before switching their visibility to public as part of the triage process.
+
+View [current issues scheduled for the next triage meeting](https://mattermost.atlassian.net/browse/PLT-1203?filter=10105).
+
+#### Re-triage
+
+If someone feels an existing ticket should be reexamined, they can add "triage" to the Fix Version and it will be routed to the triage team for review at the next meeting.
+
+### Release Planning
+
+Release planning sets the "Fix Version" of tickets to one of the upcoming monthly releases. The Fix Version is an estimate of when a feature might ship, which may change as the planning process continues, until the ticket is scheduled for a Sprint.
+
+### Sprint Planning
+
+Tickets to be completed in the upcoming two weeks are organized on Tuesdays, with input from developers, and finalized on Fridays.
+
+## Merge Requests
+
+### Core Team Weekly Rhythm
+
+Core team work on tickets in the active sprint on a weekly basis, which flow into GitHub Pull Requests.
+
+Each Pull Request needs a minimum of two reviews by other core team developers before it is merged, with possible feedback shared as reviews happen.
+
+Key contributors might also pick up tickets, or through conversations with the core team contribute pull requests as needed.
+
+### Community Contributions
+
+Community members following the Contribution Guidelines might also submit pull requests.
+
+#### Bug Fixes
+
+If you see an obvious bug and want to submit a fix, pull requests following the [contribution guidelines](https://github.com/mattermost/platform/blob/master/CONTRIBUTING.md) are gladly accepted.
+
+Examples:
+- [Fix: Unable to change password #1390](https://github.com/mattermost/platform/pull/1390)
+- [Fix isBrowserEdge typo #1260](https://github.com/mattermost/platform/pull/1260)
+
+#### Tickets Accepting Pull Requests
+
+If you'd like to improve the product beyond bug fixes, you can select from a list of tickets accepting pull requests prepared by the core team.
+
+Tickets labelled "accepting pull requests" are intended to be unambiguous projects that could be reasonably completed by contributors outside the core team and are welcome contributions.
+
+Tickets may have a "mana" value assigned, which is an estimate of the relative complexity of each ticket (2 is "small", "4" is medium, "8" is large).
+
+Even if the ticket is assigned to someone else, so long as the ticket has Status set to Open and the ticket is not in the [Active Sprint](https://mattermost.atlassian.net/browse/PLT-839?jql=status%20%3D%20Open%20AND%20sprint%20in%20openSprints%20()) contributors following the contribution guidelines are welcome to submit pull requests.
+
+For a list of tickets that meet this criteria, please the list of [Tickets Accepting Pull Requests](https://mattermost.atlassian.net/browse/PLT-1263?filter=10101).
+
+#### Documentation Improvements
+
+Improvements to documentation on master is highly welcome.
+
+Please see [documentation contribution guidelines](https://forum.mattermost.org/t/help-improve-mattermost-documentation/194) for more details.
+
+Examples:
+- [Production installation instructions for Debian Jessie with Systemd #1134](https://github.com/mattermost/platform/pull/1134)
+- [Fix deadlink to AWS file in doc #622]( https://github.com/mattermost/platform/pull/622)
+
+#### Minor Improvements
+
+Minor improvements without an Accepting Pull Request ticket may be accepted if:
+
+1. The contribution aligns with product scope
+2. The change is high quality, and does not impose a significant burden for others to test, document and maintain your change.
+3. The change aligns with the [fast, obvious, forgiving](http://www.mattermost.org/design-principles/) design principle.
+
+Examples:
+- [Do not clear LastActivityAt for GetProfiles #1396](https://github.com/mattermost/platform/pull/1396/files)
+- [Update to proxy_pass #1331](https://github.com/mattermost/platform/pull/1331)
+
+## Release
+
+Mattermost ships stable releases on the 16th of the month. Releases begin with a planning process reviewing internal designs and community feedback in the context of the product purpose. Feature development is done in weekly sprints, and releases end with feature complete, stablization, code complete and release candidate milestones prior to final release.
+
+See [release process documentation](https://github.com/mattermost/platform/blob/master/doc/process/release-process.md) for more details.
diff --git a/doc/usage/Markdown.md b/doc/usage/Markdown.md
index dd90ede19..65e6f2121 100644
--- a/doc/usage/Markdown.md
+++ b/doc/usage/Markdown.md
@@ -1,180 +1,3 @@
# Markdown Help
-Markdown makes it easy to format messages. Type a message as you normally would, and use these rules to render it with special formatting.
-
-## Text Style:
-
-You can use either `_` or `*` around a word to make it italic. Use two to make it bold.
-
-* `_italics_` renders as _italics_
-* `**bold**` renders as **bold**
-* `**_bold-italic_**` renders as **_bold-italics_**
-* `~~strikethrough~~` renders as ~~strikethrough~~
-
-## Code Block:
-
-Create a code block by indenting each line by four spaces, or by placing ``` on the line above and below your code.
-
-Example:
-
- ```
- code block
- ```
-
-Renders as:
-```
-code block
-```
-
-### Syntax Highlighting
-
-To add syntax highlighting, type the language to be highlighted after the ``` at the beginning of the code block.
-
-Supported languages are:
-`diff, apache, makefile, http, json, markdown, javascript, css, nginx, objectivec, python, xml, perl, bash, php, coffee (CoffeeScript), cs (C#), cpp (C++), sql, go, ruby, java, ini, latex`
-
-Example:
-
- ``` go
- package main
- import "fmt"
- func main() {
- fmt.Println("Hello, 世界")
- }
- ```
-
-Renders as:
-``` go
-package main
-import "fmt"
-func main() {
- fmt.Println("Hello, 世界")
-}
-```
-
-## In-line Code:
-
-Create in-line monospaced font by surrounding it with backticks.
-```
-`monospace`
-```
-Renders as: `monospace`.
-
-## Links:
-
-Create labeled links by putting the desired text in square brackets and the associated link in normal brackets.
-
-`[Check out Mattermost!](www.mattermost.com)`
-
-Renders as: [Check out Mattermost!](www.mattermost.com)
-
-## In-line Images
-
-Create in-line images using an `!` followed by the alt text in square brackets and the link in normal brackets. Add hover text by placing it in quotes after the link.
-```
-![alt text](link "hover text")
-
-and
-
-[![Build Status](https://travis-ci.org/mattermost/platform.svg?branch=master)](https://travis-ci.org/mattermost/platform) [![Github](https://assets-cdn.github.com/favicon.ico)](https://github.com/mattermost/platform)
-```
-Renders as:
-
-![alt text](link "hover text")
-
-and
-
-[![Build Status](https://travis-ci.org/mattermost/platform.svg?branch=master)](https://travis-ci.org/mattermost/platform) [![Github](https://assets-cdn.github.com/favicon.ico)](https://github.com/mattermost/platform)
-
-## Emojis
-
-Check out a full list of emojis [here](http://www.emoji-cheat-sheet.com/).
-
-```
-:smile: :+1: :sheep:
-```
-Renders as:
-:smile: :+1: :sheep:
-
-## Lines:
-
-Create a line by using three `*`, `_`, or `-`.
-
-`***` renders as:
-***
-
-## Block quotes:
-
-Create block quotes using `>`.
-
-`> block quotes` renders as:
-> block quotes
-
-## Lists:
-
-Create a list by using `*` or `-` as bullets. Indent a bullet point by adding two spaces in front of it.
-```
-* list item one
-* list item two
- * item two sub-point
-```
-Renders as:
-* list item one
-* list item two
- * item two sub-point
-
-Make it an ordered list by using numbers instead:
-```
-1. Item one
-2. Item two
-```
-Renders as:
-1. Item one
-2. Item two
-
-## Tables:
-
-Create a table by placing a dashed line under the header row and separating the columns with a pipe `|`. (The columns don’t need to line up exactly for it to work). Choose how to align table columns by including colons `:` within the header row.
-```
-| Left-Aligned  | Center Aligned  | Right Aligned |
-| :------------ |:---------------:| -----:|
-| Left column 1 | this text       |  $100 |
-| Left column 2 | is              |   $10 |
-| Left column 3 | centered        |    $1 |
-```
-
-Renders as:
-
-| Left-Aligned  | Center Aligned  | Right Aligned |
-| :------------ |:---------------:| -----:|
-| Left column 1 | this text       |  $100 |
-| Left column 2 | is              |   $10 |
-| Left column 3 | centered        |    $1 |
-
-## Headings:
-
-Make a heading by typing # and a space before your title. For smaller headings, use more #’s.
-```
-# Large heading
-## Smaller heading
-### Even smaller heading
-```
-Renders as:
-# Large Heading
-## Smaller Heading
-### Even smaller heading
-
-Alternatively, for the large heading you can underline the text using `===`. For the smaller heading you can underline using `---`
-```
-Large Heading
-=============
-
-Smaller Heading
---------------
-```
-Renders as:
-Large Heading
-=============
-
-Smaller Heading
---------------
+Moved to [help/Markdown.md](../help/Markdown.md)
diff --git a/web/react/.eslintrc b/web/react/.eslintrc
index 29ca97faf..935bb638a 100644
--- a/web/react/.eslintrc
+++ b/web/react/.eslintrc
@@ -47,7 +47,7 @@
"no-irregular-whitespace": 2,
"no-unexpected-multiline": 2,
"no-unreachable": 2,
- "no-magic-numbers": [0, { "enforceConst": true, "detectObjects": true } ],
+ "no-magic-numbers": [1, { "enforceConst": true, "detectObjects": true } ],
"valid-typeof": 2,
"block-scoped-var": 2,
@@ -190,7 +190,7 @@
"react/no-did-mount-set-state": 2,
"react/no-did-update-set-state": 2,
"react/no-direct-mutation-state": 2,
- "react/no-multi-comp": 2,
+ "react/no-multi-comp": [2, { "ignoreStateless": true }],
"react/no-set-state": 0,
"react/no-unknown-property": 2,
"react/prefer-es6-class": 2,
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
index c2ecf4fa2..3c6a36ad4 100644
--- a/web/react/components/center_panel.jsx
+++ b/web/react/components/center_panel.jsx
@@ -4,11 +4,13 @@
import TutorialIntroScreens from './tutorial/tutorial_intro_screens.jsx';
import CreatePost from './create_post.jsx';
import PostsViewContainer from './posts_view_container.jsx';
+import PostFocusView from './post_focus_view.jsx';
import ChannelHeader from './channel_header.jsx';
import Navbar from './navbar.jsx';
import FileUploadOverlay from './file_upload_overlay.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx';
import Constants from '../utils/constants.jsx';
@@ -20,26 +22,48 @@ export default class CenterPanel extends React.Component {
super(props);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
+ this.onChannelChange = this.onChannelChange.bind(this);
const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
- this.state = {showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS};
+ this.state = {
+ showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS,
+ showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS
+ };
}
componentDidMount() {
PreferenceStore.addChangeListener(this.onPreferenceChange);
+ ChannelStore.addChangeListener(this.onChannelChange);
}
componentWillUnmount() {
PreferenceStore.removeChangeListener(this.onPreferenceChange);
+ ChannelStore.removeChangeListener(this.onChannelChange);
}
onPreferenceChange() {
const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
this.setState({showTutorialScreens: parseInt(tutorialPref.value, 10) <= TutorialSteps.INTRO_SCREENS});
}
+ onChannelChange() {
+ this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS});
+ }
render() {
let postsContainer;
+ let createPost;
if (this.state.showTutorialScreens) {
postsContainer = <TutorialIntroScreens />;
+ createPost = null;
+ } else if (this.state.showPostFocus) {
+ postsContainer = <PostFocusView />;
+ createPost = null;
} else {
postsContainer = <PostsViewContainer />;
+ createPost = (
+ <div
+ className='post-create__container'
+ id='post-create'
+ >
+ <CreatePost />
+ </div>
+ );
}
return (
@@ -62,12 +86,7 @@ export default class CenterPanel extends React.Component {
<ChannelHeader />
</div>
{postsContainer}
- <div
- className='post-create__container'
- id='post-create'
- >
- <CreatePost />
- </div>
+ {createPost}
</div>
</div>
</div>
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 8c721348f..6e12c7c14 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -4,6 +4,7 @@
import NavbarSearchBox from './search_bar.jsx';
import MessageWrapper from './message_wrapper.jsx';
import PopoverListMembers from './popover_list_members.jsx';
+import EditChannelHeaderModal from './edit_channel_header_modal.jsx';
import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx';
import ChannelInfoModal from './channel_info_modal.jsx';
import ChannelInviteModal from './channel_invite_modal.jsx';
@@ -167,17 +168,13 @@ export default class ChannelHeader extends React.Component {
key='edit_header_direct'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
+ dialogType={EditChannelHeaderModal}
+ dialogProps={{channel}}
>
{'Set Channel Header...'}
- </a>
+ </ToggleModalButton>
</li>
);
} else {
@@ -235,17 +232,13 @@ export default class ChannelHeader extends React.Component {
key='set_channel_header'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
+ dialogType={EditChannelHeaderModal}
+ dialogProps={{channel}}
>
- {'Set '}{channelTerm}{' Header...'}
- </a>
+ {`Set ${channelTerm} Header...`}
+ </ToggleModalButton>
</li>
);
dropdownContents.push(
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 6d3203ae5..0518ccb86 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -22,6 +22,17 @@ export default class ChannelInviteModal extends React.Component {
this.state = this.getStateFromStores();
}
+ shouldComponentUpdate(nextProps, nextState) {
+ if (!Utils.areObjectsEqual(this.props, nextProps)) {
+ return true;
+ }
+
+ if (!Utils.areObjectsEqual(this.state, nextState)) {
+ return true;
+ }
+
+ return false;
+ }
getStateFromStores() {
function getId(user) {
return user.id;
@@ -105,7 +116,7 @@ export default class ChannelInviteModal extends React.Component {
}
this.setState({inviteError: null, memberIds, nonmembers});
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannelExtraInfo();
},
(err) => {
this.setState({inviteError: err.message});
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index e29c659c7..c8f1196a8 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -27,8 +27,8 @@ export default class ChannelLoader extends React.Component {
componentDidMount() {
/* Initial aysnc loads */
AsyncClient.getPosts(ChannelStore.getCurrentId());
- AsyncClient.getChannels(true, true);
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannels();
+ AsyncClient.getChannelExtraInfo();
AsyncClient.findTeams();
AsyncClient.getMyTeam();
setTimeout(() => AsyncClient.getStatuses(), 3000); // temporary until statuses are reworked a bit
diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx
index 08ad95091..f07fc166a 100644
--- a/web/react/components/channel_members_modal.jsx
+++ b/web/react/components/channel_members_modal.jsx
@@ -25,6 +25,17 @@ export default class ChannelMembersModal extends React.Component {
state.showInviteModal = false;
this.state = state;
}
+ shouldComponentUpdate(nextProps, nextState) {
+ if (!Utils.areObjectsEqual(this.props, nextProps)) {
+ return true;
+ }
+
+ if (!Utils.areObjectsEqual(this.state, nextState)) {
+ return true;
+ }
+
+ return false;
+ }
getStateFromStores() {
const users = UserStore.getActiveOnlyProfiles();
const memberList = ChannelStore.getCurrentExtraInfo().members;
@@ -74,6 +85,7 @@ export default class ChannelMembersModal extends React.Component {
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
+ this.onChange();
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
@@ -130,7 +142,7 @@ export default class ChannelMembersModal extends React.Component {
}
this.setState({memberList, nonmemberList});
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannelExtraInfo();
},
(err) => {
this.setState({inviteError: err.message});
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 0a2979e21..f7f63fb92 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -8,6 +8,7 @@ import FilePreview from './file_preview.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import * as Utils from '../utils/utils.jsx';
@@ -19,6 +20,7 @@ import PreferenceStore from '../stores/preference_store.jsx';
import SocketStore from '../stores/socket_store.jsx';
import Constants from '../utils/constants.jsx';
+
const Preferences = Constants.Preferences;
const TutorialSteps = Constants.TutorialSteps;
const ActionTypes = Constants.ActionTypes;
@@ -176,9 +178,7 @@ export default class CreatePost extends React.Component {
const channel = ChannelStore.get(this.state.channelId);
- PostStore.storePendingPost(post);
- PostStore.storeDraft(channel.id, null);
- PostStore.jumpPostsViewToBottom();
+ EventHelpers.emitUserPostedEvent(post);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel,
@@ -190,10 +190,7 @@ export default class CreatePost extends React.Component {
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POST,
- post: data
- });
+ EventHelpers.emitPostRecievedEvent(data);
},
(err) => {
const state = {};
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index fab5b60ea..3c4b17905 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -159,13 +159,4 @@ export default class DeletePostModal extends React.Component {
</Modal>
);
}
-
- static show(post, commentCount) {
- AppDispatcher.handleViewAction({
- type: ActionTypes.TOGGLE_DELETE_POST_MODAL,
- value: true,
- post,
- commentCount: commentCount || 0
- });
- }
}
diff --git a/web/react/components/edit_channel_header_modal.jsx b/web/react/components/edit_channel_header_modal.jsx
new file mode 100644
index 000000000..5529a419d
--- /dev/null
+++ b/web/react/components/edit_channel_header_modal.jsx
@@ -0,0 +1,126 @@
+// 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 * as Utils from '../utils/utils.jsx';
+
+const Modal = ReactBootstrap.Modal;
+
+export default class EditChannelHeaderModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleEdit = this.handleEdit.bind(this);
+
+ this.onShow = this.onShow.bind(this);
+ this.onHide = this.onHide.bind(this);
+
+ this.state = {
+ serverError: ''
+ };
+ }
+
+ componentDidMount() {
+ if (this.props.show) {
+ this.onShow();
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.show && !prevProps.show) {
+ this.onShow();
+ }
+ }
+
+ handleEdit() {
+ var data = {};
+ data.channel_id = this.props.channel.id;
+
+ if (data.channel_id.length !== 26) {
+ return;
+ }
+
+ data.channel_header = ReactDOM.findDOMNode(this.refs.textarea).value;
+
+ Client.updateChannelHeader(data,
+ () => {
+ this.setState({serverError: ''});
+ AsyncClient.getChannel(this.props.channel.id);
+ this.onHide();
+ },
+ (err) => {
+ if (err.message === 'Invalid channel_header parameter') {
+ this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
+ } else {
+ this.setState({serverError: err.message});
+ }
+ }
+ );
+ }
+
+ onShow() {
+ const textarea = ReactDOM.findDOMNode(this.refs.textarea);
+ Utils.placeCaretAtEnd(textarea);
+ }
+
+ onHide() {
+ this.setState({
+ serverError: ''
+ });
+
+ this.props.onHide();
+ }
+
+ render() {
+ var serverError = null;
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><br/><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.onHide}
+ >
+ <Modal.Header closeButton={true}>
+ {'Edit Header for ' + this.props.channel.display_name}
+ </Modal.Header>
+ <Modal.Body>
+ <p>{'Edit the text appearing next to the channel name in the channel header.'}</p>
+ <textarea
+ ref='textarea'
+ className='form-control no-resize'
+ rows='6'
+ id='edit_header'
+ maxLength='1024'
+ defaultValue={this.props.channel.header}
+ />
+ {serverError}
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.props.onHide}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ type='button'
+ className='btn btn-primary'
+ onClick={this.handleEdit}
+ >
+ {'Save'}
+ </button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+EditChannelHeaderModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
+};
diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx
deleted file mode 100644
index 80dab4a57..000000000
--- a/web/react/components/edit_channel_modal.jsx
+++ /dev/null
@@ -1,150 +0,0 @@
-// 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';
-
-export default class EditChannelModal extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleEdit = this.handleEdit.bind(this);
- this.handleUserInput = this.handleUserInput.bind(this);
- this.handleClose = this.handleClose.bind(this);
- this.onShow = this.onShow.bind(this);
- this.handleShown = this.handleShown.bind(this);
-
- this.state = {
- header: '',
- title: '',
- channelId: '',
- serverError: ''
- };
- }
- handleEdit() {
- var data = {};
- data.channel_id = this.state.channelId;
-
- if (data.channel_id.length !== 26) {
- return;
- }
-
- data.channel_header = this.state.header.trim();
-
- Client.updateChannelHeader(data,
- () => {
- this.setState({serverError: ''});
- AsyncClient.getChannel(this.state.channelId);
- $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
- },
- (err) => {
- if (err.message === 'Invalid channel_header parameter') {
- this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
- } else {
- this.setState({serverError: err.message});
- }
- }
- );
- }
- handleUserInput(e) {
- this.setState({header: e.target.value});
- }
- handleClose() {
- this.setState({header: '', serverError: ''});
- }
- onShow(e) {
- const button = e.relatedTarget;
- this.setState({header: $(button).attr('data-header'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
- }
- handleShown() {
- $('#edit_channel #edit_header').focus();
- }
- componentDidMount() {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
- $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.handleShown);
- }
- componentWillUnmount() {
- $(ReactDOM.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose);
- }
- render() {
- var serverError = null;
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><br/><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var editTitle = (
- <h4
- className='modal-title'
- ref='title'
- >
- {'Edit Header'}
- </h4>
- );
- if (this.state.title) {
- editTitle = (
- <h4
- className='modal-title'
- ref='title'
- >
- {'Edit Header for '}<span className='name'>{this.state.title}</span>
- </h4>
- );
- }
-
- return (
- <div
- className='modal fade'
- ref='modal'
- id='edit_channel'
- role='dialog'
- tabIndex='-1'
- aria-hidden='true'
- >
- <div className='modal-dialog'>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>{'×'}</span>
- </button>
- {editTitle}
- </div>
- <div className='modal-body'>
- <p>{'Edit the text appearing next to the channel name in the channel header.'}</p>
- <textarea
- className='form-control no-resize'
- rows='6'
- id='edit_header'
- maxLength='1024'
- value={this.state.header}
- onChange={this.handleUserInput}
- />
- {serverError}
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- {'Cancel'}
- </button>
- <button
- type='button'
- className='btn btn-primary'
- onClick={this.handleEdit}
- >
- {'Save'}
- </button>
- </div>
- </div>
- </div>
- </div>
- );
- }
-}
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index ddbdee8a4..eb58fe721 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -3,7 +3,7 @@
import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
-import DeletePostModal from './delete_post_modal.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import Textbox from './textbox.jsx';
import BrowserStore from '../stores/browser_store.jsx';
import PostStore from '../stores/post_store.jsx';
@@ -35,7 +35,7 @@ export default class EditPostModal extends React.Component {
delete tempState.editText;
BrowserStore.setItem('edit_state_transfer', tempState);
$('#edit_post').modal('hide');
- DeletePostModal.show(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
+ EventHelpers.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
return;
}
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 2bd2c42d6..df5d6b8e1 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -1,32 +1,28 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import UserStore from '../stores/user_store.jsx';
+const Modal = ReactBootstrap.Modal;
export default class GetLinkModal extends React.Component {
constructor(props) {
super(props);
- this.handleClick = this.handleClick.bind(this);
- this.onShow = this.onShow.bind(this);
this.onHide = this.onHide.bind(this);
- this.state = {copiedLink: false};
- }
- onShow(e) {
- var button = e.relatedTarget;
- this.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value')});
+ this.copyLink = this.copyLink.bind(this);
+
+ this.state = {
+ copiedLink: false
+ };
}
+
onHide() {
this.setState({copiedLink: false});
+
+ this.props.onHide();
}
- componentDidMount() {
- if (this.refs.modal) {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', this.onHide);
- }
- }
- handleClick() {
+
+ copyLink() {
var copyTextarea = $(ReactDOM.findDOMNode(this.refs.textarea));
copyTextarea.select();
@@ -41,8 +37,18 @@ export default class GetLinkModal extends React.Component {
this.setState({copiedLink: false});
}
}
+
render() {
- var currentUser = UserStore.getCurrentUser();
+ let helpText = null;
+ if (this.props.helpText) {
+ helpText = (
+ <p>
+ {this.props.helpText}
+ <br />
+ <br />
+ </p>
+ );
+ }
let copyLink = null;
if (document.queryCommandSupported('copy')) {
@@ -51,75 +57,59 @@ export default class GetLinkModal extends React.Component {
data-copy-btn='true'
type='button'
className='btn btn-primary pull-left'
- onClick={this.handleClick}
- data-clipboard-text={this.state.value}
+ onClick={this.copyLink}
>
- Copy Link
+ {'Copy Link'}
</button>
);
}
var copyLinkConfirm = null;
if (this.state.copiedLink) {
- copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className='fa fa-check'></i> Link copied to clipboard.</p>;
+ copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className='fa fa-check'></i>{' Link copied to clipboard.'}</p>;
}
- if (currentUser != null) {
- return (
- <div
- className='modal fade'
- ref='modal'
- id='get_link'
- tabIndex='-1'
- role='dialog'
- aria-hidden='true'
- >
- <div className='modal-dialog'>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- id='myModalLabel'
- >
- {this.state.title} Link
- </h4>
- </div>
- <div className='modal-body'>
- <p>
- Send teammates the link below for them to sign-up to this team site.
- <br /><br />
- </p>
- <textarea
- className='form-control no-resize min-height'
- readOnly='true'
- ref='textarea'
- value={this.state.value}
- />
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- Close
- </button>
- {copyLink}
- {copyLinkConfirm}
- </div>
- </div>
- </div>
- </div>
- );
- }
- return <div/>;
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.onHide}
+ >
+ <Modal.Header closeButton={true}>
+ {this.props.title}
+ </Modal.Header>
+ <Modal.Body>
+ {helpText}
+ <textarea
+ className='form-control no-resize min-height'
+ readOnly='true'
+ ref='textarea'
+ value={this.props.link}
+ />
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.onHide}
+ >
+ {'Close'}
+ </button>
+ {copyLink}
+ {copyLinkConfirm}
+ </Modal.Footer>
+ </Modal>
+ );
}
}
+
+GetLinkModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired,
+ title: React.PropTypes.string.isRequired,
+ helpText: React.PropTypes.string,
+ link: React.PropTypes.string.isRequired
+};
+
+GetLinkModal.defaultProps = {
+ helpText: null
+};
diff --git a/web/react/components/get_team_invite_link_modal.jsx b/web/react/components/get_team_invite_link_modal.jsx
new file mode 100644
index 000000000..a926c4451
--- /dev/null
+++ b/web/react/components/get_team_invite_link_modal.jsx
@@ -0,0 +1,45 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Constants from '../utils/constants.jsx';
+import GetLinkModal from './get_link_modal.jsx';
+import ModalStore from '../stores/modal_store.jsx';
+import TeamStore from '../stores/team_store.jsx';
+
+export default class GetTeamInviteLinkModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleToggle = this.handleToggle.bind(this);
+
+ this.state = {
+ show: false
+ };
+ }
+
+ componentDidMount() {
+ ModalStore.addModalListener(Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, this.handleToggle);
+ }
+
+ componentWillUnmount() {
+ ModalStore.removeModalListener(Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, this.handleToggle);
+ }
+
+ handleToggle(value) {
+ this.setState({
+ show: value
+ });
+ }
+
+ render() {
+ return (
+ <GetLinkModal
+ show={this.state.show}
+ onHide={() => this.setState({show: false})}
+ title='Team Invite Link'
+ helpText='Send teammates the link below for them to sign-up to this team site.'
+ link={TeamStore.getCurrentInviteLink()}
+ />
+ );
+ }
+}
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 7df75252e..76f52faa9 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -4,8 +4,8 @@
import * as utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as Client from '../utils/client.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import ModalStore from '../stores/modal_store.jsx';
import UserStore from '../stores/user_store.jsx';
import TeamStore from '../stores/team_store.jsx';
@@ -23,6 +23,7 @@ export default class InviteMemberModal extends React.Component {
this.addInviteFields = this.addInviteFields.bind(this);
this.clearFields = this.clearFields.bind(this);
this.removeInviteFields = this.removeInviteFields.bind(this);
+ this.showGetTeamInviteLinkModal = this.showGetTeamInviteLinkModal.bind(this);
this.state = {
show: false,
@@ -188,6 +189,12 @@ export default class InviteMemberModal extends React.Component {
this.setState({inviteIds: inviteIds, idCount: count});
}
+ showGetTeamInviteLinkModal() {
+ this.handleHide(false);
+
+ EventHelpers.showGetTeamInviteLinkModal();
+ }
+
render() {
var currentUser = UserStore.getCurrentUser();
@@ -333,22 +340,18 @@ export default class InviteMemberModal extends React.Component {
} else {
var teamInviteLink = null;
if (currentUser && TeamStore.getCurrent().type === 'O') {
- var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id;
- var link =
- (
- <a
- href='#'
- data-toggle='modal'
- data-target='#get_link'
- data-title='Team Invite'
- data-value={linkUrl}
- onClick={() => this.handleHide(this, false)}
- >Team Invite Link</a>
+ var link = (
+ <a
+ href='#'
+ onClick={this.showGetTeamInviteLinkModal}
+ >
+ {'Team Invite Link'}
+ </a>
);
teamInviteLink = (
<p>
- You can also invite people using the {link}.
+ {'You can also invite people using the '}{link}{'.'}
</p>
);
}
@@ -405,13 +408,6 @@ export default class InviteMemberModal extends React.Component {
return null;
}
-
- static show() {
- AppDispatcher.handleViewAction({
- type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL,
- value: true
- });
- }
}
InviteMemberModal.propTypes = {
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index 6848ee5da..03cc75a08 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import EditChannelHeaderModal from './edit_channel_header_modal.jsx';
import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx';
import MessageWrapper from './message_wrapper.jsx';
import NotifyCounts from './notify_counts.jsx';
@@ -33,11 +34,15 @@ export default class Navbar extends React.Component {
this.onChange = this.onChange.bind(this);
this.handleLeave = this.handleLeave.bind(this);
this.showSearch = this.showSearch.bind(this);
+
+ this.showEditChannelHeaderModal = this.showEditChannelHeaderModal.bind(this);
+
this.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this);
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
+ state.showEditChannelHeaderModal = false;
state.showMembersModal = false;
state.showInviteModal = false;
this.state = state;
@@ -110,6 +115,16 @@ export default class Navbar extends React.Component {
this.setState(this.getStateFromStores());
$('#navbar .navbar-brand .description').popover({placement: 'bottom', trigger: 'click', html: true});
}
+ showEditChannelHeaderModal() {
+ // this can't be done using a ToggleModalButton because we can't use one inside an OverlayTrigger
+ if (this.refs.headerOverlay) {
+ this.refs.headerOverlay.hide();
+ }
+
+ this.setState({
+ showEditChannelHeaderModal: true
+ });
+ }
createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) {
if (channel) {
var viewInfoOption = (
@@ -129,11 +144,7 @@ export default class Navbar extends React.Component {
<a
role='menuitem'
href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
+ onClick={this.showEditChannelHeaderModal}
>
{'Set Channel Header...'}
</a>
@@ -239,7 +250,7 @@ export default class Navbar extends React.Component {
dialogType={ChannelNotificationsModal}
dialogProps={{channel}}
>
- {'Notification Preferences'}
+ {'Notification Preferences'}
</ToggleModalButton>
</li>
);
@@ -249,6 +260,7 @@ export default class Navbar extends React.Component {
<div className='navbar-brand'>
<div className='dropdown'>
<OverlayTrigger
+ ref='headerOverlay'
trigger='click'
placement='bottom'
overlay={popoverContent}
@@ -358,6 +370,9 @@ export default class Navbar extends React.Component {
var isAdmin = false;
var isDirect = false;
+ var editChannelHeaderModal = null;
+ var editChannelPurposeModal = null;
+
if (channel) {
popoverContent = (
<Popover
@@ -400,11 +415,7 @@ export default class Navbar extends React.Component {
<br/>
<a
href='#'
- data-toggle='modal'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- data-target='#edit_channel'
+ onClick={this.showEditChannelHeaderModal}
>
{'Click here'}
</a>
@@ -413,6 +424,22 @@ export default class Navbar extends React.Component {
</Popover>
);
}
+
+ editChannelHeaderModal = (
+ <EditChannelHeaderModal
+ show={this.state.showEditChannelHeaderModal}
+ onHide={() => this.setState({showEditChannelHeaderModal: false})}
+ channel={channel}
+ />
+ );
+
+ editChannelPurposeModal = (
+ <EditChannelPurposeModal
+ show={this.state.showEditChannelPurposeModal}
+ onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
+ channel={channel}
+ />
+ );
}
var collapseButtons = this.createCollapseButtons(currentId);
@@ -443,11 +470,8 @@ export default class Navbar extends React.Component {
</div>
</div>
</nav>
- <EditChannelPurposeModal
- show={this.state.showEditChannelPurposeModal}
- onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
- channel={channel}
- />
+ {editChannelHeaderModal}
+ {editChannelPurposeModal}
<ChannelMembersModal
show={this.state.showMembersModal}
onModalDismissed={() => this.setState({showMembersModal: false})}
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index c0230fe5f..a14434bfc 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -5,9 +5,9 @@ import * as Utils from '../utils/utils.jsx';
import * as client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
import TeamStore from '../stores/team_store.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import AboutBuildModal from './about_build_modal.jsx';
-import InviteMemberModal from './invite_member_modal.jsx';
import UserSettingsModal from './user_settings/user_settings_modal.jsx';
import Constants from '../utils/constants.jsx';
@@ -93,7 +93,7 @@ export default class NavbarDropdown extends React.Component {
<li>
<a
href='#'
- onClick={InviteMemberModal.show}
+ onClick={EventHelpers.showInviteMemberModal}
>
{'Invite New Member'}
</a>
@@ -105,10 +105,7 @@ export default class NavbarDropdown extends React.Component {
<li>
<a
href='#'
- data-toggle='modal'
- data-target='#get_link'
- data-title='Team Invite'
- data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id}
+ onClick={EventHelpers.showGetTeamInviteLinkModal}
>
{'Get Team Invite Link'}
</a>
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 5b61c711c..278261e22 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -105,7 +105,7 @@ export default class Post extends React.Component {
} else {
commentRootId = post.id;
}
- for (let postId in posts) {
+ for (const postId in posts) {
if (posts[postId].root_id === commentRootId) {
commentCount += 1;
}
@@ -114,53 +114,58 @@ export default class Post extends React.Component {
return commentCount;
}
render() {
- var post = this.props.post;
- var parentPost = this.props.parentPost;
- var posts = this.props.posts;
+ const post = this.props.post;
+ const parentPost = this.props.parentPost;
+ const posts = this.props.posts;
if (!post.props) {
post.props = {};
}
- var type = 'Post';
+ let type = 'Post';
if (post.root_id && post.root_id.length > 0) {
type = 'Comment';
}
const commentCount = this.getCommentCount(this.props);
- var rootUser;
+ let rootUser;
if (this.props.sameRoot) {
rootUser = 'same--root';
} else {
rootUser = 'other--root';
}
- var postType = '';
+ let postType = '';
if (type !== 'Post') {
postType = 'post--comment';
} else if (commentCount > 0) {
postType = 'post--root';
}
- var currentUserCss = '';
+ let currentUserCss = '';
if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook) {
currentUserCss = 'current--user';
}
- var userProfile = UserStore.getProfile(post.user_id);
+ const userProfile = UserStore.getProfile(post.user_id);
- var timestamp = UserStore.getCurrentUser().update_at;
+ let timestamp = UserStore.getCurrentUser().update_at;
if (userProfile) {
timestamp = userProfile.update_at;
}
- var sameUserClass = '';
+ let sameUserClass = '';
if (this.props.sameUser) {
sameUserClass = 'same--user';
}
- var profilePic = null;
+ let shouldHighlightClass = '';
+ if (this.props.shouldHighlight) {
+ shouldHighlightClass = 'post--highlight';
+ }
+
+ let profilePic = null;
if (!this.props.hideProfilePic) {
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex();
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
@@ -182,7 +187,7 @@ export default class Post extends React.Component {
<div>
<div
id={'post_' + post.id}
- className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss}
+ className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass}
>
<div className='post__content'>
<div className='post__img'>{profilePic}</div>
@@ -218,5 +223,6 @@ Post.propTypes = {
sameUser: React.PropTypes.bool,
sameRoot: React.PropTypes.bool,
hideProfilePic: React.PropTypes.bool,
- isLastComment: React.PropTypes.bool
+ isLastComment: React.PropTypes.bool,
+ shouldHighlight: React.PropTypes.bool
};
diff --git a/web/react/components/post_focus_view.jsx b/web/react/components/post_focus_view.jsx
new file mode 100644
index 000000000..5c6ad6c28
--- /dev/null
+++ b/web/react/components/post_focus_view.jsx
@@ -0,0 +1,110 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import PostsView from './posts_view.jsx';
+
+import PostStore from '../stores/post_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+
+export default class PostFocusView extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChannelChange = this.onChannelChange.bind(this);
+ this.onPostsChange = this.onPostsChange.bind(this);
+ this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
+ this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
+ this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
+
+ const focusedPostId = PostStore.getFocusedPostId();
+
+ this.state = {
+ scrollType: PostsView.SCROLL_TYPE_POST,
+ scrollPostId: focusedPostId,
+ postList: PostStore.getVisiblePosts(focusedPostId),
+ atTop: PostStore.getVisibilityAtTop(focusedPostId),
+ atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
+ };
+ }
+
+ componentDidMount() {
+ ChannelStore.addChangeListener(this.onChannelChange);
+ PostStore.addChangeListener(this.onPostsChange);
+ }
+
+ componentWillUnmount() {
+ ChannelStore.removeChangeListener(this.onChannelChange);
+ PostStore.removeChangeListener(this.onPostsChange);
+ }
+
+ onChannelChange() {
+ this.setState({
+ scrollType: PostsView.SCROLL_TYPE_POST
+ });
+ }
+
+ onPostsChange() {
+ const focusedPostId = PostStore.getFocusedPostId();
+ if (focusedPostId == null) {
+ return;
+ }
+
+ this.setState({
+ scrollPostId: focusedPostId,
+ postList: PostStore.getVisiblePosts(focusedPostId),
+ atTop: PostStore.getVisibilityAtTop(focusedPostId),
+ atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
+ });
+ }
+
+ handlePostsViewScroll() {
+ this.setState({scrollType: PostsView.SCROLL_TYPE_FREE});
+ }
+
+ loadMorePostsTop() {
+ EventHelpers.emitLoadMorePostsFocusedTopEvent();
+ }
+
+ loadMorePostsBottom() {
+ EventHelpers.emitLoadMorePostsFocusedBottomEvent();
+ }
+
+ getIntroMessage() {
+ return (
+ <div className='channel-intro'>
+ <h4 className='channel-intro__title'>{'Beginning of Channel'}</h4>
+ </div>
+ );
+ }
+
+ render() {
+ const postsToHighlight = {};
+ postsToHighlight[this.state.scrollPostId] = true;
+
+ return (
+ <div id='post-list'>
+ <PostsView
+ key={'postfocusview'}
+ isActive={true}
+ postList={this.state.postList}
+ scrollType={this.state.scrollType}
+ scrollPostId={this.state.scrollPostId}
+ postViewScrolled={this.handlePostsViewScroll}
+ loadMorePostsTopClicked={this.loadMorePostsTop}
+ loadMorePostsBottomClicked={this.loadMorePostsBottom}
+ showMoreMessagesTop={!this.state.atTop}
+ showMoreMessagesBottom={!this.state.atBottom}
+ introText={this.getIntroMessage()}
+ messageSeparatorTime={0}
+ postsToHighlight={postsToHighlight}
+ />
+ </div>
+ );
+ }
+}
+PostFocusView.defaultProps = {
+};
+
+PostFocusView.propTypes = {
+};
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 5306edd3d..cedb2b59b 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -1,22 +1,30 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import DeletePostModal from './delete_post_modal.jsx';
import UserStore from '../stores/user_store.jsx';
-import * as utils from '../utils/utils.jsx';
+import TeamStore from '../stores/team_store.jsx';
+import * as Utils from '../utils/utils.jsx';
import TimeSince from './time_since.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import Constants from '../utils/constants.jsx';
+const OverlayTrigger = ReactBootstrap.OverlayTrigger;
+const Popover = ReactBootstrap.Popover;
+
export default class PostInfo extends React.Component {
constructor(props) {
super(props);
- this.state = {};
+ this.state = {
+ copiedLink: false
+ };
+
+ this.handlePermalinkCopy = this.handlePermalinkCopy.bind(this);
}
createDropdown() {
var post = this.props.post;
var isOwner = UserStore.getCurrentId() === post.user_id;
- var isAdmin = utils.isAdmin(UserStore.getCurrentUser().roles);
+ var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles);
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.state === Constants.POST_DELETED) {
return '';
@@ -66,7 +74,7 @@ export default class PostInfo extends React.Component {
<a
href='#'
role='menuitem'
- onClick={() => DeletePostModal.show(post, dataComments)}
+ onClick={() => EventHelpers.showDeletePostModal(post, dataComments)}
>
{'Delete'}
</a>
@@ -81,7 +89,7 @@ export default class PostInfo extends React.Component {
role='presentation'
>
<a
- className='reply-link visible-xs theme'
+ className='link__reply theme'
href='#'
onClick={this.props.handleCommentClick}
>
@@ -113,6 +121,21 @@ export default class PostInfo extends React.Component {
</div>
);
}
+ handlePermalinkCopy() {
+ const textBox = $(ReactDOM.findDOMNode(this.refs.permalinkbox));
+ textBox.select();
+
+ try {
+ const successful = document.execCommand('copy');
+ if (successful) {
+ this.setState({copiedLink: true});
+ } else {
+ this.setState({copiedLink: false});
+ }
+ } catch (err) {
+ this.setState({copiedLink: false});
+ }
+ }
render() {
var post = this.props.post;
var comments = '';
@@ -143,6 +166,37 @@ export default class PostInfo extends React.Component {
var dropdown = this.createDropdown();
+ const permalink = TeamStore.getCurrentTeamUrl() + '/pl/' + post.id;
+ const copyButtonText = this.state.copiedLink ? (<div>{'Copy '}<i className='fa fa-check'/></div>) : 'Copy';
+ const permalinkOverlay = (
+ <Popover
+ id='permalink-overlay'
+ className='permalink-popover'
+ placement='left'
+ title=''
+ >
+ <div className='form-inline'>
+ <input
+ type='text'
+ readOnly='true'
+ ref='permalinkbox'
+ className='permalink-text form-control no-resize min-height input-large'
+ rows='1'
+ value={permalink}
+ />
+ <button
+ data-copy-btn='true'
+ type='button'
+ className='btn btn-primary'
+ onClick={this.handlePermalinkCopy}
+ data-clipboard-text={permalink}
+ >
+ {copyButtonText}
+ </button>
+ </div>
+ </Popover>
+ );
+
return (
<ul className='post__header post__header--info'>
<li className='col'>
@@ -152,6 +206,15 @@ export default class PostInfo extends React.Component {
</li>
<li className='col col__reply'>
{comments}
+ <OverlayTrigger
+ trigger='click'
+ placement='left'
+ rootClose={true}
+ overlay={permalinkOverlay}
+ >
+ <i className={'permalink-icon fa fa-link ' + showCommentClass}/>
+ </OverlayTrigger>
+
<div className='dropdown'>
{dropdown}
</div>
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index 5b36ecbc5..5e374b877 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import UserStore from '../stores/user_store.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as Utils from '../utils/utils.jsx';
import Post from './post.jsx';
import Constants from '../utils/constants.jsx';
@@ -13,6 +14,7 @@ export default class PostsView extends React.Component {
this.handleScroll = this.handleScroll.bind(this);
this.isAtBottom = this.isAtBottom.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
+ this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
this.createPosts = this.createPosts.bind(this);
this.updateScrolling = this.updateScrolling.bind(this);
this.handleResize = this.handleResize.bind(this);
@@ -27,12 +29,15 @@ export default class PostsView extends React.Component {
static get SCROLL_TYPE_BOTTOM() {
return 2;
}
- static get SIDEBAR_OPEN() {
+ static get SCROLL_TYPE_SIDEBAR_OPEN() {
return 3;
}
static get SCROLL_TYPE_NEW_MESSAGE() {
return 4;
}
+ static get SCROLL_TYPE_POST() {
+ return 5;
+ }
isAtBottom() {
return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight);
}
@@ -47,15 +52,22 @@ export default class PostsView extends React.Component {
}
}
this.wasAtBottom = this.isAtBottom();
+ if (!this.jumpToPostNode && childNodes.length > 0) {
+ this.jumpToPostNode = childNodes[childNodes.length - 1];
+ }
// --- --------
this.props.postViewScrolled(this.isAtBottom());
this.prevScrollHeight = this.refs.postlist.scrollHeight;
+ this.prevOffsetTop = this.jumpToPostNode.offsetTop;
}
loadMorePostsTop() {
this.props.loadMorePostsTopClicked();
}
+ loadMorePostsBottom() {
+ this.props.loadMorePostsBottomClicked();
+ }
createPosts(posts, order) {
const postCtls = [];
let previousPostDay = new Date(0);
@@ -63,12 +75,7 @@ export default class PostsView extends React.Component {
let renderedLastViewed = false;
- let numToDisplay = this.props.numPostsToDisplay;
- if (order.length - 1 < numToDisplay) {
- numToDisplay = order.length - 1;
- }
-
- for (let i = numToDisplay; i >= 0; i--) {
+ for (let i = order.length - 1; i >= 0; i--) {
const post = posts[order[i]];
const parentPost = posts[post.parent_id];
const prevPost = posts[order[i + 1]];
@@ -113,6 +120,8 @@ export default class PostsView extends React.Component {
const keyPrefix = post.id ? post.id : i;
+ const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id);
+
const postCtl = (
<Post
key={keyPrefix + 'postKey'}
@@ -124,6 +133,8 @@ export default class PostsView extends React.Component {
posts={posts}
hideProfilePic={hideProfilePic}
isLastComment={isLastComment}
+ shouldHighlight={shouldHighlight}
+ onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func
/>
);
@@ -185,9 +196,12 @@ export default class PostsView extends React.Component {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
}
});
- } else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPost) {
+ } else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPostId) {
window.requestAnimationFrame(() => {
- const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPost]);
+ const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
+ if (postNode == null) {
+ return;
+ }
postNode.scrollIntoView();
if (this.refs.postlist.scrollTop === postNode.offsetTop) {
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3);
@@ -195,7 +209,7 @@ export default class PostsView extends React.Component {
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - postNode.offsetTop);
}
});
- } else if (this.props.scrollType === PostsView.SIDEBAR_OPEN) {
+ } else if (this.props.scrollType === PostsView.SCROLL_TYPE_SIDEBAR_OPEN) {
// If we are at the bottom then stay there
if (this.wasAtBottom) {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
@@ -211,7 +225,10 @@ export default class PostsView extends React.Component {
}
} else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) {
window.requestAnimationFrame(() => {
- this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight);
+ // Only need to jump if we added posts to the top.
+ if (this.jumpToPostNode && (this.jumpToPostNode.offsetTop !== this.prevOffsetTop)) {
+ this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight);
+ }
});
}
}
@@ -219,14 +236,18 @@ export default class PostsView extends React.Component {
this.updateScrolling();
}
componentDidMount() {
- this.updateScrolling();
+ if (this.props.postList != null) {
+ this.updateScrolling();
+ }
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
componentDidUpdate() {
- this.updateScrolling();
+ if (this.props.postList != null) {
+ this.updateScrolling();
+ }
}
shouldComponentUpdate(nextProps) {
if (this.props.isActive !== nextProps.isActive) {
@@ -235,15 +256,12 @@ export default class PostsView extends React.Component {
if (this.props.postList !== nextProps.postList) {
return true;
}
- if (this.props.scrollPost !== nextProps.scrollPost) {
+ if (this.props.scrollPostId !== nextProps.scrollPostId) {
return true;
}
if (this.props.scrollType !== nextProps.scrollType && nextProps.scrollType !== PostsView.SCROLL_TYPE_FREE) {
return true;
}
- if (this.props.numPostsToDisplay !== nextProps.numPostsToDisplay) {
- return true;
- }
if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) {
return true;
}
@@ -256,7 +274,8 @@ export default class PostsView extends React.Component {
render() {
let posts = [];
let order = [];
- let moreMessages;
+ let moreMessagesTop;
+ let moreMessagesBottom;
let postElements;
let activeClass = 'inactive';
if (this.props.postList != null) {
@@ -264,10 +283,10 @@ export default class PostsView extends React.Component {
order = this.props.postList.order;
// Create intro message or top loadmore link
- if (order.length >= this.props.numPostsToDisplay) {
- moreMessages = (
+ if (this.props.showMoreMessagesTop) {
+ moreMessagesTop = (
<a
- ref='loadmore'
+ ref='loadmoretop'
className='more-messages-text theme'
href='#'
onClick={this.loadMorePostsTop}
@@ -276,7 +295,23 @@ export default class PostsView extends React.Component {
</a>
);
} else {
- moreMessages = this.props.introText;
+ moreMessagesTop = this.props.introText;
+ }
+
+ // Give option to load more posts at bottom if nessisary
+ if (this.props.showMoreMessagesBottom) {
+ moreMessagesBottom = (
+ <a
+ ref='loadmorebottom'
+ className='more-messages-text theme'
+ href='#'
+ onClick={this.loadMorePostsBottom}
+ >
+ {'Load more messages'}
+ </a>
+ );
+ } else {
+ moreMessagesBottom = null;
}
// Create post elements
@@ -299,8 +334,9 @@ export default class PostsView extends React.Component {
ref='postlistcontent'
className='post-list__content'
>
- {moreMessages}
+ {moreMessagesTop}
{postElements}
+ {moreMessagesBottom}
</div>
</div>
</div>
@@ -313,11 +349,14 @@ PostsView.defaultProps = {
PostsView.propTypes = {
isActive: React.PropTypes.bool,
postList: React.PropTypes.object,
- scrollPost: React.PropTypes.string,
+ scrollPostId: React.PropTypes.string,
scrollType: React.PropTypes.number,
postViewScrolled: React.PropTypes.func.isRequired,
loadMorePostsTopClicked: React.PropTypes.func.isRequired,
- numPostsToDisplay: React.PropTypes.number,
+ loadMorePostsBottomClicked: React.PropTypes.func.isRequired,
+ showMoreMessagesTop: React.PropTypes.bool,
+ showMoreMessagesBottom: React.PropTypes.bool,
introText: React.PropTypes.element,
- messageSeparatorTime: React.PropTypes.number
+ messageSeparatorTime: React.PropTypes.number,
+ postsToHighlight: React.PropTypes.object
};
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
index c71ef401e..367d3687e 100644
--- a/web/react/components/posts_view_container.jsx
+++ b/web/react/components/posts_view_container.jsx
@@ -9,12 +9,9 @@ import ChannelStore from '../stores/channel_store.jsx';
import PostStore from '../stores/post_store.jsx';
import * as Utils from '../utils/utils.jsx';
-import * as Client from '../utils/client.jsx';
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import * as AsyncClient from '../utils/async_client.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import Constants from '../utils/constants.jsx';
-const ActionTypes = Constants.ActionTypes;
import {createChannelIntroMessage} from '../utils/channel_intro_mssages.jsx';
@@ -27,27 +24,26 @@ export default class PostsViewContainer extends React.Component {
this.onPostsChange = this.onPostsChange.bind(this);
this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
- this.postsLoaded = this.postsLoaded.bind(this);
- this.postsLoadedFailure = this.postsLoadedFailure.bind(this);
this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this);
const currentChannelId = ChannelStore.getCurrentId();
const state = {
scrollType: PostsView.SCROLL_TYPE_BOTTOM,
- scrollPost: null,
- numPostsToDisplay: Constants.POST_CHUNK_SIZE
+ scrollPost: null
};
if (currentChannelId) {
Object.assign(state, {
currentChannelIndex: 0,
channels: [currentChannelId],
- postLists: [this.getChannelPosts(currentChannelId)]
+ postLists: [this.getChannelPosts(currentChannelId)],
+ atTop: [PostStore.getVisibilityAtTop(currentChannelId)]
});
} else {
Object.assign(state, {
currentChannelIndex: null,
channels: [],
- postLists: []
+ postLists: [],
+ atTop: []
});
}
@@ -78,24 +74,21 @@ export default class PostsViewContainer extends React.Component {
});
break;
case Constants.PostsViewJumpTypes.SIDEBAR_OPEN:
- this.setState({scrollType: PostsView.SIDEBAR_OPEN});
+ this.setState({scrollType: PostsView.SCROLL_TYPE_SIDEBAR_OPEN});
break;
}
}
onChannelChange() {
const postLists = this.state.postLists.slice();
+ const atTop = this.state.atTop.slice();
const channels = this.state.channels.slice();
const channelId = ChannelStore.getCurrentId();
// Has the channel really changed?
if (channelId === channels[this.state.currentChannelIndex]) {
- // Dirty hack
- this.forceUpdate();
return;
}
- PostStore.clearUnseenDeletedPosts(channelId);
-
let lastViewed = Number.MAX_VALUE;
const member = ChannelStore.getMember(channelId);
if (member != null) {
@@ -107,115 +100,45 @@ export default class PostsViewContainer extends React.Component {
newIndex = channels.length;
channels.push(channelId);
postLists[newIndex] = this.getChannelPosts(channelId);
+ atTop[newIndex] = PostStore.getVisibilityAtTop(channelId);
}
+
this.setState({
currentChannelIndex: newIndex,
currentLastViewed: lastViewed,
scrollType: PostsView.SCROLL_TYPE_NEW_MESSAGE,
channels,
- postLists});
+ postLists,
+ atTop});
}
onChannelLeave(id) {
const postLists = this.state.postLists.slice();
const channels = this.state.channels.slice();
+ const atTop = this.state.atTop.slice();
const index = channels.indexOf(id);
if (index !== -1) {
postLists.splice(index, 1);
channels.splice(index, 1);
+ atTop.splice(index, 1);
}
- this.setState({channels, postLists});
+ this.setState({channels, postLists, atTop});
}
onPostsChange() {
const channels = this.state.channels;
const postLists = this.state.postLists.slice();
- const newPostsView = this.getChannelPosts(channels[this.state.currentChannelIndex]);
+ const atTop = this.state.atTop.slice();
+ const currentChannelId = channels[this.state.currentChannelIndex];
+ const newPostsView = this.getChannelPosts(currentChannelId);
postLists[this.state.currentChannelIndex] = newPostsView;
- this.setState({postLists});
+ atTop[this.state.currentChannelIndex] = PostStore.getVisibilityAtTop(currentChannelId);
+ this.setState({postLists, atTop});
}
getChannelPosts(id) {
- const postList = PostStore.getPosts(id);
-
- if (postList != null) {
- const deletedPosts = PostStore.getUnseenDeletedPosts(id);
-
- if (deletedPosts && Object.keys(deletedPosts).length > 0) {
- for (const pid in deletedPosts) {
- if (deletedPosts.hasOwnProperty(pid)) {
- postList.posts[pid] = deletedPosts[pid];
- postList.order.unshift(pid);
- }
- }
-
- postList.order.sort((a, b) => {
- if (postList.posts[a].create_at > postList.posts[b].create_at) {
- return -1;
- }
- if (postList.posts[a].create_at < postList.posts[b].create_at) {
- return 1;
- }
- return 0;
- });
- }
-
- const pendingPostList = PostStore.getPendingPosts(id);
-
- if (pendingPostList) {
- postList.order = pendingPostList.order.concat(postList.order);
- for (const ppid in pendingPostList.posts) {
- if (pendingPostList.posts.hasOwnProperty(ppid)) {
- postList.posts[ppid] = pendingPostList.posts[ppid];
- }
- }
- }
- }
-
- return postList;
+ return PostStore.getVisiblePosts(id);
}
loadMorePostsTop() {
- const postLists = this.state.postLists;
- const channels = this.state.channels;
- const currentChannelId = channels[this.state.currentChannelIndex];
- const currentPostList = postLists[this.state.currentChannelIndex];
-
- this.setState({numPostsToDisplay: this.state.numPostsToDisplay + Constants.POST_CHUNK_SIZE});
-
- Client.getPostsPage(
- currentChannelId,
- currentPostList.order.length,
- Constants.POST_CHUNK_SIZE,
- this.postsLoaded,
- this.postsLoadedFailure
- );
- }
- postsLoaded(data) {
- if (!data) {
- return;
- }
-
- if (data.order.length === 0) {
- return;
- }
-
- const postLists = this.state.postLists;
- const currentPostList = postLists[this.state.currentChannelIndex];
- const channels = this.state.channels;
- const currentChannelId = channels[this.state.currentChannelIndex];
-
- var newPostList = {};
- newPostList.posts = Object.assign(currentPostList.posts, data.posts);
- newPostList.order = currentPostList.order.concat(data.order);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POSTS,
- id: currentChannelId,
- post_list: newPostList
- });
-
- Client.getProfiles();
- }
- postsLoadedFailure(err) {
- AsyncClient.dispatchError(err, 'getPosts');
+ EventHelpers.emitLoadMorePostsEvent();
}
handlePostsViewScroll(atBottom) {
if (atBottom) {
@@ -246,15 +169,17 @@ export default class PostsViewContainer extends React.Component {
isActive={isActive}
postList={postLists[i]}
scrollType={this.state.scrollType}
- scrollPost={this.state.scrollPost}
+ scrollPostId={this.state.scrollPost}
postViewScrolled={this.handlePostsViewScroll}
loadMorePostsTopClicked={this.loadMorePostsTop}
- numPostsToDisplay={this.state.numPostsToDisplay}
+ loadMorePostsBottomClicked={() => {}}
+ showMoreMessagesTop={!this.state.atTop[this.state.currentChannelIndex]}
+ showMoreMessagesBottom={false}
introText={channel ? createChannelIntroMessage(channel, () => this.setState({showInviteModal: true})) : null}
messageSeparatorTime={this.state.currentLastViewed}
/>
);
- if ((!postLists[i] || !channel) && isActive) {
+ if (!postLists[i] && isActive) {
postListCtls.push(
<LoadingScreen
position='absolute'
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index 3e555c85a..7aae5177e 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -8,13 +8,13 @@ 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';
-import DeletePostModal from './delete_post_modal.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
var ActionTypes = Constants.ActionTypes;
import * as TextFormatting from '../utils/text_formatting.jsx';
import twemoji from 'twemoji';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
export default class RhsComment extends React.Component {
constructor(props) {
@@ -115,7 +115,7 @@ export default class RhsComment extends React.Component {
<a
href='#'
role='menuitem'
- onClick={() => DeletePostModal.show(post, 0)}
+ onClick={() => EventHelpers.showDeletePostModal(post, 0)}
>
{'Delete'}
</a>
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 96f43bdb5..3d3d9e13f 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -6,11 +6,11 @@ import UserProfile from './user_profile.jsx';
import UserStore from '../stores/user_store.jsx';
import * as TextFormatting from '../utils/text_formatting.jsx';
import * as utils from '../utils/utils.jsx';
-import DeletePostModal from './delete_post_modal.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
import twemoji from 'twemoji';
import Constants from '../utils/constants.jsx';
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
export default class RhsRootPost extends React.Component {
constructor(props) {
@@ -38,7 +38,9 @@ export default class RhsRootPost extends React.Component {
}
render() {
var post = this.props.post;
- var isOwner = UserStore.getCurrentId() === post.user_id;
+ var currentUser = UserStore.getCurrentUser();
+ var isOwner = currentUser.id === post.user_id;
+ var isAdmin = utils.isAdmin(currentUser.roles);
var timestamp = UserStore.getProfile(post.user_id).update_at;
var channel = ChannelStore.get(post.channel_id);
@@ -61,11 +63,54 @@ export default class RhsRootPost extends React.Component {
}
}
- var ownerOptions;
+ var dropdownContents = [];
+
if (isOwner) {
- ownerOptions = (
- <div>
- <a href='#'
+ dropdownContents.push(
+ <li
+ key='rhs-root-edit'
+ role='presentation'
+ >
+ <a
+ href='#'
+ role='menuitem'
+ data-toggle='modal'
+ data-target='#edit_post'
+ data-refocusid='#reply_textbox'
+ data-title={type}
+ data-message={post.message}
+ data-postid={post.id}
+ data-channelid={post.channel_id}
+ >
+ {'Edit'}
+ </a>
+ </li>
+ );
+ }
+
+ if (isOwner || isAdmin) {
+ dropdownContents.push(
+ <li
+ key='rhs-root-delete'
+ role='presentation'
+ >
+ <a
+ href='#'
+ role='menuitem'
+ onClick={() => EventHelpers.showDeletePostModal(post, this.props.commentCount)}
+ >
+ {'Delete'}
+ </a>
+ </li>
+ );
+ }
+
+ var rootOptions = '';
+ if (dropdownContents.length > 0) {
+ rootOptions = (
+ <div className='dropdown'>
+ <a
+ href='#'
className='post__dropdown dropdown-toggle'
type='button'
data-toggle='dropdown'
@@ -75,30 +120,7 @@ export default class RhsRootPost extends React.Component {
className='dropdown-menu'
role='menu'
>
- <li role='presentation'>
- <a
- href='#'
- role='menuitem'
- data-toggle='modal'
- data-target='#edit_post'
- data-refocusid='#reply_textbox'
- data-title={type}
- data-message={post.message}
- data-postid={post.id}
- data-channelid={post.channel_id}
- >
- {'Edit'}
- </a>
- </li>
- <li role='presentation'>
- <a
- href='#'
- role='menuitem'
- onClick={() => DeletePostModal.show(post, this.props.commentCount)}
- >
- {'Delete'}
- </a>
- </li>
+ {dropdownContents}
</ul>
</div>
);
@@ -166,7 +188,7 @@ export default class RhsRootPost extends React.Component {
</li>
<li className='col col__reply'>
<div className='dropdown'>
- {ownerOptions}
+ {rootOptions}
</div>
</li>
</ul>
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index 4f453f76b..61f138539 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -94,7 +94,7 @@ export default class RhsThread extends React.Component {
return;
}
- var currentPosts = PostStore.getPosts(currentSelected.posts[currentSelected.order[0]].channel_id);
+ var currentPosts = PostStore.getVisiblePosts(currentSelected.posts[currentSelected.order[0]].channel_id);
if (!currentPosts || currentPosts.order.length === 0) {
return;
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index 2202831a3..da422fe1b 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -1,17 +1,12 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import SearchStore from '../stores/search_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx';
import UserProfile from './user_profile.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as utils from '../utils/utils.jsx';
-import * as client from '../utils/client.jsx';
-import * as AsyncClient from '../utils/async_client.jsx';
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import Constants from '../utils/constants.jsx';
import * as TextFormatting from '../utils/text_formatting.jsx';
-var ActionTypes = Constants.ActionTypes;
export default class SearchResultsItem extends React.Component {
constructor(props) {
@@ -23,32 +18,7 @@ export default class SearchResultsItem extends React.Component {
handleClick(e) {
e.preventDefault();
- var self = this;
-
- client.getPost(
- this.props.post.channel_id,
- this.props.post.id,
- function success(data) {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POST_SELECTED,
- post_list: data,
- from_search: SearchStore.getSearchTerm()
- });
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_SEARCH,
- results: null,
- is_mention_search: self.props.isMentionSearch
- });
- },
- function success(err) {
- AsyncClient.dispatchError(err, 'getPost');
- }
- );
-
- var postChannel = ChannelStore.get(this.props.post.channel_id);
-
- utils.switchChannel(postChannel);
+ EventHelpers.emitPostFocusEvent(this.props.post.id);
}
render() {
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 77d222436..30422ff7d 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -201,10 +201,6 @@ export default class Sidebar extends React.Component {
});
}
updateScrollbar() {
- if (this.state.windowWidth > 768) {
- $('.nav-pills__container').perfectScrollbar();
- $('.nav-pills__container').perfectScrollbar('update');
- }
}
onChange() {
this.setState(this.getStateFromStores());
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index f6c0c8adb..0525eca4b 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -1,11 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import InviteMemberModal from './invite_member_modal.jsx';
import UserSettingsModal from './user_settings/user_settings_modal.jsx';
import UserStore from '../stores/user_store.jsx';
-import TeamStore from '../stores/team_store.jsx';
import * as client from '../utils/client.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as utils from '../utils/utils.jsx';
export default class SidebarRightMenu extends React.Component {
@@ -46,7 +45,7 @@ export default class SidebarRightMenu extends React.Component {
<li>
<a
href='#'
- onClick={InviteMemberModal.show}
+ onClick={EventHelpers.showInviteMemberModal}
>
<i className='fa fa-user'></i>Invite New Member
</a>
@@ -56,12 +55,12 @@ export default class SidebarRightMenu extends React.Component {
if (this.props.teamType === 'O') {
teamLink = (
<li>
- <a href='#'
- data-toggle='modal'
- data-target='#get_link'
- data-title='Team Invite'
- data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id}
- ><i className='fa fa-link'></i>Get Team Invite Link</a>
+ <a
+ href='#'
+ onClick={EventHelpers.showGetTeamInviteLinkModal}
+ >
+ <i className='glyphicon glyphicon-link'></i>{'Get Team Invite Link'}
+ </a>
</li>
);
}
diff --git a/web/react/components/toggle_modal_button.jsx b/web/react/components/toggle_modal_button.jsx
index eae4a024d..ce8ff3f60 100644
--- a/web/react/components/toggle_modal_button.jsx
+++ b/web/react/components/toggle_modal_button.jsx
@@ -22,7 +22,17 @@ export default class ModalToggleButton extends React.Component {
}
render() {
- const {children, dialogType, dialogProps, ...props} = this.props; //eslint-disable-line no-redeclare
+ const {children, dialogType, dialogProps, onClick, ...props} = this.props; // eslint-disable-line no-redeclare
+
+ // allow callers to provide an onClick which will be called before the modal is shown
+ let clickHandler = this.show;
+ if (onClick) {
+ clickHandler = () => {
+ onClick();
+
+ this.show();
+ };
+ }
// this assumes that all modals will have a show property and an onHide event
const dialog = React.createElement(this.props.dialogType, Object.assign({}, dialogProps, {
@@ -42,7 +52,7 @@ export default class ModalToggleButton extends React.Component {
<a
{...props}
href='#'
- onClick={this.show}
+ onClick={clickHandler}
>
{children}
{dialog}
@@ -54,7 +64,8 @@ export default class ModalToggleButton extends React.Component {
ModalToggleButton.propTypes = {
children: React.PropTypes.node.isRequired,
dialogType: React.PropTypes.func.isRequired,
- dialogProps: React.PropTypes.object
+ dialogProps: React.PropTypes.object,
+ onClick: React.PropTypes.func
};
ModalToggleButton.defaultProps = {
diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx
new file mode 100644
index 000000000..d7f255aaa
--- /dev/null
+++ b/web/react/dispatcher/event_helpers.jsx
@@ -0,0 +1,106 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import PostStore from '../stores/post_store.jsx';
+import Constants from '../utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as Client from '../utils/client.jsx';
+
+export function emitChannelClickEvent(channel) {
+ AsyncClient.getChannels();
+ AsyncClient.getChannelExtraInfo();
+ AsyncClient.updateLastViewedAt();
+ AsyncClient.getPosts(channel.id);
+
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.CLICK_CHANNEL,
+ name: channel.name,
+ id: channel.id
+ });
+}
+
+export function emitPostFocusEvent(postId) {
+ Client.getPostById(
+ postId,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_FOCUSED_POST,
+ postId,
+ post_list: data
+ });
+
+ AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS);
+ AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS);
+ }
+ );
+}
+
+export function emitLoadMorePostsEvent() {
+ const id = ChannelStore.getCurrentId();
+ loadMorePostsTop(id);
+}
+
+export function emitLoadMorePostsFocusedTopEvent() {
+ const id = PostStore.getFocusedPostId();
+ loadMorePostsTop(id);
+}
+
+export function loadMorePostsTop(id) {
+ const earliestPostId = PostStore.getEarliestPost(id).id;
+ if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) {
+ AsyncClient.getPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE);
+ }
+}
+
+export function emitLoadMorePostsFocusedBottomEvent() {
+ const id = PostStore.getFocusedPostId();
+ const latestPostId = PostStore.getLatestPost(id).id;
+ AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE);
+}
+
+export function emitPostRecievedEvent(post) {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POST,
+ post
+ });
+}
+
+export function emitUserPostedEvent(post) {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.CREATE_POST,
+ post
+ });
+}
+
+export function emitPostDeletedEvent(post) {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.POST_DELETED,
+ post
+ });
+}
+
+export function showDeletePostModal(post, commentCount = 0) {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_DELETE_POST_MODAL,
+ value: true,
+ post,
+ commentCount
+ });
+}
+
+export function showGetTeamInviteLinkModal() {
+ AppDispatcher.handleViewAction({
+ type: Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL,
+ value: true
+ });
+}
+
+export function showInviteMemberModal() {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL,
+ value: true
+ });
+}
diff --git a/web/react/package.json b/web/react/package.json
index b63fe35fb..41b2468af 100644
--- a/web/react/package.json
+++ b/web/react/package.json
@@ -22,13 +22,14 @@
"watchify": "3.6.1",
"eslint": "1.9.0",
"eslint-plugin-react": "3.9.0",
+ "exorcist": "0.4.0",
"babel-eslint": "4.1.5"
},
"scripts": {
"check": "",
"build-libs": "browserify -r crypto -r autolinker -r flux -r keymirror -r marked -r object-assign -r twemoji | uglifyjs -c -m --screw-ie8 > ../static/js/libs.min.js",
"start": "watchify --fast -x crypto -x node -x autolinker -x flux -x keymirror -x marked -x object-assign -x twemoji -o ../static/js/bundle.js -v -d ./**/*.jsx",
- "build": "browserify -x crypto -x autolinker -x flux -x keymirror -x marked -x object-assign -x twemoji ./**/*.jsx | uglifyjs -c -m --screw-ie8 > ../static/js/bundle.min.js"
+ "build": "browserify -x crypto -x autolinker -x flux -x keymirror -x marked -x object-assign -x twemoji -d ./**/*.jsx | exorcist ../static/js/inter.js.map > ../static/js/tmp.js && uglifyjs ../static/js/tmp.js --in-source-map \"../static/js/inter.js.map\" --source-map \"../static/js/bundle.min.js.map\" --source-map-url \"/static/js/bundle.min.js.map\" -c -m --screw-ie8 > ../static/js/bundle.min.js && rm ../static/js/tmp.js && rm ../static/js/inter.js.map"
},
"browserify": {
"transform": [
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index 126942e65..161e6ab22 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -1,15 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import ChannelView from '../components/channel_view.jsx';
import ChannelLoader from '../components/channel_loader.jsx';
import ErrorBar from '../components/error_bar.jsx';
import ErrorStore from '../stores/error_store.jsx';
import MentionList from '../components/mention_list.jsx';
-import GetLinkModal from '../components/get_link_modal.jsx';
-import EditChannelModal from '../components/edit_channel_modal.jsx';
+import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx';
import RenameChannelModal from '../components/rename_channel_modal.jsx';
import EditPostModal from '../components/edit_post_modal.jsx';
import DeletePostModal from '../components/delete_post_modal.jsx';
@@ -23,15 +21,14 @@ import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx
import InviteMemberModal from '../components/invite_member_modal.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
-import Constants from '../utils/constants.jsx';
-var ActionTypes = Constants.ActionTypes;
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
-function setupChannelPage(props) {
- AppDispatcher.handleViewAction({
- type: ActionTypes.CLICK_CHANNEL,
- name: props.ChannelName,
- id: props.ChannelId
- });
+function setupChannelPage(props, team, channel) {
+ if (props.PostId === '') {
+ EventHelpers.emitChannelClickEvent(channel);
+ } else {
+ EventHelpers.emitPostFocusEvent(props.PostId);
+ }
AsyncClient.getAllPreferences();
@@ -70,8 +67,8 @@ function setupChannelPage(props) {
// Modals
//
ReactDOM.render(
- <GetLinkModal />,
- document.getElementById('get_link_modal')
+ <GetTeamInviteLinkModal />,
+ document.getElementById('get_team_invite_link_modal')
);
ReactDOM.render(
@@ -95,11 +92,6 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <EditChannelModal />,
- document.getElementById('edit_channel_modal')
- );
-
- ReactDOM.render(
<RenameChannelModal />,
document.getElementById('rename_channel_modal')
);
diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx
index 2c1edaa3a..ff81c4994 100644
--- a/web/react/pages/home.jsx
+++ b/web/react/pages/home.jsx
@@ -1,12 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import ChannelStore from '../stores/channel_store.jsx';
import TeamStore from '../stores/team_store.jsx';
import Constants from '../utils/constants.jsx';
function setupHomePage() {
- var last = ChannelStore.getLastVisitedName();
+ var last = null;
if (last == null || last.length === 0) {
window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL;
} else {
diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx
index 1d481ada4..dec4926f5 100644
--- a/web/react/stores/channel_store.jsx
+++ b/web/react/stores/channel_store.jsx
@@ -8,8 +8,6 @@ var Utils;
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-import BrowserStore from '../stores/browser_store.jsx';
-
const CHANGE_EVENT = 'change';
const LEAVE_EVENT = 'leave';
const MORE_CHANGE_EVENT = 'change';
@@ -21,7 +19,38 @@ class ChannelStoreClass extends EventEmitter {
this.setMaxListeners(11);
+ this.emitChange = this.emitChange.bind(this);
+ this.addChangeListener = this.addChangeListener.bind(this);
+ this.removeChangeListener = this.removeChangeListener.bind(this);
+ this.emitMoreChange = this.emitMoreChange.bind(this);
+ this.addMoreChangeListener = this.addMoreChangeListener.bind(this);
+ this.removeMoreChangeListener = this.removeMoreChangeListener.bind(this);
+ this.emitExtraInfoChange = this.emitExtraInfoChange.bind(this);
+ this.addExtraInfoChangeListener = this.addExtraInfoChangeListener.bind(this);
+ this.removeExtraInfoChangeListener = this.removeExtraInfoChangeListener.bind(this);
+ this.emitLeave = this.emitLeave.bind(this);
+ this.addLeaveListener = this.addLeaveListener.bind(this);
+ this.removeLeaveListener = this.removeLeaveListener.bind(this);
+ this.findFirstBy = this.findFirstBy.bind(this);
+ this.get = this.get.bind(this);
+ this.getMember = this.getMember.bind(this);
+ this.getByName = this.getByName.bind(this);
+ this.pSetPostMode = this.pSetPostMode.bind(this);
+ this.getPostMode = this.getPostMode.bind(this);
+
this.currentId = null;
+ this.postMode = this.POST_MODE_CHANNEL;
+ this.channels = [];
+ this.channelMembers = {};
+ this.moreChannels = {};
+ this.moreChannels.loading = true;
+ this.extraInfos = {};
+ }
+ get POST_MODE_CHANNEL() {
+ return 1;
+ }
+ get POST_MODE_FOCUS() {
+ return 2;
}
emitChange() {
this.emit(CHANGE_EVENT);
@@ -90,16 +119,6 @@ class ChannelStoreClass extends EventEmitter {
setCurrentId(id) {
this.currentId = id;
}
- setLastVisitedName(name) {
- if (name == null) {
- BrowserStore.removeItem('last_visited_name');
- } else {
- BrowserStore.setItem('last_visited_name', name);
- }
- }
- getLastVisitedName() {
- return BrowserStore.getItem('last_visited_name');
- }
resetCounts(id) {
var cm = this.pGetChannelMembers();
for (var cmid in cm) {
@@ -192,10 +211,10 @@ class ChannelStoreClass extends EventEmitter {
this.pStoreChannels(channels);
}
pStoreChannels(channels) {
- BrowserStore.setItem('channels', channels);
+ this.channels = channels;
}
pGetChannels() {
- return BrowserStore.getItem('channels', []);
+ return this.channels;
}
pStoreChannelMember(channelMember) {
var members = this.pGetChannelMembers();
@@ -203,49 +222,58 @@ class ChannelStoreClass extends EventEmitter {
this.pStoreChannelMembers(members);
}
pStoreChannelMembers(channelMembers) {
- BrowserStore.setItem('channel_members', channelMembers);
+ this.channelMembers = channelMembers;
}
pGetChannelMembers() {
- return BrowserStore.getItem('channel_members', {});
+ return this.channelMembers;
}
pStoreMoreChannels(channels) {
- BrowserStore.setItem('more_channels', channels);
+ this.moreChannels = channels;
}
pGetMoreChannels() {
- var channels = BrowserStore.getItem('more_channels');
-
- if (channels == null) {
- channels = {};
- channels.loading = true;
- }
-
- return channels;
+ return this.moreChannels;
}
pStoreExtraInfos(extraInfos) {
- BrowserStore.setItem('extra_infos', extraInfos);
+ this.extraInfos = extraInfos;
}
pGetExtraInfos() {
- return BrowserStore.getItem('extra_infos', {});
+ return this.extraInfos;
}
isDefault(channel) {
return channel.name === Constants.DEFAULT_CHANNEL;
}
+
+ pSetPostMode(mode) {
+ this.postMode = mode;
+ }
+
+ getPostMode() {
+ return this.postMode;
+ }
}
var ChannelStore = new ChannelStoreClass();
-ChannelStore.dispatchToken = AppDispatcher.register(function handleAction(payload) {
+ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
var currentId;
switch (action.type) {
case ActionTypes.CLICK_CHANNEL:
ChannelStore.setCurrentId(action.id);
- ChannelStore.setLastVisitedName(action.name);
ChannelStore.resetCounts(action.id);
+ ChannelStore.pSetPostMode(ChannelStore.POST_MODE_CHANNEL);
ChannelStore.emitChange();
break;
+ case ActionTypes.RECIEVED_FOCUSED_POST: {
+ const post = action.post_list.posts[action.postId];
+ ChannelStore.setCurrentId(post.channel_id);
+ ChannelStore.pSetPostMode(ChannelStore.POST_MODE_FOCUS);
+ ChannelStore.emitChange();
+ break;
+ }
+
case ActionTypes.RECIEVED_CHANNELS:
ChannelStore.pStoreChannels(action.channels);
ChannelStore.pStoreChannelMembers(action.members);
diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx
index 69f43a5cf..a26a97f53 100644
--- a/web/react/stores/modal_store.jsx
+++ b/web/react/stores/modal_store.jsx
@@ -34,6 +34,7 @@ class ModalStoreClass extends EventEmitter {
case ActionTypes.TOGGLE_IMPORT_THEME_MODAL:
case ActionTypes.TOGGLE_INVITE_MEMBER_MODAL:
case ActionTypes.TOGGLE_DELETE_POST_MODAL:
+ case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL:
this.emit(type, value, args);
break;
}
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index ec01eef18..c76560c25 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -12,9 +12,10 @@ import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
const CHANGE_EVENT = 'change';
-const SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
+const FOCUSED_POST_CHANGE = 'focused_post_change';
const EDIT_POST_EVENT = 'edit_post';
const POSTS_VIEW_JUMP_EVENT = 'post_list_jump';
+const SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
class PostStoreClass extends EventEmitter {
constructor() {
@@ -24,10 +25,6 @@ class PostStoreClass extends EventEmitter {
this.addChangeListener = this.addChangeListener.bind(this);
this.removeChangeListener = this.removeChangeListener.bind(this);
- this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this);
- this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this);
- this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this);
-
this.emitEditPost = this.emitEditPost.bind(this);
this.addEditPostListener = this.addEditPostListener.bind(this);
this.removeEditPostListener = this.removeEditPostListner.bind(this);
@@ -36,27 +33,49 @@ class PostStoreClass extends EventEmitter {
this.addPostsViewJumpListener = this.addPostsViewJumpListener.bind(this);
this.removePostsViewJumpListener = this.removePostsViewJumpListener.bind(this);
- this.getCurrentPosts = this.getCurrentPosts.bind(this);
+ this.emitPostFocused = this.emitPostFocused.bind(this);
+ this.addPostFocusedListener = this.addPostFocusedListener.bind(this);
+ this.removePostFocusedListener = this.removePostFocusedListener.bind(this);
+
+ this.makePostsInfo = this.makePostsInfo.bind(this);
+
+ this.getAllPosts = this.getAllPosts.bind(this);
+ this.getEarliestPost = this.getEarliestPost.bind(this);
+ this.getLatestPost = this.getLatestPost.bind(this);
+ this.getVisiblePosts = this.getVisiblePosts.bind(this);
+ this.getVisibilityAtTop = this.getVisibilityAtTop.bind(this);
+ this.getVisibilityAtBottom = this.getVisibilityAtBottom.bind(this);
+ this.requestVisibilityIncrease = this.requestVisibilityIncrease.bind(this);
+ this.getFocusedPostId = this.getFocusedPostId.bind(this);
+
this.storePosts = this.storePosts.bind(this);
- this.pStorePosts = this.pStorePosts.bind(this);
- this.getPosts = this.getPosts.bind(this);
- this.getPost = this.getPost.bind(this);
this.storePost = this.storePost.bind(this);
- this.pStorePost = this.pStorePost.bind(this);
+ this.storeFocusedPost = this.storeFocusedPost.bind(this);
+ this.checkBounds = this.checkBounds.bind(this);
+
+ this.clearFocusedPost = this.clearFocusedPost.bind(this);
+ this.clearChannelVisibility = this.clearChannelVisibility.bind(this);
+
this.removePost = this.removePost.bind(this);
- this.storePendingPost = this.storePendingPost.bind(this);
- this.pStorePendingPosts = this.pStorePendingPosts.bind(this);
+
this.getPendingPosts = this.getPendingPosts.bind(this);
- this.storeUnseenDeletedPost = this.storeUnseenDeletedPost.bind(this);
- this.storeUnseenDeletedPosts = this.storeUnseenDeletedPosts.bind(this);
- this.getUnseenDeletedPosts = this.getUnseenDeletedPosts.bind(this);
- this.clearUnseenDeletedPosts = this.clearUnseenDeletedPosts.bind(this);
+ this.storePendingPost = this.storePendingPost.bind(this);
this.removePendingPost = this.removePendingPost.bind(this);
- this.pRemovePendingPost = this.pRemovePendingPost.bind(this);
this.clearPendingPosts = this.clearPendingPosts.bind(this);
this.updatePendingPost = this.updatePendingPost.bind(this);
+
+ this.storeUnseenDeletedPost = this.storeUnseenDeletedPost.bind(this);
+ this.getUnseenDeletedPosts = this.getUnseenDeletedPosts.bind(this);
+ this.clearUnseenDeletedPosts = this.clearUnseenDeletedPosts.bind(this);
+
+ // These functions are bad and work should be done to remove this system when the RHS dies
this.storeSelectedPost = this.storeSelectedPost.bind(this);
this.getSelectedPost = this.getSelectedPost.bind(this);
+ this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this);
+ this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this);
+ this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this);
+ this.selectedPost = null;
+
this.getEmptyDraft = this.getEmptyDraft.bind(this);
this.storeCurrentDraft = this.storeCurrentDraft.bind(this);
this.getCurrentDraft = this.getCurrentDraft.bind(this);
@@ -70,6 +89,9 @@ class PostStoreClass extends EventEmitter {
this.getLatestUpdate = this.getLatestUpdate.bind(this);
this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this);
this.getCommentCount = this.getCommentCount.bind(this);
+
+ this.postsInfo = {};
+ this.currentFocusedPostId = null;
}
emitChange() {
this.emit(CHANGE_EVENT);
@@ -83,16 +105,16 @@ class PostStoreClass extends EventEmitter {
this.removeListener(CHANGE_EVENT, callback);
}
- emitSelectedPostChange(fromSearch) {
- this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch);
+ emitPostFocused() {
+ this.emit(FOCUSED_POST_CHANGE);
}
- addSelectedPostChangeListener(callback) {
- this.on(SELECTED_POST_CHANGE_EVENT, callback);
+ addPostFocusedListener(callback) {
+ this.on(FOCUSED_POST_CHANGE, callback);
}
- removeSelectedPostChangeListener(callback) {
- this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
+ removePostFocusedListener(callback) {
+ this.removeListener(FOCUSED_POST_CHANGE, callback);
}
emitEditPost(post) {
@@ -131,104 +153,157 @@ class PostStoreClass extends EventEmitter {
this.emitPostsViewJump(Constants.PostsViewJumpTypes.SIDEBAR_OPEN, null);
}
- getCurrentPosts() {
- var currentId = ChannelStore.getCurrentId();
+ // All this does is makes sure the postsInfo is not null for the specified channel
+ makePostsInfo(id) {
+ if (!this.postsInfo.hasOwnProperty(id)) {
+ this.postsInfo[id] = {};
+ }
+ }
- if (currentId != null) {
- return this.getPosts(currentId);
+ getAllPosts(id) {
+ if (this.postsInfo.hasOwnProperty(id)) {
+ return Object.assign({}, this.postsInfo[id].postList);
}
+
return null;
}
- storePosts(channelId, newPostsView) {
- if (isPostListNull(newPostsView)) {
+
+ getEarliestPost(id) {
+ if (this.postsInfo.hasOwnProperty(id)) {
+ return this.postsInfo[id].postList.posts[this.postsInfo[id].postList.order[this.postsInfo[id].postList.order.length - 1]];
+ }
+
+ return null;
+ }
+
+ getLatestPost(id) {
+ if (this.postsInfo.hasOwnProperty(id)) {
+ return this.postsInfo[id].postList.posts[this.postsInfo[id].postList.order[0]];
+ }
+
+ return null;
+ }
+
+ getVisiblePosts(id) {
+ if (this.postsInfo.hasOwnProperty(id) && this.postsInfo[id].hasOwnProperty('postList')) {
+ const postList = JSON.parse(JSON.stringify(this.postsInfo[id].postList));
+
+ // Only limit visibility if we are not focused on a post
+ if (this.currentFocusedPostId === null) {
+ postList.order = postList.order.slice(0, this.postsInfo[id].endVisible);
+ }
+
+ // Add pending posts
+ if (this.postsInfo[id].hasOwnProperty('pendingPosts')) {
+ Object.assign(postList.posts, this.postsInfo[id].pendingPosts.posts);
+ postList.order = this.postsInfo[id].pendingPosts.order.concat(postList.order);
+ }
+
+ // Add delteted posts
+ if (this.postsInfo[id].hasOwnProperty('deletedPosts')) {
+ Object.assign(postList.posts, this.postsInfo[id].deletedPosts);
+
+ for (const postID in this.postsInfo[id].deletedPosts) {
+ if (this.postsInfo[id].deletedPosts.hasOwnProperty(postID)) {
+ postList.order.push(postID);
+ }
+ }
+
+ // Merge would be faster
+ postList.order.sort((a, b) => {
+ if (postList.posts[a].create_at > postList.posts[b].create_at) {
+ return -1;
+ }
+ if (postList.posts[a].create_at < postList.posts[b].create_at) {
+ return 1;
+ }
+ return 0;
+ });
+ }
+
+ return postList;
+ }
+
+ return null;
+ }
+
+ getVisibilityAtTop(id) {
+ if (this.postsInfo.hasOwnProperty(id)) {
+ return this.postsInfo[id].atTop && this.postsInfo[id].endVisible >= this.postsInfo[id].postList.order.length;
+ }
+
+ return false;
+ }
+
+ getVisibilityAtBottom(id) {
+ if (this.postsInfo.hasOwnProperty(id)) {
+ return this.postsInfo[id].atBottom;
+ }
+
+ return false;
+ }
+
+ // Returns true if posts need to be fetched
+ requestVisibilityIncrease(id, ammount) {
+ const endVisible = this.postsInfo[id].endVisible;
+ const postList = this.postsInfo[id].postList;
+ if (this.getVisibilityAtTop(id)) {
+ return false;
+ }
+ this.postsInfo[id].endVisible += ammount;
+ this.emitChange();
+ return endVisible + ammount > postList.order.length;
+ }
+
+ getFocusedPostId() {
+ return this.currentFocusedPostId;
+ }
+
+ storePosts(id, newPosts) {
+ if (isPostListNull(newPosts)) {
return;
}
- var postList = makePostListNonNull(this.getPosts(channelId));
+ const combinedPosts = makePostListNonNull(this.getAllPosts(id));
- for (const pid in newPostsView.posts) {
- if (newPostsView.posts.hasOwnProperty(pid)) {
- const np = newPostsView.posts[pid];
+ for (const pid in newPosts.posts) {
+ if (newPosts.posts.hasOwnProperty(pid)) {
+ const np = newPosts.posts[pid];
if (np.delete_at === 0) {
- postList.posts[pid] = np;
- if (postList.order.indexOf(pid) === -1) {
- postList.order.push(pid);
+ combinedPosts.posts[pid] = np;
+ if (combinedPosts.order.indexOf(pid) === -1) {
+ combinedPosts.order.push(pid);
}
} else {
- if (pid in postList.posts) {
- delete postList.posts[pid];
+ if (pid in combinedPosts.posts) {
+ Reflect.deleteProperty(combinedPosts.posts, pid);
}
- const index = postList.order.indexOf(pid);
+ const index = combinedPosts.order.indexOf(pid);
if (index !== -1) {
- postList.order.splice(index, 1);
+ combinedPosts.order.splice(index, 1);
}
}
}
}
- postList.order.sort((a, b) => {
- if (postList.posts[a].create_at > postList.posts[b].create_at) {
+ combinedPosts.order.sort((a, b) => {
+ if (combinedPosts.posts[a].create_at > combinedPosts.posts[b].create_at) {
return -1;
}
- if (postList.posts[a].create_at < postList.posts[b].create_at) {
+ if (combinedPosts.posts[a].create_at < combinedPosts.posts[b].create_at) {
return 1;
}
return 0;
});
- var latestUpdate = 0;
- for (var pid in postList.posts) {
- if (postList.posts[pid].update_at > latestUpdate) {
- latestUpdate = postList.posts[pid].update_at;
- }
- }
-
- this.storeLatestUpdate(channelId, latestUpdate);
- this.pStorePosts(channelId, postList);
- this.emitChange();
- }
- pStorePosts(channelId, posts) {
- BrowserStore.setItem('posts_' + channelId, posts);
- }
- getPosts(channelId) {
- return BrowserStore.getItem('posts_' + channelId);
+ this.makePostsInfo(id);
+ this.postsInfo[id].postList = combinedPosts;
}
- getPost(channelId, postId) {
- return this.getPosts(channelId).posts[postId];
- }
- getCurrentUsersLatestPost(channelId, rootId) {
- const userId = UserStore.getCurrentId();
- var postList = makePostListNonNull(this.getPosts(channelId));
- var i = 0;
- var len = postList.order.length;
- var lastPost = null;
- for (i; i < len; i++) {
- let post = postList.posts[postList.order[i]];
- if (post.user_id === userId && (post.props && !post.props.from_webhook || !post.props)) {
- if (rootId) {
- if (post.root_id === rootId || post.id === rootId) {
- lastPost = post;
- break;
- }
- } else {
- lastPost = post;
- break;
- }
- }
- }
-
- return lastPost;
- }
storePost(post) {
- this.pStorePost(post);
- this.emitChange();
- }
- pStorePost(post) {
- var postList = this.getPosts(post.channel_id);
- postList = makePostListNonNull(postList);
+ const postList = makePostListNonNull(this.getAllPosts(post.channel_id));
if (post.pending_post_id !== '') {
this.removePendingPost(post.channel_id, post.pending_post_id);
@@ -241,65 +316,117 @@ class PostStoreClass extends EventEmitter {
postList.order.unshift(post.id);
}
- this.pStorePosts(post.channel_id, postList);
+ this.makePostsInfo(post.channel_id);
+ this.postsInfo[post.channel_id].postList = postList;
+ }
+
+ storeFocusedPost(postId, postList) {
+ const focusedPost = postList.posts[postId];
+ if (!focusedPost) {
+ return;
+ }
+ this.currentFocusedPostId = postId;
+ this.storePosts(postId, postList);
+ }
+
+ checkBounds(id, numRequested, postList, before) {
+ if (numRequested > postList.order.length) {
+ if (before) {
+ this.postsInfo[id].atTop = true;
+ } else {
+ this.postsInfo[id].atBottom = true;
+ }
+ }
}
- removePost(postId, channelId) {
- var postList = this.getPosts(channelId);
+
+ clearFocusedPost() {
+ if (this.currentFocusedPostId != null) {
+ Reflect.deleteProperty(this.postsInfo, this.currentFocusedPostId);
+ this.currentFocusedPostId = null;
+ }
+ }
+
+ clearChannelVisibility(id, atBottom) {
+ this.makePostsInfo(id);
+ this.postsInfo[id].endVisible = Constants.POST_CHUNK_SIZE;
+ this.postsInfo[id].atTop = false;
+ this.postsInfo[id].atBottom = atBottom;
+ }
+
+ removePost(post) {
+ const channelId = post.channel_id;
+ this.makePostsInfo(channelId);
+ const postList = this.postsInfo[channelId].postList;
if (isPostListNull(postList)) {
return;
}
- if (postId in postList.posts) {
- delete postList.posts[postId];
+ if (post.id in postList.posts) {
+ Reflect.deleteProperty(postList.posts, post.id);
}
- var index = postList.order.indexOf(postId);
+ const index = postList.order.indexOf(post.id);
if (index !== -1) {
postList.order.splice(index, 1);
}
- this.pStorePosts(channelId, postList);
+ this.postsInfo[channelId].postList = postList;
}
+
+ getPendingPosts(channelId) {
+ if (this.postsInfo.hasOwnProperty(channelId)) {
+ return this.postsInfo[channelId].pendingPosts;
+ }
+
+ return null;
+ }
+
storePendingPost(post) {
post.state = Constants.POST_LOADING;
- var postList = this.getPendingPosts(post.channel_id);
- postList = makePostListNonNull(postList);
+ const postList = makePostListNonNull(this.getPendingPosts(post.channel_id));
postList.posts[post.pending_post_id] = post;
postList.order.unshift(post.pending_post_id);
- this.pStorePendingPosts(post.channel_id, postList);
+
+ this.makePostsInfo(post.channel_id);
+ this.postsInfo[post.channel_id].pendingPosts = postList;
this.emitChange();
}
- pStorePendingPosts(channelId, postList) {
- var posts = postList.posts;
- // sort failed posts to the bottom
- postList.order.sort((a, b) => {
- if (posts[a].state === Constants.POST_LOADING && posts[b].state === Constants.POST_FAILED) {
- return 1;
- }
- if (posts[a].state === Constants.POST_FAILED && posts[b].state === Constants.POST_LOADING) {
- return -1;
- }
+ removePendingPost(channelId, pendingPostId) {
+ const postList = makePostListNonNull(this.getPendingPosts(channelId));
- if (posts[a].create_at > posts[b].create_at) {
- return -1;
- }
- if (posts[a].create_at < posts[b].create_at) {
- return 1;
- }
+ Reflect.deleteProperty(postList.posts, pendingPostId);
+ const index = postList.order.indexOf(pendingPostId);
+ if (index !== -1) {
+ postList.order.splice(index, 1);
+ }
- return 0;
- });
+ this.postsInfo[channelId].pendingPosts = postList;
+ this.emitChange();
+ }
- BrowserStore.setGlobalItem('pending_posts_' + channelId, postList);
+ clearPendingPosts(channelId) {
+ if (this.postsInfo.hasOwnProperty(channelId)) {
+ Reflect.deleteProperty(this.postsInfo[channelId], 'pendingPosts');
+ }
}
- getPendingPosts(channelId) {
- return BrowserStore.getGlobalItem('pending_posts_' + channelId);
+
+ updatePendingPost(post) {
+ const postList = makePostListNonNull(this.getPendingPosts(post.channel_id));
+
+ if (postList.order.indexOf(post.pending_post_id) === -1) {
+ return;
+ }
+
+ postList.posts[post.pending_post_id] = post;
+ this.postsInfo[post.channel_id].pendingPosts = postList;
+ this.emitChange();
}
+
storeUnseenDeletedPost(post) {
- var posts = this.getUnseenDeletedPosts(post.channel_id);
+ let posts = this.getUnseenDeletedPosts(post.channel_id);
if (!posts) {
posts = {};
@@ -310,58 +437,68 @@ class PostStoreClass extends EventEmitter {
post.filenames = [];
posts[post.id] = post;
- this.storeUnseenDeletedPosts(post.channel_id, posts);
- }
- storeUnseenDeletedPosts(channelId, posts) {
- BrowserStore.setItem('deleted_posts_' + channelId, posts);
+ this.postsInfo[post.channel_id].deletedPosts = posts;
}
+
getUnseenDeletedPosts(channelId) {
- return BrowserStore.getItem('deleted_posts_' + channelId);
+ if (this.postsInfo.hasOwnProperty(channelId)) {
+ return this.postsInfo[channelId].deletedPosts;
+ }
+
+ return null;
}
+
clearUnseenDeletedPosts(channelId) {
- BrowserStore.setItem('deleted_posts_' + channelId, {});
+ if (this.postsInfo.hasOwnProperty(channelId)) {
+ Reflect.deleteProperty(this.postsInfo[channelId], 'deletedPosts');
+ }
}
- removePendingPost(channelId, pendingPostId) {
- this.pRemovePendingPost(channelId, pendingPostId);
- this.emitChange();
+
+ storeSelectedPost(postList) {
+ this.selectedPost = postList;
}
- pRemovePendingPost(channelId, pendingPostId) {
- var postList = this.getPendingPosts(channelId);
- postList = makePostListNonNull(postList);
- if (pendingPostId in postList.posts) {
- delete postList.posts[pendingPostId];
- }
- var index = postList.order.indexOf(pendingPostId);
- if (index !== -1) {
- postList.order.splice(index, 1);
- }
+ getSelectedPost() {
+ return this.selectedPost;
+ }
- this.pStorePendingPosts(channelId, postList);
+ emitSelectedPostChange(fromSearch) {
+ this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch);
}
- clearPendingPosts() {
- BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', (key) => {
- BrowserStore.removeItem(key);
- });
+
+ addSelectedPostChangeListener(callback) {
+ this.on(SELECTED_POST_CHANGE_EVENT, callback);
}
- updatePendingPost(post) {
- var postList = this.getPendingPosts(post.channel_id);
- postList = makePostListNonNull(postList);
- if (postList.order.indexOf(post.pending_post_id) === -1) {
- return;
+ removeSelectedPostChangeListener(callback) {
+ this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
+ }
+
+ getCurrentUsersLatestPost(channelId, rootId) {
+ const userId = UserStore.getCurrentId();
+ var postList = makePostListNonNull(this.getAllPosts(channelId));
+ var i = 0;
+ var len = postList.order.length;
+ var lastPost = null;
+
+ for (i; i < len; i++) {
+ const post = postList.posts[postList.order[i]];
+ if (post.user_id === userId && (post.props && !post.props.from_webhook || !post.props)) {
+ if (rootId) {
+ if (post.root_id === rootId || post.id === rootId) {
+ lastPost = post;
+ break;
+ }
+ } else {
+ lastPost = post;
+ break;
+ }
+ }
}
- postList.posts[post.pending_post_id] = post;
- this.pStorePendingPosts(post.channel_id, postList);
- this.emitChange();
- }
- storeSelectedPost(postList) {
- BrowserStore.setItem('select_post', postList);
- }
- getSelectedPost() {
- return BrowserStore.getItem('select_post');
+ return lastPost;
}
+
getEmptyDraft() {
return {message: '', uploadsInProgress: [], previews: []};
}
@@ -402,16 +539,23 @@ class PostStoreClass extends EventEmitter {
});
}
storeLatestUpdate(channelId, time) {
- BrowserStore.setItem('latest_post_' + channelId, time);
+ if (!this.postsInfo.hasOwnProperty(channelId)) {
+ this.postsInfo[channelId] = {};
+ }
+ this.postsInfo[channelId].latestPost = time;
}
getLatestUpdate(channelId) {
- return BrowserStore.getItem('latest_post_' + channelId, 0);
+ if (this.postsInfo.hasOwnProperty(channelId) && this.postsInfo[channelId].hasOwnProperty('latestPost')) {
+ return this.postsInfo[channelId].latestPost;
+ }
+
+ return 0;
}
getCommentCount(post) {
const posts = this.getPosts(post.channel_id).posts;
let commentCount = 0;
- for (let id in posts) {
+ for (const id in posts) {
if (posts.hasOwnProperty(id)) {
if (posts[id].root_id === post.id) {
commentCount += 1;
@@ -429,20 +573,45 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
- case ActionTypes.RECIEVED_POSTS:
- PostStore.storePosts(action.id, makePostListNonNull(action.post_list));
+ case ActionTypes.RECIEVED_POSTS: {
+ const id = PostStore.currentFocusedPostId == null ? action.id : PostStore.currentFocusedPostId;
+ PostStore.checkBounds(id, action.numRequested, makePostListNonNull(action.post_list), action.before);
+ PostStore.storePosts(id, makePostListNonNull(action.post_list));
+ PostStore.emitChange();
+ break;
+ }
+ case ActionTypes.RECIEVED_FOCUSED_POST:
+ PostStore.clearChannelVisibility(action.postId, false);
+ PostStore.storeFocusedPost(action.postId, makePostListNonNull(action.post_list));
+ PostStore.emitChange();
break;
case ActionTypes.RECIEVED_POST:
- PostStore.pStorePost(action.post);
+ PostStore.storePost(action.post);
+ PostStore.emitChange();
+ break;
+ case ActionTypes.RECIEVED_EDIT_POST:
+ PostStore.emitEditPost(action);
+ PostStore.emitChange();
+ break;
+ case ActionTypes.CLICK_CHANNEL:
+ PostStore.clearFocusedPost();
+ PostStore.clearChannelVisibility(action.id, true);
+ PostStore.clearUnseenDeletedPosts(action.id);
+ break;
+ case ActionTypes.CREATE_POST:
+ PostStore.storePendingPost(action.post);
+ PostStore.storeDraft(action.post.channel_id, null);
+ PostStore.jumpPostsViewToBottom();
+ break;
+ case ActionTypes.POST_DELETED:
+ PostStore.storeUnseenDeletedPost(action.post);
+ PostStore.removePost(action.post);
PostStore.emitChange();
break;
case ActionTypes.RECIEVED_POST_SELECTED:
PostStore.storeSelectedPost(action.post_list);
PostStore.emitSelectedPostChange(action.from_search);
break;
- case ActionTypes.RECIEVED_EDIT_POST:
- PostStore.emitEditPost(action);
- break;
default:
}
});
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index f2936c50a..2e0769cc4 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -1,7 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import UserStore from './user_store.jsx';
import PostStore from './post_store.jsx';
import ChannelStore from './channel_store.jsx';
@@ -11,9 +10,9 @@ import EventEmitter from 'events';
import * as Utils from '../utils/utils.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import Constants from '../utils/constants.jsx';
-const ActionTypes = Constants.ActionTypes;
const SocketEvents = Constants.SocketEvents;
const CHANGE_EVENT = 'change';
@@ -91,10 +90,9 @@ class SocketStoreClass extends EventEmitter {
};
conn.onmessage = (evt) => {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_MSG,
- msg: JSON.parse(evt.data)
- });
+ const msg = JSON.parse(evt.data);
+ this.handleMessage(msg);
+ this.emitChange(msg);
};
}
}
@@ -153,12 +151,12 @@ class SocketStoreClass extends EventEmitter {
function handleNewPostEvent(msg) {
// Store post
const post = JSON.parse(msg.props.post);
- PostStore.storePost(post);
+ EventHelpers.emitPostRecievedEvent(post);
// Update channel state
if (ChannelStore.getCurrentId() === msg.channel_id) {
if (window.isActive) {
- AsyncClient.updateLastViewedAt(true);
+ AsyncClient.updateLastViewedAt();
}
} else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) {
AsyncClient.getChannel(msg.channel_id);
@@ -237,20 +235,17 @@ function handlePostEditEvent(msg) {
function handlePostDeleteEvent(msg) {
const post = JSON.parse(msg.props.post);
-
- PostStore.storeUnseenDeletedPost(post);
- PostStore.removePost(post, true);
- PostStore.emitChange();
+ EventHelpers.emitPostDeletedEvent(post);
}
function handleNewUserEvent() {
AsyncClient.getProfiles();
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannelExtraInfo();
}
function handleUserAddedEvent(msg) {
if (ChannelStore.getCurrentId() === msg.channel_id) {
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannelExtraInfo();
}
if (UserStore.getCurrentId() === msg.user_id) {
@@ -273,7 +268,7 @@ function handleUserRemovedEvent(msg) {
$('#removed_from_channel').modal('show');
}
} else if (ChannelStore.getCurrentId() === msg.channel_id) {
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannelExtraInfo();
}
}
@@ -286,17 +281,12 @@ function handleChannelViewedEvent(msg) {
var SocketStore = new SocketStoreClass();
-SocketStore.dispatchToken = AppDispatcher.register((payload) => {
+/*SocketStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
- case ActionTypes.RECIEVED_MSG:
- SocketStore.handleMessage(action.msg);
- SocketStore.emitChange(action.msg);
- break;
-
default:
}
-});
+ });*/
export default SocketStore;
diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx
index 26c83cc8c..2d518d9e7 100644
--- a/web/react/stores/team_store.jsx
+++ b/web/react/stores/team_store.jsx
@@ -31,6 +31,7 @@ class TeamStoreClass extends EventEmitter {
this.getCurrentId = this.getCurrentId.bind(this);
this.getCurrent = this.getCurrent.bind(this);
this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this);
+ this.getCurrentInviteLink = this.getCurrentInviteLink.bind(this);
this.saveTeam = this.saveTeam.bind(this);
}
@@ -92,6 +93,16 @@ class TeamStoreClass extends EventEmitter {
return null;
}
+ getCurrentInviteLink() {
+ const current = this.getCurrent();
+
+ if (current) {
+ return getWindowLocationOrigin() + '/signup_user_complete/?id=' + current.invite_id;
+ }
+
+ return '';
+ }
+
saveTeam(team) {
var teams = this.getAll();
teams[team.id] = team;
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index fac4cd009..8cf111d55 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -40,88 +40,42 @@ function isCallInProgress(callName) {
return true;
}
-export function getChannels(force, updateLastViewed, checkVersion) {
- var channels = ChannelStore.getAll();
-
- if (channels.length === 0 || force) {
- if (isCallInProgress('getChannels')) {
- return;
- }
-
- callTracker.getChannels = utils.getTimestamp();
+export function getChannels(checkVersion) {
+ if (isCallInProgress('getChannels')) {
+ return;
+ }
- client.getChannels(
- (data, textStatus, xhr) => {
- callTracker.getChannels = 0;
+ callTracker.getChannels = utils.getTimestamp();
- if (checkVersion) {
- var serverVersion = xhr.getResponseHeader('X-Version-ID');
+ client.getChannels(
+ (data, textStatus, xhr) => {
+ callTracker.getChannels = 0;
- if (!BrowserStore.getLastServerVersion()) {
- BrowserStore.setLastServerVersion(serverVersion);
- }
+ if (checkVersion) {
+ var serverVersion = xhr.getResponseHeader('X-Version-ID');
- if (serverVersion !== BrowserStore.getLastServerVersion()) {
- BrowserStore.setLastServerVersion(serverVersion);
- window.location.reload(true);
- console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
- }
+ if (serverVersion !== BrowserStore.getLastServerVersion()) {
+ BrowserStore.setLastServerVersion(serverVersion);
+ window.location.reload(true);
+ console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
}
-
- if (xhr.status === 304 || !data) {
- return;
- }
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_CHANNELS,
- channels: data.channels,
- members: data.members
- });
- },
- (err) => {
- callTracker.getChannels = 0;
- dispatchError(err, 'getChannels');
}
- );
- } else {
- if (isCallInProgress('getChannelCounts')) {
- return;
- }
-
- callTracker.getChannelCounts = utils.getTimestamp();
-
- client.getChannelCounts(
- function getChannelCountsSuccess(data, textStatus, xhr) {
- callTracker.getChannelCounts = 0;
-
- if (xhr.status === 304 || !data) {
- return;
- }
- var countMap = data.counts;
- var updateAtMap = data.update_times;
-
- for (var id in countMap) {
- if ({}.hasOwnProperty.call(countMap, id)) {
- var c = ChannelStore.get(id);
- var count = countMap[id];
- var updateAt = updateAtMap[id];
- if (!c || c.total_msg_count !== count || updateAt > c.update_at) {
- getChannel(id);
- }
- }
- }
- },
- function getChannelCountsFailure(err) {
- callTracker.getChannelCounts = 0;
- dispatchError(err, 'getChannelCounts');
+ if (xhr.status === 304 || !data) {
+ return;
}
- );
- }
- if (updateLastViewed && ChannelStore.getCurrentId() != null) {
- updateLastViewedAt();
- }
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_CHANNELS,
+ channels: data.channels,
+ members: data.members
+ });
+ },
+ (err) => {
+ callTracker.getChannels = 0;
+ dispatchError(err, 'getChannels');
+ }
+ );
}
export function getChannel(id) {
@@ -152,14 +106,14 @@ export function getChannel(id) {
);
}
-export function updateLastViewedAt(force) {
+export function updateLastViewedAt() {
const channelId = ChannelStore.getCurrentId();
if (channelId === null) {
return;
}
- if (isCallInProgress(`updateLastViewed${channelId}`) && !force) {
+ if (isCallInProgress(`updateLastViewed${channelId}`)) {
return;
}
@@ -205,40 +159,35 @@ export function getMoreChannels(force) {
}
}
-export function getChannelExtraInfo(force) {
- var channelId = ChannelStore.getCurrentId();
+export function getChannelExtraInfo() {
+ const channelId = ChannelStore.getCurrentId();
if (channelId != null) {
if (isCallInProgress('getChannelExtraInfo_' + channelId)) {
return;
}
- var minMembers = 0;
- if (ChannelStore.getCurrent() && ChannelStore.getCurrent().type === 'D') {
- minMembers = 1;
- }
- if (ChannelStore.getCurrentExtraInfo().members.length <= minMembers || force) {
- callTracker['getChannelExtraInfo_' + channelId] = utils.getTimestamp();
- client.getChannelExtraInfo(
- channelId,
- function getChannelExtraInfoSuccess(data, textStatus, xhr) {
- callTracker['getChannelExtraInfo_' + channelId] = 0;
-
- if (xhr.status === 304 || !data) {
- return;
- }
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO,
- extra_info: data
- });
- },
- function getChannelExtraInfoFailure(err) {
- callTracker['getChannelExtraInfo_' + channelId] = 0;
- dispatchError(err, 'getChannelExtraInfo');
+ callTracker['getChannelExtraInfo_' + channelId] = utils.getTimestamp();
+
+ client.getChannelExtraInfo(
+ channelId,
+ (data, textStatus, xhr) => {
+ callTracker['getChannelExtraInfo_' + channelId] = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
}
- );
- }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO,
+ extra_info: data
+ });
+ },
+ (err) => {
+ callTracker['getChannelExtraInfo_' + channelId] = 0;
+ dispatchError(err, 'getChannelExtraInfo');
+ }
+ );
}
}
@@ -457,89 +406,92 @@ export function search(terms) {
);
}
-export function getPostsPage(force, id, maxPosts) {
- if (PostStore.getCurrentPosts() == null || force) {
- var channelId = id;
+export function getPostsPage(id, maxPosts) {
+ let channelId = id;
+ if (channelId == null) {
+ channelId = ChannelStore.getCurrentId();
if (channelId == null) {
- channelId = ChannelStore.getCurrentId();
- }
-
- if (isCallInProgress('getPostsPage_' + channelId)) {
return;
}
+ }
- var postList = PostStore.getCurrentPosts();
+ if (isCallInProgress('getPostsPage_' + channelId)) {
+ return;
+ }
- var max = maxPosts;
- if (max == null) {
- max = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS;
- }
+ var postList = PostStore.getAllPosts(id);
- // if we already have more than POST_CHUNK_SIZE posts,
- // let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE,
- // with a max at maxPosts
- var numPosts = Math.min(max, Constants.POST_CHUNK_SIZE);
- if (postList && postList.order.length > 0) {
- numPosts = Math.min(max, Constants.POST_CHUNK_SIZE * Math.ceil(postList.order.length / Constants.POST_CHUNK_SIZE));
- }
+ var max = maxPosts;
+ if (max == null) {
+ max = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS;
+ }
+
+ // if we already have more than POST_CHUNK_SIZE posts,
+ // let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE,
+ // with a max at maxPosts
+ var numPosts = Math.min(max, Constants.POST_CHUNK_SIZE);
+ if (postList && postList.order.length > 0) {
+ numPosts = Math.min(max, Constants.POST_CHUNK_SIZE * Math.ceil(postList.order.length / Constants.POST_CHUNK_SIZE));
+ }
+
+ if (channelId != null) {
+ callTracker['getPostsPage_' + channelId] = utils.getTimestamp();
- if (channelId != null) {
- callTracker['getPostsPage_' + channelId] = utils.getTimestamp();
-
- client.getPostsPage(
- channelId,
- 0,
- numPosts,
- function getPostsPageSuccess(data, textStatus, xhr) {
- if (xhr.status === 304 || !data) {
- return;
- }
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POSTS,
- id: channelId,
- post_list: data
- });
-
- getProfiles();
- },
- function getPostsPageFailure(err) {
- dispatchError(err, 'getPostsPage');
- },
- function getPostsPageComplete() {
- callTracker['getPostsPage_' + channelId] = 0;
+ client.getPostsPage(
+ channelId,
+ 0,
+ numPosts,
+ (data, textStatus, xhr) => {
+ if (xhr.status === 304 || !data) {
+ return;
}
- );
- }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POSTS,
+ id: channelId,
+ before: true,
+ numRequested: numPosts,
+ post_list: data
+ });
+
+ getProfiles();
+ },
+ (err) => {
+ dispatchError(err, 'getPostsPage');
+ },
+ () => {
+ callTracker['getPostsPage_' + channelId] = 0;
+ }
+ );
}
}
export function getPosts(id) {
- var channelId = id;
+ let channelId = id;
if (channelId == null) {
- if (ChannelStore.getCurrentId() == null) {
+ channelId = ChannelStore.getCurrentId();
+ if (channelId == null) {
return;
}
- channelId = ChannelStore.getCurrentId();
}
if (isCallInProgress('getPosts_' + channelId)) {
return;
}
- if (PostStore.getCurrentPosts() == null) {
- getPostsPage(true, id, Constants.POST_CHUNK_SIZE);
+ if (PostStore.getAllPosts(channelId) == null) {
+ getPostsPage(channelId, Constants.POST_CHUNK_SIZE);
return;
}
- var latestUpdate = PostStore.getLatestUpdate(channelId);
+ const latestUpdate = PostStore.getLatestUpdate(channelId);
callTracker['getPosts_' + channelId] = utils.getTimestamp();
client.getPosts(
channelId,
latestUpdate,
- function success(data, textStatus, xhr) {
+ (data, textStatus, xhr) => {
if (xhr.status === 304 || !data) {
return;
}
@@ -547,20 +499,100 @@ export function getPosts(id) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POSTS,
id: channelId,
+ before: true,
+ numRequested: Constants.POST_CHUNK_SIZE,
post_list: data
});
getProfiles();
},
- function fail(err) {
+ (err) => {
dispatchError(err, 'getPosts');
},
- function complete() {
+ () => {
callTracker['getPosts_' + channelId] = 0;
}
);
}
+export function getPostsBefore(postId, offset, numPost) {
+ const channelId = ChannelStore.getCurrentId();
+ if (channelId == null) {
+ return;
+ }
+
+ if (isCallInProgress('getPostsBefore_' + channelId)) {
+ return;
+ }
+
+ client.getPostsBefore(
+ channelId,
+ postId,
+ offset,
+ numPost,
+ (data, textStatus, xhr) => {
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POSTS,
+ id: channelId,
+ before: true,
+ numRequested: numPost,
+ post_list: data
+ });
+
+ getProfiles();
+ },
+ (err) => {
+ dispatchError(err, 'getPostsBefore');
+ },
+ () => {
+ callTracker['getPostsBefore_' + channelId] = 0;
+ }
+ );
+}
+
+export function getPostsAfter(postId, offset, numPost) {
+ const channelId = ChannelStore.getCurrentId();
+ if (channelId == null) {
+ return;
+ }
+
+ if (isCallInProgress('getPostsAfter_' + channelId)) {
+ return;
+ }
+
+ client.getPostsAfter(
+ channelId,
+ postId,
+ offset,
+ numPost,
+ (data, textStatus, xhr) => {
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POSTS,
+ id: channelId,
+ before: false,
+ numRequested: numPost,
+ post_list: data
+ });
+
+ getProfiles();
+ },
+ (err) => {
+ dispatchError(err, 'getPostsAfter');
+ },
+ () => {
+ callTracker['getPostsAfter_' + channelId] = 0;
+ }
+ );
+}
+
export function getMe() {
if (isCallInProgress('getMe')) {
return;
diff --git a/web/react/utils/channel_intro_mssages.jsx b/web/react/utils/channel_intro_mssages.jsx
index aef1593dc..6f83778c9 100644
--- a/web/react/utils/channel_intro_mssages.jsx
+++ b/web/react/utils/channel_intro_mssages.jsx
@@ -1,13 +1,14 @@
-
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as Utils from './utils.jsx';
-import InviteMemberModal from '../components/invite_member_modal.jsx';
+import EditChannelHeaderModal from '../components/edit_channel_header_modal.jsx';
+import ToggleModalButton from '../components/toggle_modal_button.jsx';
import UserProfile from '../components/user_profile.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import Constants from '../utils/constants.jsx';
import TeamStore from '../stores/team_store.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
export function createChannelIntroMessage(channel, showInviteModal) {
if (channel.type === 'D') {
@@ -49,17 +50,7 @@ export function createDMIntroMessage(channel) {
{'This is the start of your direct message history with ' + teammateName + '.'}<br/>
{'Direct messages and files shared here are not shown to people outside this area.'}
</p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a header'}
- </a>
+ {createSetHeaderButton(channel)}
</div>
);
}
@@ -71,7 +62,7 @@ export function createDMIntroMessage(channel) {
);
}
-export function createOffTopicIntroMessage(channel, showInviteModal) { //eslint-disable-line react/no-multi-comp
+export function createOffTopicIntroMessage(channel, showInviteModal) {
return (
<div className='channel-intro'>
<h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
@@ -79,17 +70,7 @@ export function createOffTopicIntroMessage(channel, showInviteModal) { //eslint-
{'This is the start of ' + channel.display_name + ', a channel for non-work-related conversations.'}
<br/>
</p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a header'}
- </a>
+ {createSetHeaderButton(channel)}
<a
href='#'
className='intro-links'
@@ -101,7 +82,7 @@ export function createOffTopicIntroMessage(channel, showInviteModal) { //eslint-
);
}
-export function createDefaultIntroMessage(channel) { //eslint-disable-line react/no-multi-comp
+export function createDefaultIntroMessage(channel) {
const team = TeamStore.getCurrent();
let inviteModalLink;
if (team.type === Constants.INVITE_TEAM) {
@@ -109,7 +90,7 @@ export function createDefaultIntroMessage(channel) { //eslint-disable-line react
<a
className='intro-links'
href='#'
- onClick={InviteMemberModal.show}
+ onClick={EventHelpers.showInviteMemberModal}
>
<i className='fa fa-user-plus'></i>{'Invite others to this team'}
</a>
@@ -119,10 +100,7 @@ export function createDefaultIntroMessage(channel) { //eslint-disable-line react
<a
className='intro-links'
href='#'
- data-toggle='modal'
- data-target='#get_link'
- data-title='Team Invite'
- data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + team.id}
+ onClick={EventHelpers.showGetTeamInviteLinkModal}
>
<i className='fa fa-user-plus'></i>{'Invite others to this team'}
</a>
@@ -138,23 +116,13 @@ export function createDefaultIntroMessage(channel) { //eslint-disable-line react
{'This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'}
</p>
{inviteModalLink}
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a header'}
- </a>
+ {createSetHeaderButton(channel)}
<br/>
</div>
);
}
-export function createStandardIntroMessage(channel, showInviteModal) { //eslint-disable-line react/no-multi-comp
+export function createStandardIntroMessage(channel, showInviteModal) {
var uiName = channel.display_name;
var creatorName = '';
@@ -193,17 +161,7 @@ export function createStandardIntroMessage(channel, showInviteModal) { //eslint-
{memberMessage}
<br/>
</p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a header'}
- </a>
+ {createSetHeaderButton(channel)}
<a
className='intro-links'
href='#'
@@ -214,3 +172,15 @@ export function createStandardIntroMessage(channel, showInviteModal) { //eslint-
</div>
);
}
+
+function createSetHeaderButton(channel) {
+ return (
+ <ToggleModalButton
+ className='intro-links'
+ dialogType={EditChannelHeaderModal}
+ dialogProps={{channel}}
+ >
+ <i className='fa fa-pencil'></i>{'Set a header'}
+ </ToggleModalButton>
+ );
+}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index e6c24aa9c..09e962161 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -820,7 +820,37 @@ export function getPosts(channelId, since, success, error, complete) {
});
}
-export function getPost(channelId, postId, success, error) {
+export function getPostsBefore(channelId, post, offset, numPost, success, error, complete) {
+ $.ajax({
+ url: '/api/v1/channels/' + channelId + '/post/' + post + '/before/' + offset + '/' + numPost,
+ dataType: 'json',
+ type: 'GET',
+ ifModified: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getPostsBefore', xhr, status, err);
+ error(e);
+ },
+ complete: complete
+ });
+}
+
+export function getPostsAfter(channelId, post, offset, numPost, success, error, complete) {
+ $.ajax({
+ url: '/api/v1/channels/' + channelId + '/post/' + post + '/after/' + offset + '/' + numPost,
+ dataType: 'json',
+ type: 'GET',
+ ifModified: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getPostsAfter', xhr, status, err);
+ error(e);
+ },
+ complete: complete
+ });
+}
+
+export function getPost(channelId, postId, success, error, complete) {
$.ajax({
cache: false,
url: '/api/v1/channels/' + channelId + '/post/' + postId,
@@ -831,7 +861,24 @@ export function getPost(channelId, postId, success, error) {
error: function onError(xhr, status, err) {
var e = handleError('getPost', xhr, status, err);
error(e);
- }
+ },
+ complete
+ });
+}
+
+export function getPostById(postId, success, error, complete) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/posts/' + postId,
+ dataType: 'json',
+ type: 'GET',
+ ifModified: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getPostById', xhr, status, err);
+ error(e);
+ },
+ complete
});
}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 958bfa8d2..6281813e9 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -10,12 +10,17 @@ export default {
CLICK_CHANNEL: null,
CREATE_CHANNEL: null,
LEAVE_CHANNEL: null,
+ CREATE_POST: null,
+ POST_DELETED: null,
+
RECIEVED_CHANNELS: null,
RECIEVED_CHANNEL: null,
RECIEVED_MORE_CHANNELS: null,
RECIEVED_CHANNEL_EXTRA_INFO: null,
+ FOCUS_POST: null,
RECIEVED_POSTS: null,
+ RECIEVED_FOCUSED_POST: null,
RECIEVED_POST: null,
RECIEVED_EDIT_POST: null,
RECIEVED_SEARCH: null,
@@ -43,7 +48,8 @@ export default {
TOGGLE_IMPORT_THEME_MODAL: null,
TOGGLE_INVITE_MEMBER_MODAL: null,
- TOGGLE_DELETE_POST_MODAL: null
+ TOGGLE_DELETE_POST_MODAL: null,
+ TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null
}),
PayloadSources: keyMirror({
@@ -99,6 +105,7 @@ export default {
EMAIL_SERVICE: 'email',
POST_CHUNK_SIZE: 60,
MAX_POST_CHUNKS: 3,
+ POST_FOCUS_CONTEXT_RADIUS: 10,
POST_LOADING: 'loading',
POST_FAILED: 'failed',
POST_DELETED: 'deleted',
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index 7957ea31b..b0ec64bfd 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -110,32 +110,47 @@ class MattermostMarkdownRenderer extends marked.Renderer {
this.formattingOptions = formattingOptions;
}
- code(code, language) {
- let usedLanguage = language;
+ code(code, language, escaped) {
+ let usedLanguage = language || '';
+ usedLanguage = usedLanguage.toLowerCase();
- if (String(usedLanguage).toLocaleLowerCase() === 'html') {
+ // treat html as xml to prevent injection attacks
+ if (usedLanguage === 'html') {
usedLanguage = 'xml';
}
- if (usedLanguage && (usedLanguage === 'tex' || usedLanguage === 'latex')) {
+ if (HighlightedLanguages[usedLanguage]) {
+ const parsed = highlightJs.highlight(usedLanguage, code);
+
+ return (
+ '<div class="post-body--code">' +
+ '<span class="post-body--code__language">' +
+ HighlightedLanguages[usedLanguage] +
+ '</span>' +
+ '<pre>' +
+ '<code class="hljs">' +
+ parsed.value +
+ '</code>' +
+ '</pre>' +
+ '</div>'
+ );
+ } else if (usedLanguage === 'tex' || usedLanguage === 'latex') {
try {
- var html = katex.renderToString(TextFormatting.sanitizeHtml(code), {throwOnError: false, displayMode: true});
+ const html = katex.renderToString(TextFormatting.sanitizeHtml(code), {throwOnError: false, displayMode: true});
+
return '<div class="post-body--code tex">' + html + '</div>';
} catch (e) {
- return '<div class="post-body--code">' + TextFormatting.sanitizeHtml(code) + '</div>';
+ // fall through if latex parsing fails and handle below
}
}
- if (!usedLanguage || highlightJs.listLanguages().indexOf(usedLanguage) < 0) {
- let parsed = super.code(code, usedLanguage);
- return '<div class="post-body--code"><code class="hljs">' + TextFormatting.sanitizeHtml($(parsed).text()) + '</code></div>';
- }
-
- let parsed = highlightJs.highlight(usedLanguage, code);
- return '<div class="post-body--code">' +
- '<span class="post-body--code__language">' + HighlightedLanguages[usedLanguage] + '</span>' +
- '<code class="hljs">' + parsed.value + '</code>' +
- '</div>';
+ return (
+ '<pre>' +
+ '<code class="hljs">' +
+ (escaped ? code : TextFormatting.sanitizeHtml(code)) + '\n' +
+ '</code>' +
+ '</pre>'
+ );
}
br() {
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 668d8100f..9b2f7e057 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
@@ -748,19 +749,10 @@ export function updateCodeTheme(theme) {
export function placeCaretAtEnd(el) {
el.focus();
- if (typeof window.getSelection != 'undefined' && typeof document.createRange != 'undefined') {
- var range = document.createRange();
- range.selectNodeContents(el);
- range.collapse(false);
- var sel = window.getSelection();
- sel.removeAllRanges();
- sel.addRange(range);
- } else if (typeof document.body.createTextRange != 'undefined') {
- var textRange = document.body.createTextRange();
- textRange.moveToElementText(el);
- textRange.collapse(false);
- textRange.select();
- }
+ el.selectionStart = el.value.length;
+ el.selectionEnd = el.value.length;
+
+ return;
}
export function getCaretPosition(el) {
@@ -839,23 +831,15 @@ export function isValidUsername(name) {
}
export function updateAddressBar(channelName) {
- var teamURL = window.location.href.split('/channels')[0];
+ const teamURL = TeamStore.getCurrentTeamUrl();
history.replaceState('data', '', teamURL + '/channels/' + channelName);
}
export function switchChannel(channel) {
- AppDispatcher.handleViewAction({
- type: ActionTypes.CLICK_CHANNEL,
- name: channel.name,
- id: channel.id
- });
+ EventHelpers.emitChannelClickEvent(channel);
updateAddressBar(channel.name);
- AsyncClient.getChannels(true, true, true);
- AsyncClient.getChannelExtraInfo(true);
- AsyncClient.getPosts(channel.id);
-
$('.inner__wrap').removeClass('move--right');
$('.sidebar--left').removeClass('move--right');
diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss
index ea5ccd2d2..6d1ecbfa0 100644
--- a/web/sass-files/sass/partials/_markdown.scss
+++ b/web/sass-files/sass/partials/_markdown.scss
@@ -9,11 +9,26 @@
}
}
#post-list {
- .markdown-inline-img {
- -moz-force-broken-image-icon: 1;
- max-height: 500px;
- height: 500px;
- }
+ .markdown-inline-img {
+ -moz-force-broken-image-icon: 1;
+ max-height: 500px;
+ height: 500px;
+ }
+}
+
+.post-body--code {
+ position: relative;
+}
+
+.post-body--code__language {
+ position: absolute;
+ top: 0;
+ right: 0;
+ color: #fff;
+ background: #21586D;
+ padding: 4px 10px 5px 10px;
+ font-size: 13px;
+ opacity: 0.7;
}
.post__body {
@@ -25,6 +40,9 @@
border: 0 none;
@include opacity(0.2);
}
+ code {
+ white-space: pre;
+ }
}
.markdown__table {
background: #fff;
diff --git a/web/sass-files/sass/partials/_navbar.scss b/web/sass-files/sass/partials/_navbar.scss
index c570c4663..aad46d3d9 100644
--- a/web/sass-files/sass/partials/_navbar.scss
+++ b/web/sass-files/sass/partials/_navbar.scss
@@ -33,6 +33,9 @@
background: #fff;
width: 21px;
}
+ .glyphicon-search {
+ top: -1px;
+ }
.icon--white {
color: #fff;
}
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 99e985b0c..b7609bb7d 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -361,8 +361,15 @@ body.ios {
.dropdown, .comment-icon__container {
visibility: visible;
}
+ .permalink-icon {
+ visibility: visible;
+ }
}
+ &.post--highlight {
+ background-color: beige;
+ }
+
ul {
margin: 0;
padding: 0;
@@ -520,6 +527,8 @@ body.ios {
}
.post__header {
+ padding: 0;
+ list-style: none;
margin-bottom: 2px;
li {
@@ -588,6 +597,11 @@ body.ios {
padding: 0.2em 0.5em 0em;
@include legacy-pie-clearfix;
width: calc(100% - 70px);
+
+ ul {
+ padding: 5px 0 0 20px;
+ }
+
}
.post__link {
@@ -617,6 +631,12 @@ body.ios {
color: #999;
}
+ .permalink-icon {
+ display: inline-block;
+ color: $primary-color;
+ visibility: hidden;
+ }
+
.comment-icon__container {
fill: $primary-color;
display: inline-block;
@@ -755,4 +775,12 @@ body.ios {
margin: 0;
}
}
-} \ No newline at end of file
+}
+
+.permalink-text {
+ overflow: hidden;
+}
+
+.permalink-popover {
+ min-width: 320px;
+}
diff --git a/web/templates/channel.html b/web/templates/channel.html
index c15cea178..7b8f6a243 100644
--- a/web/templates/channel.html
+++ b/web/templates/channel.html
@@ -10,30 +10,21 @@
<div id="post_mention_tab"></div>
<div id="reply_mention_tab"></div>
<div id="edit_mention_tab"></div>
- <div id="get_link_modal"></div>
- <div id="user_settings_modal"></div>
+ <div id="get_team_invite_link_modal"></div>
<div id="import_theme_modal"></div>
<div id="team_settings_modal"></div>
<div id="invite_member_modal"></div>
- <div id="edit_channel_modal"></div>
- <div id="delete_channel_modal"></div>
<div id="rename_channel_modal"></div>
- <div id="rename_team_modal"></div>
<div id="edit_post_modal"></div>
<div id="delete_post_modal"></div>
<div id="more_channels_modal"></div>
- <div id="new_channel_modal"></div>
<div id="post_deleted_modal"></div>
<div id="channel_notifications_modal"></div>
<div id="team_members_modal"></div>
- <div id="direct_channel_modal"></div>
- <div id="channel_info_modal"></div>
- <div id="access_history_modal"></div>
- <div id="activity_log_modal"></div>
<div id="removed_from_channel_modal"></div>
<div id="register_app_modal"></div>
<script>
- window.setup_channel_page({{ .Props }});
+window.setup_channel_page({{ .Props }}, {{ .Team }}, {{ .Channel }}, {{ .User }});
$('body').tooltip( {selector: '[data-toggle=tooltip]'} );
if($(window).height() > 1200){
$('.modal-body').css('max-height', 1000);
diff --git a/web/templates/head.html b/web/templates/head.html
index 30a83c4f2..ec06ba9bc 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -47,6 +47,7 @@
window.mm_config = {{ .ClientCfg }};
window.mm_team = {{ .Team }};
window.mm_user = {{ .User }};
+ window.mm_channel = {{ .Channel }};
if ({{.SessionTokenIndex}} >= 0) {
window.mm_session_token_index = {{.SessionTokenIndex}};
diff --git a/web/web.go b/web/web.go
index 477bd8b27..a72bff2bf 100644
--- a/web/web.go
+++ b/web/web.go
@@ -91,6 +91,7 @@ func InitWeb() {
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET")
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET")
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET")
+ mainrouter.Handle("/{team}/pl/{postid}", api.AppHandler(postPermalink)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
@@ -342,15 +343,142 @@ func logout(c *api.Context, w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, c.GetTeamURL(), http.StatusTemporaryRedirect)
}
+func postPermalink(c *api.Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ teamName := params["team"]
+ postId := params["postid"]
+
+ if len(postId) != 26 {
+ c.Err = model.NewAppError("postPermalink", "Invalid Post ID", "id="+postId)
+ return
+ }
+
+ team := checkSessionSwitch(c, w, r, teamName)
+ if team == nil {
+ // Error already set by getTeam
+ return
+ }
+
+ var post *model.Post
+ if result := <-api.Srv.Store.Post().Get(postId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ postlist := result.Data.(*model.PostList)
+ post = postlist.Posts[postlist.Order[0]]
+ }
+
+ var channel *model.Channel
+ if result := <-api.Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ if result.Data.(int64) == 0 {
+ if channel = autoJoinChannelId(c, w, r, post.ChannelId); channel == nil {
+ http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
+ return
+ }
+ } else {
+ if result := <-api.Srv.Store.Channel().Get(post.ChannelId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ channel = result.Data.(*model.Channel)
+ }
+ }
+ }
+
+ doLoadChannel(c, w, r, team, channel, post.Id)
+}
+
func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
name := params["channelname"]
teamName := params["team"]
+ team := checkSessionSwitch(c, w, r, teamName)
+ if team == nil {
+ // Error already set by getTeam
+ return
+ }
+
+ var channel *model.Channel
+ if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ channelId := result.Data.(string)
+ if len(channelId) == 0 {
+ if channel = autoJoinChannelName(c, w, r, name); channel == nil {
+ http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
+ return
+ }
+ } else {
+ if result := <-api.Srv.Store.Channel().Get(channelId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ channel = result.Data.(*model.Channel)
+ }
+ }
+ }
+
+ doLoadChannel(c, w, r, team, channel, "")
+}
+
+func autoJoinChannelName(c *api.Context, w http.ResponseWriter, r *http.Request, channelName string) *model.Channel {
+ if strings.Index(channelName, "__") > 0 {
+ // It's a direct message channel that doesn't exist yet so let's create it
+ ids := strings.Split(channelName, "__")
+ otherUserId := ""
+ if ids[0] == c.Session.UserId {
+ otherUserId = ids[1]
+ } else {
+ otherUserId = ids[0]
+ }
+
+ if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil {
+ api.Handle404(w, r)
+ return nil
+ } else {
+ return sc
+ }
+ } else {
+ // We will attempt to auto-join open channels
+ return joinOpenChannel(c, w, r, api.Srv.Store.Channel().GetByName(c.Session.TeamId, channelName))
+ }
+
+ return nil
+}
+
+func autoJoinChannelId(c *api.Context, w http.ResponseWriter, r *http.Request, channelId string) *model.Channel {
+ return joinOpenChannel(c, w, r, api.Srv.Store.Channel().Get(channelId))
+}
+
+func joinOpenChannel(c *api.Context, w http.ResponseWriter, r *http.Request, channel store.StoreChannel) *model.Channel {
+ if cr := <-channel; cr.Err != nil {
+ http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
+ return nil
+ } else {
+ channel := cr.Data.(*model.Channel)
+ if channel.Type == model.CHANNEL_OPEN {
+ api.JoinChannel(c, channel.Id, "")
+ if c.Err != nil {
+ return nil
+ }
+ } else {
+ http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
+ return nil
+ }
+ return channel
+ }
+}
+
+func checkSessionSwitch(c *api.Context, w http.ResponseWriter, r *http.Request, teamName string) *model.Team {
var team *model.Team
if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
- return
+ return nil
} else {
team = result.Data.(*model.Team)
}
@@ -368,15 +496,11 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
}
}
- userChan := api.Srv.Store.User().Get(c.Session.UserId)
+ return team
+}
- var channelId string
- if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- channelId = result.Data.(string)
- }
+func doLoadChannel(c *api.Context, w http.ResponseWriter, r *http.Request, team *model.Team, channel *model.Channel, postid string) {
+ userChan := api.Srv.Store.User().Get(c.Session.UserId)
var user *model.User
if ur := <-userChan; ur.Err != nil {
@@ -388,54 +512,15 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
user = ur.Data.(*model.User)
}
- if len(channelId) == 0 {
- if strings.Index(name, "__") > 0 {
- // It's a direct message channel that doesn't exist yet so let's create it
- ids := strings.Split(name, "__")
- otherUserId := ""
- if ids[0] == c.Session.UserId {
- otherUserId = ids[1]
- } else {
- otherUserId = ids[0]
- }
-
- if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil {
- api.Handle404(w, r)
- return
- } else {
- channelId = sc.Id
- }
- } else {
- // We will attempt to auto-join open channels
- if cr := <-api.Srv.Store.Channel().GetByName(c.Session.TeamId, name); cr.Err != nil {
- http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
- } else {
- channel := cr.Data.(*model.Channel)
- if channel.Type == model.CHANNEL_OPEN {
- api.JoinChannel(c, channel.Id, "")
- if c.Err != nil {
- return
- }
-
- channelId = channel.Id
- } else {
- http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
- }
- }
- }
- }
-
page := NewHtmlTemplatePage("channel", "")
- page.Props["Title"] = name + " - " + team.DisplayName + " " + page.ClientCfg["SiteName"]
+ page.Props["Title"] = channel.DisplayName + " - " + team.DisplayName + " " + page.ClientCfg["SiteName"]
page.Props["TeamDisplayName"] = team.DisplayName
- page.Props["TeamName"] = team.Name
- page.Props["TeamType"] = team.Type
- page.Props["TeamId"] = team.Id
- page.Props["ChannelName"] = name
- page.Props["ChannelId"] = channelId
- page.Props["UserId"] = c.Session.UserId
+ page.Props["ChannelName"] = channel.Name
+ page.Props["ChannelId"] = channel.Id
+ page.Props["PostId"] = postid
page.Team = team
page.User = user
+ page.Channel = channel
page.Render(c, w)
}