diff options
49 files changed, 2022 insertions, 897 deletions
diff --git a/.travis.yml b/.travis.yml index 359de244e..b8a503714 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,10 +30,25 @@ addons: hosts: - 127.0.0.1 dockerhost deploy: - provider: releases - api_key: - secure: ma8Y0oimU+LB6LTAh8to2E1/ghaDPhcsAFXBrODsHpd4JgxA6HYoEwSEBCJFHSpu/JteclsxSTfp9hcuzw/IOtlwlSAiVoBZ60s24MRKTIAQNtrJ4QrX5wyfAZi+Bcuk/E8NynmoIW5qpaElSAdjgocyjAJIQ5ChMEztglL0cAEBXQRWbWMqSZ0hVLPrKDCIkWIyv3pFxqdLOxktkzxW07r2dlT0hppXR3dCaPJo0nelArS2H3LdN/3Iv6cAddfS27RaZkqDj/PDh6OZr4EguC99TxlVNChIr7nPr3/OiAssbkvEnhlSLeABFO9+7KfutL2WhAjpFXTjtPVq6Qalc8UW0K0gxq//sVfhb1MzjenmdOf06uB2bilQ8kgwHo7dDdRZBqqAtxQ6Q0Ht3SFMj6v/1zVD3s+YX/kWCEbUTHm6r2G/eF794ozcJyU+6j1L8hm6mvf8Mr9XCqBfgpZy6FCLX+9OKdMvX2jY8reo3Xz1PA9R6yzhN08vjku+jW+fsoYrBLd0fY1UGK2uOuvBByCeJzXupd3YpBMjEyRupVxqEj7K0GWOJeml65mkqKSNsHdDSeSjMpb8mwneZyTbdjsxCFQRLcLgpAajFrkk4G2Yz3KfhXSo29XKEGX+EbY5NuP8KmDsBsguPI0zfwv/co0hAY8PIIcehxcdoR9Vb2c= - file: dist/mattermost.tar.gz - skip_cleanup: true - on: - tags: true + - provider: releases + api_key: + secure: ma8Y0oimU+LB6LTAh8to2E1/ghaDPhcsAFXBrODsHpd4JgxA6HYoEwSEBCJFHSpu/JteclsxSTfp9hcuzw/IOtlwlSAiVoBZ60s24MRKTIAQNtrJ4QrX5wyfAZi+Bcuk/E8NynmoIW5qpaElSAdjgocyjAJIQ5ChMEztglL0cAEBXQRWbWMqSZ0hVLPrKDCIkWIyv3pFxqdLOxktkzxW07r2dlT0hppXR3dCaPJo0nelArS2H3LdN/3Iv6cAddfS27RaZkqDj/PDh6OZr4EguC99TxlVNChIr7nPr3/OiAssbkvEnhlSLeABFO9+7KfutL2WhAjpFXTjtPVq6Qalc8UW0K0gxq//sVfhb1MzjenmdOf06uB2bilQ8kgwHo7dDdRZBqqAtxQ6Q0Ht3SFMj6v/1zVD3s+YX/kWCEbUTHm6r2G/eF794ozcJyU+6j1L8hm6mvf8Mr9XCqBfgpZy6FCLX+9OKdMvX2jY8reo3Xz1PA9R6yzhN08vjku+jW+fsoYrBLd0fY1UGK2uOuvBByCeJzXupd3YpBMjEyRupVxqEj7K0GWOJeml65mkqKSNsHdDSeSjMpb8mwneZyTbdjsxCFQRLcLgpAajFrkk4G2Yz3KfhXSo29XKEGX+EbY5NuP8KmDsBsguPI0zfwv/co0hAY8PIIcehxcdoR9Vb2c= + file: dist/mattermost.tar.gz + skip_cleanup: true + on: + repo: mattermost/platform + tags: true + + - provider: s3 + access_key_id: AKIAJCO3KJYEGWJIKDIQ + secret_access_key: + secure: p66X2tJBmKgtcVyPtGgkAwW29IiRojqGA39RjCJkIWNTJ0e/9JvBOiMS2c4a7I4aOads38rsthwdaigBWagDWNH7bGsEZN7B0TszZuFAuU+XGjU5A66MIOfFfzbUg8AnByysr+XG5/bknFIrP/XhM2fbRr6gbYrFUK7TNkpgjFs5u3BzUrz2iTAV8uOpSJqKSnaf0pTZk1EywOK/X8W8ViIjc7Di3FzQcqIW9K3D27N+3rVsv8SRT1hWASVlnG6aThqqebiM8FCGCzAYVgQb3h3Wu8JT5fIz7Qo7A6siVRwNBwWwzP8HkGoinEK32Wsj/fDXk27vjpFQO/+9sV0xfcTbIZA6MnuYWF4rHOT59KcshCWCD3V0FopX57p/dtOzM9+6lxIctAT++izxWoZit/5c5A4633iY1d+RMeTko1POix6MSlxPMRHZUFwSXROgFuWWRpyD6TlUTCST9/wTTd0WDPklAAiYcnuEPW3qCnw0r0xkrA4AwWUXqXdAIwDt5bA27KcjRyY4Fofv9NxH09BNuBTXNPrvnYPZMmaKrv+HOX3NFTreuV6+5LJdhYUxYSBvSWo1jeWIQ5Q9RUdTU0PqmKpMhJKbKey/S4gxCXHg2HR8DwLCcbIZcvneF9yPEAT71YA6zpLKoPVSwWwH97huKSzjpic/RUfFXQOcgCQ= + bucket: mattermost-travis-master + local_dir: dist + acl: public_read + region: us-east-1 + skip_cleanup: true + detect_encoding: true + on: + repo: mattermost/platform + branch: master @@ -245,10 +245,8 @@ dist: install tar -C dist -czf $(DIST_PATH).tar.gz mattermost -docker-build: stop dist - cp $(DIST_PATH).tar.gz docker/dev - cd docker/dev && docker build -t ${DOCKERNAME} . - rm docker/dev/mattermost.tar.gz +docker-build: stop + docker build -t ${DOCKERNAME} -f docker/local/Dockerfile . docker-run: docker-build docker run --name ${DOCKER_CONTAINER_NAME} -d --publish 8065:80 ${DOCKERNAME} @@ -24,7 +24,7 @@ You're installing "Mattermost Alpha", a pre-released version providing an early That said, any issues at all, please let us know on the Mattermost forum at: http://forum.mattermost.org Notes: -- For Alpha, Docker is intentionally setup as a single container, since production deployment not yet recommended. +- For Alpha, Docker is intentionally setup as a single container, since production deployment is not yet recommended. Local Machine Setup (Docker) ----------------------------- diff --git a/api/api.go b/api/api.go index 2ea27ed9f..9770930f7 100644 --- a/api/api.go +++ b/api/api.go @@ -40,6 +40,7 @@ func InitApi() { InitWebSocket(r) InitFile(r) InitCommand(r) + InitConfig(r) templatesDir := utils.FindDir("api/templates") l4g.Debug("Parsing server templates at %v", templatesDir) diff --git a/api/channel.go b/api/channel.go index 803274d32..151627623 100644 --- a/api/channel.go +++ b/api/channel.go @@ -18,11 +18,13 @@ func InitChannel(r *mux.Router) { sr := r.PathPrefix("/channels").Subrouter() sr.Handle("/", ApiUserRequiredActivity(getChannels, false)).Methods("GET") sr.Handle("/more", ApiUserRequired(getMoreChannels)).Methods("GET") + sr.Handle("/counts", ApiUserRequiredActivity(getChannelCounts, false)).Methods("GET") sr.Handle("/create", ApiUserRequired(createChannel)).Methods("POST") sr.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST") sr.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST") sr.Handle("/update_desc", ApiUserRequired(updateChannelDesc)).Methods("POST") sr.Handle("/update_notify_level", ApiUserRequired(updateNotifyLevel)).Methods("POST") + sr.Handle("/{id:[A-Za-z0-9]+}/", ApiUserRequiredActivity(getChannel, false)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}/join", ApiUserRequired(joinChannel)).Methods("POST") sr.Handle("/{id:[A-Za-z0-9]+}/leave", ApiUserRequired(leaveChannel)).Methods("POST") @@ -275,7 +277,7 @@ func updateChannelDesc(c *Context, w http.ResponseWriter, r *http.Request) { func getChannels(c *Context, w http.ResponseWriter, r *http.Request) { - // user is already in the newtork + // user is already in the team if result := <-Srv.Store.Channel().GetChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { if result.Err.Message == "No channels were found" { @@ -300,7 +302,7 @@ func getChannels(c *Context, w http.ResponseWriter, r *http.Request) { func getMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) { - // user is already in the newtork + // user is already in the team if result := <-Srv.Store.Channel().GetMoreChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { c.Err = result.Err @@ -314,6 +316,22 @@ func getMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getChannelCounts(c *Context, w http.ResponseWriter, r *http.Request) { + + // user is already in the team + + if result := <-Srv.Store.Channel().GetChannelCounts(c.Session.TeamId, c.Session.UserId); result.Err != nil { + c.Err = model.NewAppError("getChannelCounts", "Unable to get channel counts from the database", result.Err.Message) + return + } else if HandleEtag(result.Data.(*model.ChannelCounts).Etag(), w, r) { + return + } else { + data := result.Data.(*model.ChannelCounts) + w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag()) + w.Write([]byte(data.ToJson())) + } +} + func joinChannel(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) @@ -548,6 +566,37 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(result))) } +func getChannel(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id := params["id"] + + //pchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId) + cchan := Srv.Store.Channel().Get(id) + cmchan := Srv.Store.Channel().GetMember(id, c.Session.UserId) + + if cresult := <-cchan; cresult.Err != nil { + c.Err = cresult.Err + return + } else if cmresult := <-cmchan; cmresult.Err != nil { + c.Err = cmresult.Err + return + } else { + data := &model.ChannelData{} + data.Channel = cresult.Data.(*model.Channel) + member := cmresult.Data.(model.ChannelMember) + data.Member = &member + + if HandleEtag(data.Etag(), w, r) { + return + } else { + w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag()) + w.Header().Set("Expires", "-1") + w.Write([]byte(data.ToJson())) + } + } + +} + func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) diff --git a/api/channel_test.go b/api/channel_test.go index d4fb11bd8..d65aff66c 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -320,6 +320,27 @@ func TestGetChannel(t *testing.T) { if _, err := Client.UpdateLastViewedAt(channel2.Id); err != nil { t.Fatal(err) } + + if resp, err := Client.GetChannel(channel1.Id, ""); err != nil { + t.Fatal(err) + } else { + data := resp.Data.(*model.ChannelData) + if data.Channel.DisplayName != channel1.DisplayName { + t.Fatal("name didn't match") + } + + // test etag caching + if cache_result, err := Client.GetChannel(channel1.Id, resp.Etag); err != nil { + t.Fatal(err) + } else if cache_result.Data.(*model.ChannelData) != nil { + t.Log(cache_result.Data) + t.Fatal("cache should be empty") + } + } + + if _, err := Client.GetChannel("junk", ""); err == nil { + t.Fatal("should have failed - bad channel id") + } } func TestGetMoreChannel(t *testing.T) { @@ -366,6 +387,47 @@ func TestGetMoreChannel(t *testing.T) { } } +func TestGetChannelCounts(t *testing.T) { + Setup() + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user.Id)) + + Client.LoginByEmail(team.Name, user.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + channel2 := &model.Channel{DisplayName: "B Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + if result, err := Client.GetChannelCounts(""); err != nil { + t.Fatal(err) + } else { + counts := result.Data.(*model.ChannelCounts) + + if len(counts.Counts) != 4 { + t.Fatal("wrong number of channel counts") + } + + if len(counts.UpdateTimes) != 4 { + t.Fatal("wrong number of channel update times") + } + + if cache_result, err := Client.GetChannelCounts(result.Etag); err != nil { + t.Fatal(err) + } else if cache_result.Data.(*model.ChannelCounts) != nil { + t.Log(cache_result.Data) + t.Fatal("result data should be empty") + } + } + +} + func TestJoinChannel(t *testing.T) { Setup() diff --git a/api/config.go b/api/config.go new file mode 100644 index 000000000..142d1ca66 --- /dev/null +++ b/api/config.go @@ -0,0 +1,34 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "encoding/json" + "github.com/gorilla/mux" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "net/http" + "strconv" +) + +func InitConfig(r *mux.Router) { + l4g.Debug("Initializing config api routes") + + sr := r.PathPrefix("/config").Subrouter() + sr.Handle("/get_all", ApiAppHandler(getConfig)).Methods("GET") +} + +func getConfig(c *Context, w http.ResponseWriter, r *http.Request) { + settings := make(map[string]string) + + settings["ByPassEmail"] = strconv.FormatBool(utils.Cfg.EmailSettings.ByPassEmail) + + if bytes, err := json.Marshal(settings); err != nil { + c.Err = model.NewAppError("getConfig", "Unable to marshall configuration data", err.Error()) + return + } else { + w.Write(bytes) + } +} diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index a684ac10d..cdfdc1845 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -13,8 +13,6 @@ ENV MYSQL_DATABASE=mattermost_test RUN groupadd -r mysql && useradd -r -g mysql mysql -RUN apt-get update && apt-get install -y perl --no-install-recommends && rm -rf /var/lib/apt/lists/* - RUN apt-key adv --keyserver pool.sks-keyservers.net --recv-keys A4A9406876FCBD3C456770C88C718D3B5072E1F5 ENV MYSQL_MAJOR 5.6 @@ -24,7 +22,7 @@ RUN echo "deb http://repo.mysql.com/apt/debian/ wheezy mysql-${MYSQL_MAJOR}" > / RUN apt-get update \ && export DEBIAN_FRONTEND=noninteractive \ - && apt-get -y install mysql-server \ + && apt-get -y install perl wget mysql-server \ && rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/mysql && mkdir -p /var/lib/mysql @@ -36,7 +34,8 @@ VOLUME /var/lib/mysql WORKDIR /mattermost # Copy over files -ADD mattermost.tar.gz / +RUN wget --no-check-certificate https://s3.amazonaws.com/mattermost-travis-master/mattermost.tar.gz +RUN tar -zxvf mattermost.tar.gz --strip-components=1 && rm mattermost.tar.gz ADD config_docker.json / ADD docker-entry.sh / diff --git a/docker/dev/Dockerrun.aws.json b/docker/dev/Dockerrun.aws.json index 9f69f1fb1..ae847ef80 100755 --- a/docker/dev/Dockerrun.aws.json +++ b/docker/dev/Dockerrun.aws.json @@ -1,7 +1,7 @@ {
"AWSEBDockerrunVersion": "1",
"Image": {
- "Name": "YOUR_DEV_INSTANCE_HERE",
+ "Name": "mattermost/platform:dev",
"Update": "true"
},
"Ports": [
diff --git a/docker/local/Dockerfile b/docker/local/Dockerfile new file mode 100644 index 000000000..55725e293 --- /dev/null +++ b/docker/local/Dockerfile @@ -0,0 +1,84 @@ +# Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +# See License.txt for license information. +FROM ubuntu:14.04 + +# Install Dependancies +RUN apt-get update && apt-get install -y build-essential +RUN apt-get install -y curl +RUN curl -sL https://deb.nodesource.com/setup | bash - +RUN apt-get install -y nodejs +RUN apt-get install -y ruby-full +RUN gem install compass + +# +# Install GO +# + +RUN apt-get update && apt-get install -y \ + gcc libc6-dev make git mercurial \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +ENV GOLANG_VERSION 1.4.2 + +RUN curl -sSL https://golang.org/dl/go$GOLANG_VERSION.src.tar.gz \ + | tar -v -C /usr/src -xz + +RUN cd /usr/src/go/src && ./make.bash --no-clean 2>&1 + +ENV PATH /usr/src/go/bin:$PATH + +RUN mkdir -p /go/src /go/bin && chmod -R 777 /go +ENV GOPATH /go +ENV PATH /go/bin:$PATH +WORKDIR /go + +# --------------------------------------------------------------------------------------------------------------------- + +# +# Install SQL +# + +ENV MYSQL_ROOT_PASSWORD=mostest +ENV MYSQL_USER=mmuser +ENV MYSQL_PASSWORD=mostest +ENV MYSQL_DATABASE=mattermost_test + +RUN groupadd -r mysql && useradd -r -g mysql mysql + +RUN apt-key adv --keyserver pool.sks-keyservers.net --recv-keys A4A9406876FCBD3C456770C88C718D3B5072E1F5 + +ENV MYSQL_MAJOR 5.6 +ENV MYSQL_VERSION 5.6.25 + +RUN echo "deb http://repo.mysql.com/apt/debian/ wheezy mysql-${MYSQL_MAJOR}" > /etc/apt/sources.list.d/mysql.list + +RUN apt-get update \ + && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install perl mysql-server \ + && rm -rf /var/lib/apt/lists/* \ + && rm -rf /var/lib/mysql && mkdir -p /var/lib/mysql + +RUN sed -Ei 's/^(bind-address|log)/#&/' /etc/mysql/my.cnf + +VOLUME /var/lib/mysql +# --------------------------------------------------------------------------------------------------------------------- + +# Copy over files +ADD . /go/src/github.com/mattermost/platform + +RUN go get github.com/tools/godep +RUN cd /go/src/github.com/mattermost/platform/; godep go install +RUN cd /go/src/github.com/mattermost/platform/web/react; npm install + +ADD /docker/local/config_docker.json / +ADD /docker/local/docker-entry.sh / + +RUN chmod +x /docker-entry.sh +ENTRYPOINT /docker-entry.sh + +# Create default storage directory +RUN mkdir /mattermost-data/ + +# Ports +EXPOSE 80 diff --git a/docker/local/Dockerrun.aws.json b/docker/local/Dockerrun.aws.json new file mode 100755 index 000000000..9f69f1fb1 --- /dev/null +++ b/docker/local/Dockerrun.aws.json @@ -0,0 +1,13 @@ +{
+ "AWSEBDockerrunVersion": "1",
+ "Image": {
+ "Name": "YOUR_DEV_INSTANCE_HERE",
+ "Update": "true"
+ },
+ "Ports": [
+ {
+ "ContainerPort": "80"
+ }
+ ],
+ "Logging": "/var/log/"
+}
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json new file mode 100644 index 000000000..cd612c7fe --- /dev/null +++ b/docker/local/config_docker.json @@ -0,0 +1,98 @@ +{ + "LogSettings": { + "ConsoleEnable": true, + "ConsoleLevel": "INFO", + "FileEnable": true, + "FileLevel": "INFO", + "FileFormat": "", + "FileLocation": "" + }, + "ServiceSettings": { + "SiteName": "Mattermost", + "Mode" : "dev", + "AllowTesting" : true, + "UseSSL": false, + "Port": "80", + "Version": "developer", + "Shards": { + }, + "InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6", + "PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4", + "ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t", + "AnalyticsUrl": "", + "UseLocalStorage": true, + "StorageDirectory": "/mattermost/data/", + "AllowedLoginAttempts": 10 + }, + "SSOSettings": { + "gitlab": { + "Allow": false, + "Secret" : "", + "Id": "", + "AuthEndpoint": "", + "TokenEndpoint": "", + "UserApiEndpoint": "" + } + }, + "SqlSettings": { + "DriverName": "mysql", + "DataSource": "mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8", + "DataSourceReplicas": ["mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8"], + "MaxIdleConns": 10, + "MaxOpenConns": 10, + "Trace": false, + "AtRestEncryptKey": "Ya0xMrybACJ3sZZVWQC7e31h5nSDWZFS" + }, + "AWSSettings": { + "S3AccessKeyId": "", + "S3SecretAccessKey": "", + "S3Bucket": "", + "S3Region": "" + }, + "ImageSettings": { + "ThumbnailWidth": 120, + "ThumbnailHeight": 100, + "PreviewWidth": 1024, + "PreviewHeight": 0, + "ProfileWidth": 128, + "ProfileHeight": 128, + "InitialFont": "luximbi.ttf" + }, + "EmailSettings": { + "ByPassEmail" : true, + "SMTPUsername": "", + "SMTPPassword": "", + "SMTPServer": "", + "UseTLS": false, + "FeedbackEmail": "", + "FeedbackName": "", + "ApplePushServer": "", + "ApplePushCertPublic": "", + "ApplePushCertPrivate": "" + }, + "RateLimitSettings": { + "UseRateLimiter": true, + "PerSec": 10, + "MemoryStoreSize": 10000, + "VaryByRemoteAddr": true, + "VaryByHeader": "" + }, + "PrivacySettings": { + "ShowEmailAddress": true, + "ShowPhoneNumber": true, + "ShowSkypeId": true, + "ShowFullName": true + }, + "TeamSettings": { + "MaxUsersPerTeam": 150, + "AllowPublicLink": true, + "AllowValetDefault": false, + "TermsLink": "/static/help/configure_links.html", + "PrivacyLink": "/static/help/configure_links.html", + "AboutLink": "/static/help/configure_links.html", + "HelpLink": "/static/help/configure_links.html", + "ReportProblemLink": "/static/help/configure_links.html", + "TourLink": "/static/help/configure_links.html", + "DefaultThemeColor": "#2389D7" + } +} diff --git a/docker/local/docker-entry.sh b/docker/local/docker-entry.sh new file mode 100755 index 000000000..16583e525 --- /dev/null +++ b/docker/local/docker-entry.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +# See License.txt for license information. + +mkdir -p web/static/js + +echo "127.0.0.1 dockerhost" >> /etc/hosts +/etc/init.d/networking restart + +echo configuring mysql + +# SQL!!! +set -e + +get_option () { + local section=$1 + local option=$2 + local default=$3 + ret=$(my_print_defaults $section | grep '^--'${option}'=' | cut -d= -f2-) + [ -z $ret ] && ret=$default + echo $ret +} + + +# Get config +DATADIR="$("mysqld" --verbose --help 2>/dev/null | awk '$1 == "datadir" { print $2; exit }')" +SOCKET=$(get_option mysqld socket "$DATADIR/mysql.sock") +PIDFILE=$(get_option mysqld pid-file "/var/run/mysqld/mysqld.pid") + +if [ ! -d "$DATADIR/mysql" ]; then + if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" ]; then + echo >&2 'error: database is uninitialized and MYSQL_ROOT_PASSWORD not set' + echo >&2 ' Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?' + exit 1 + fi + + mkdir -p "$DATADIR" + chown -R mysql:mysql "$DATADIR" + + echo 'Running mysql_install_db' + mysql_install_db --user=mysql --datadir="$DATADIR" --rpm --keep-my-cnf + echo 'Finished mysql_install_db' + + mysqld --user=mysql --datadir="$DATADIR" --skip-networking & + for i in $(seq 30 -1 0); do + [ -S "$SOCKET" ] && break + echo 'MySQL init process in progress...' + sleep 1 + done + if [ $i = 0 ]; then + echo >&2 'MySQL init process failed.' + exit 1 + fi + + # These statements _must_ be on individual lines, and _must_ end with + # semicolons (no line breaks or comments are permitted). + # TODO proper SQL escaping on ALL the things D: + + tempSqlFile=$(mktemp /tmp/mysql-first-time.XXXXXX.sql) + cat > "$tempSqlFile" <<-EOSQL + -- What's done in this file shouldn't be replicated + -- or products like mysql-fabric won't work + SET @@SESSION.SQL_LOG_BIN=0; + + DELETE FROM mysql.user ; + CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; + GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ; + DROP DATABASE IF EXISTS test ; + EOSQL + + if [ "$MYSQL_DATABASE" ]; then + echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" >> "$tempSqlFile" + fi + + if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then + echo "CREATE USER '"$MYSQL_USER"'@'%' IDENTIFIED BY '"$MYSQL_PASSWORD"' ;" >> "$tempSqlFile" + + if [ "$MYSQL_DATABASE" ]; then + echo "GRANT ALL ON \`"$MYSQL_DATABASE"\`.* TO '"$MYSQL_USER"'@'%' ;" >> "$tempSqlFile" + fi + fi + + echo 'FLUSH PRIVILEGES ;' >> "$tempSqlFile" + + mysql -uroot < "$tempSqlFile" + + rm -f "$tempSqlFile" + kill $(cat $PIDFILE) + for i in $(seq 30 -1 0); do + [ -f "$PIDFILE" ] || break + echo 'MySQL init process in progress...' + sleep 1 + done + if [ $i = 0 ]; then + echo >&2 'MySQL hangs during init process.' + exit 1 + fi + echo 'MySQL init process done. Ready for start up.' +fi + +chown -R mysql:mysql "$DATADIR" + +mysqld & + +sleep 5 + +# ------------------------ + +echo starting react processor +cd /go/src/github.com/mattermost/platform/web/react && npm start & + +echo starting go web server +cd /go/src/github.com/mattermost/platform/; godep go run mattermost.go -config=/config_docker.json & + +echo starting compass watch +cd /go/src/github.com/mattermost/platform/web/sass-files && compass watch diff --git a/model/channel_count.go b/model/channel_count.go new file mode 100644 index 000000000..d5daba14e --- /dev/null +++ b/model/channel_count.go @@ -0,0 +1,63 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "crypto/md5" + "encoding/json" + "fmt" + "io" + "sort" + "strconv" +) + +type ChannelCounts struct { + Counts map[string]int64 `json:"counts"` + UpdateTimes map[string]int64 `json:"update_times"` +} + +func (o *ChannelCounts) Etag() string { + + ids := []string{} + for id, _ := range o.Counts { + ids = append(ids, id) + } + sort.Strings(ids) + + str := "" + for _, id := range ids { + str += id + strconv.FormatInt(o.Counts[id], 10) + } + + md5Counts := fmt.Sprintf("%x", md5.Sum([]byte(str))) + + var update int64 = 0 + for _, u := range o.UpdateTimes { + if u > update { + update = u + } + } + + return Etag(md5Counts, update) +} + +func (o *ChannelCounts) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func ChannelCountsFromJson(data io.Reader) *ChannelCounts { + decoder := json.NewDecoder(data) + var o ChannelCounts + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/channel_data.go b/model/channel_data.go new file mode 100644 index 000000000..234bdec6e --- /dev/null +++ b/model/channel_data.go @@ -0,0 +1,43 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type ChannelData struct { + Channel *Channel `json:"channel"` + Member *ChannelMember `json:"member"` +} + +func (o *ChannelData) Etag() string { + var mt int64 = 0 + if o.Member != nil { + mt = o.Member.LastUpdateAt + } + + return Etag(o.Channel.Id, o.Channel.UpdateAt, o.Channel.LastPostAt, mt) +} + +func (o *ChannelData) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func ChannelDataFromJson(data io.Reader) *ChannelData { + decoder := json.NewDecoder(data) + var o ChannelData + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/client.go b/model/client.go index a5016fa2c..6fcfa5043 100644 --- a/model/client.go +++ b/model/client.go @@ -390,6 +390,15 @@ func (c *Client) GetChannels(etag string) (*Result, *AppError) { } } +func (c *Client) GetChannel(id, etag string) (*Result, *AppError) { + if r, err := c.DoGet("/channels/"+id+"/", "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelDataFromJson(r.Body)}, nil + } +} + func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) { if r, err := c.DoGet("/channels/more", "", etag); err != nil { return nil, err @@ -399,6 +408,15 @@ func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) { } } +func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) { + if r, err := c.DoGet("/channels/counts", "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelCountsFromJson(r.Body)}, nil + } +} + func (c *Client) JoinChannel(id string) (*Result, *AppError) { if r, err := c.DoPost("/channels/"+id+"/join", ""); err != nil { return nil, err diff --git a/model/file.go b/model/file.go index 3d38ddbd1..85545d718 100644 --- a/model/file.go +++ b/model/file.go @@ -13,8 +13,8 @@ const ( ) var ( - IMAGE_EXTENSIONS = [4]string{".jpg", ".gif", ".bmp", ".png"} - IMAGE_MIME_TYPES = map[string]string{".jpg": "image/jpeg", ".gif": "image/gif", ".bmp": "image/bmp", ".png": "image/png", ".tiff": "image/tiff"} + IMAGE_EXTENSIONS = [5]string{".jpg", ".jpeg", ".gif", ".bmp", ".png"} + IMAGE_MIME_TYPES = map[string]string{".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".bmp": "image/bmp", ".png": "image/png", ".tiff": "image/tiff"} ) type FileUploadResponse struct { diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go index cac5c681b..b8bf8b5ac 100644 --- a/store/sql_channel_store.go +++ b/store/sql_channel_store.go @@ -281,6 +281,41 @@ func (s SqlChannelStore) GetMoreChannels(teamId string, userId string) StoreChan return storeChannel } +type channelIdWithCountAndUpdateAt struct { + Id string + TotalMsgCount int64 + UpdateAt int64 +} + +func (s SqlChannelStore) GetChannelCounts(teamId string, userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var data []channelIdWithCountAndUpdateAt + _, err := s.GetReplica().Select(&data, "SELECT Id, TotalMsgCount, UpdateAt FROM Channels WHERE Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = :UserId) AND TeamId = :TeamId AND DeleteAt = 0 ORDER BY DisplayName", map[string]interface{}{"TeamId": teamId, "UserId": userId}) + + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetChannelCounts", "We couldn't get the channel counts", "teamId="+teamId+", userId="+userId+", err="+err.Error()) + } else { + counts := &model.ChannelCounts{Counts: make(map[string]int64), UpdateTimes: make(map[string]int64)} + for i := range data { + v := data[i] + counts.Counts[v.Id] = v.TotalMsgCount + counts.UpdateTimes[v.Id] = v.UpdateAt + } + + result.Data = counts + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (s SqlChannelStore) GetByName(teamId string, name string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go index b14883843..dabe39904 100644 --- a/store/sql_channel_store_test.go +++ b/store/sql_channel_store_test.go @@ -462,6 +462,53 @@ func TestChannelStoreGetMoreChannels(t *testing.T) { } } +func TestChannelStoreGetChannelCounts(t *testing.T) { + Setup() + + o2 := model.Channel{} + o2.TeamId = model.NewId() + o2.DisplayName = "Channel2" + o2.Name = "a" + model.NewId() + "b" + o2.Type = model.CHANNEL_OPEN + Must(store.Channel().Save(&o2)) + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Channel1" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + Must(store.Channel().Save(&o1)) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL + Must(store.Channel().SaveMember(&m1)) + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = model.NewId() + m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL + Must(store.Channel().SaveMember(&m2)) + + m3 := model.ChannelMember{} + m3.ChannelId = o2.Id + m3.UserId = model.NewId() + m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL + Must(store.Channel().SaveMember(&m3)) + + cresult := <-store.Channel().GetChannelCounts(o1.TeamId, m1.UserId) + counts := cresult.Data.(*model.ChannelCounts) + + if len(counts.Counts) != 1 { + t.Fatal("wrong number of counts") + } + + if len(counts.UpdateTimes) != 1 { + t.Fatal("wrong number of update times") + } +} + func TestChannelStoreUpdateLastViewedAt(t *testing.T) { Setup() diff --git a/store/store.go b/store/store.go index 0934fe84b..613fe4198 100644 --- a/store/store.go +++ b/store/store.go @@ -50,6 +50,7 @@ type ChannelStore interface { GetByName(team_id string, domain string) StoreChannel GetChannels(teamId string, userId string) StoreChannel GetMoreChannels(teamId string, userId string) StoreChannel + GetChannelCounts(teamId string, userId string) StoreChannel SaveMember(member *model.ChannelMember) StoreChannel GetMembers(channelId string) StoreChannel diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index 16768a119..a19e5c16e 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -13,21 +13,23 @@ function getStateFromStoresForAudits() { } module.exports = React.createClass({ + displayName: 'AccessHistoryModal', componentDidMount: function() { - UserStore.addAuditsChangeListener(this._onChange); - $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function(e) { + UserStore.addAuditsChangeListener(this.onListenerChange); + $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function() { AsyncClient.getAudits(); }); var self = this; - $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { + $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function() { + $('#user_settings').modal('show'); self.setState({moreInfo: []}); }); }, componentWillUnmount: function() { - UserStore.removeAuditsChangeListener(this._onChange); + UserStore.removeAuditsChangeListener(this.onListenerChange); }, - _onChange: function() { + onListenerChange: function() { var newState = getStateFromStoresForAudits(); if (!utils.areStatesEqual(newState.audits, this.state.audits)) { this.setState(newState); @@ -61,6 +63,21 @@ module.exports = React.createClass({ currentAudit.session_id = 'N/A (Login attempt)'; } + var moreInfo = (<a href='#' className='theme' onClick={this.handleMoreInfo.bind(this, i)}>More info</a>); + if (this.state.moreInfo[i]) { + moreInfo = ( + <div> + <div>{'Session ID: ' + currentAudit.session_id}</div> + <div>{'URL: ' + currentAudit.action.replace(/\/api\/v[1-9]/, '')}</div> + </div> + ); + } + + var divider = null; + if (i < this.state.audits.length - 1) { + divider = (<div className='divider-light'></div>) + } + accessList[i] = ( <div className='access-history__table'> <div className='access__date'>{newDate}</div> @@ -68,25 +85,21 @@ module.exports = React.createClass({ <div className='report__time'>{newHistoryDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'})}</div> <div className='report__info'> <div>{'IP: ' + currentAudit.ip_address}</div> - {this.state.moreInfo[i] ? - <div> - <div>{'Session ID: ' + currentAudit.session_id}</div> - <div>{'URL: ' + currentAudit.action.replace(/\/api\/v[1-9]/, '')}</div> - </div> - : - <a href='#' className='theme' onClick={this.handleMoreInfo.bind(this, i)}>More info</a> - } + {moreInfo} </div> - {i < this.state.audits.length - 1 ? - <div className='divider-light'/> - : - null - } + {divider} </div> </div> ); } + var content; + if (this.state.audits.loading) { + content = (<LoadingScreen />); + } else { + content = (<form role='form'>{accessList}</form>); + } + return ( <div> <div className='modal fade' ref='modal' id='access-history' tabIndex='-1' role='dialog' aria-hidden='true'> @@ -97,13 +110,7 @@ module.exports = React.createClass({ <h4 className='modal-title' id='myModalLabel'>Access History</h4> </div> <div ref='modalBody' className='modal-body'> - {!this.state.audits.loading ? - <form role='form'> - {accessList} - </form> - : - <LoadingScreen /> - } + {content} </div> </div> </div> diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index f28f0d5f1..1192a72bc 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -10,40 +10,41 @@ var utils = require('../utils/utils.jsx'); function getStateFromStoresForSessions() { return { sessions: UserStore.getSessions(), - server_error: null, - client_error: null + serverError: null, + clientError: null }; } module.exports = React.createClass({ + displayName: 'ActivityLogModal', submitRevoke: function(altId) { - var self = this; Client.revokeSession(altId, function(data) { AsyncClient.getSessions(); }.bind(this), function(err) { - state = getStateFromStoresForSessions(); - state.server_error = err; + var state = getStateFromStoresForSessions(); + state.serverError = err; this.setState(state); }.bind(this) ); }, componentDidMount: function() { - UserStore.addSessionsChangeListener(this._onChange); + UserStore.addSessionsChangeListener(this.onListenerChange); $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function (e) { AsyncClient.getSessions(); }); var self = this; $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { - self.setState({ moreInfo: [] }); + $('#user_settings').modal('show'); + self.setState({moreInfo: []}); }); }, componentWillUnmount: function() { - UserStore.removeSessionsChangeListener(this._onChange); + UserStore.removeSessionsChangeListener(this.onListenerChange); }, - _onChange: function() { + onListenerChange: function() { var newState = getStateFromStoresForSessions(); if (!utils.areStatesEqual(newState.sessions, this.state.sessions)) { this.setState(newState); @@ -52,7 +53,7 @@ module.exports = React.createClass({ handleMoreInfo: function(index) { var newMoreInfo = this.state.moreInfo; newMoreInfo[index] = true; - this.setState({ moreInfo: newMoreInfo }); + this.setState({moreInfo: newMoreInfo}); }, getInitialState: function() { var initialState = getStateFromStoresForSessions(); @@ -61,68 +62,76 @@ module.exports = React.createClass({ }, render: function() { var activityList = []; - var server_error = this.state.server_error ? this.state.server_error : null; + var serverError = this.state.serverError; + + // Squash any false-y value for server error into null + if (!serverError) { + serverError = null; + } for (var i = 0; i < this.state.sessions.length; i++) { var currentSession = this.state.sessions[i]; var lastAccessTime = new Date(currentSession.last_activity_at); var firstAccessTime = new Date(currentSession.create_at); - var devicePicture = ""; + var devicePicture = ''; - if (currentSession.props.platform === "Windows") { - devicePicture = "fa fa-windows"; + if (currentSession.props.platform === 'Windows') { + devicePicture = 'fa fa-windows'; } - else if (currentSession.props.platform === "Macintosh" || currentSession.props.platform === "iPhone") { - devicePicture = "fa fa-apple"; + else if (currentSession.props.platform === 'Macintosh' || currentSession.props.platform === 'iPhone') { + devicePicture = 'fa fa-apple'; } - else if (currentSession.props.platform === "Linux") { - devicePicture = "fa fa-linux"; + else if (currentSession.props.platform === 'Linux') { + devicePicture = 'fa fa-linux'; + } + + var moreInfo; + if (this.state.moreInfo[i]) { + moreInfo = ( + <div> + <div>{'First time active: ' + firstAccessTime.toDateString() + ', ' + lastAccessTime.toLocaleTimeString()}</div> + <div>{'OS: ' + currentSession.props.os}</div> + <div>{'Browser: ' + currentSession.props.browser}</div> + <div>{'Session ID: ' + currentSession.alt_id}</div> + </div> + ); + } else { + moreInfo = (<a className='theme' href='#' onClick={this.handleMoreInfo.bind(this, i)}>More info</a>); } activityList[i] = ( - <div className="activity-log__table"> - <div className="activity-log__report"> - <div className="report__platform"><i className={devicePicture} />{currentSession.props.platform}</div> - <div className="report__info"> - <div>{"Last activity: " + lastAccessTime.toDateString() + ", " + lastAccessTime.toLocaleTimeString()}</div> - { this.state.moreInfo[i] ? - <div> - <div>{"First time active: " + firstAccessTime.toDateString() + ", " + lastAccessTime.toLocaleTimeString()}</div> - <div>{"OS: " + currentSession.props.os}</div> - <div>{"Browser: " + currentSession.props.browser}</div> - <div>{"Session ID: " + currentSession.alt_id}</div> - </div> - : - <a className="theme" href="#" onClick={this.handleMoreInfo.bind(this, i)}>More info</a> - } + <div className='activity-log__table'> + <div className='activity-log__report'> + <div className='report__platform'><i className={devicePicture} />{currentSession.props.platform}</div> + <div className='report__info'> + <div>{'Last activity: ' + lastAccessTime.toDateString() + ', ' + lastAccessTime.toLocaleTimeString()}</div> + {moreInfo} </div> </div> - <div className="activity-log__action"><button onClick={this.submitRevoke.bind(this, currentSession.alt_id)} className="btn btn-primary">Logout</button></div> + <div className='activity-log__action'><button onClick={this.submitRevoke.bind(this, currentSession.alt_id)} className='btn btn-primary'>Logout</button></div> </div> ); } + var content; + if (this.state.sessions.loading) { + content = (<LoadingScreen />); + } else { + content = (<form role='form'>{activityList}</form>); + } + return ( <div> - <div className="modal fade" ref="modal" id="activity-log" tabIndex="-1" role="dialog" aria-hidden="true"> - <div className="modal-dialog modal-lg"> - <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> - <h4 className="modal-title" id="myModalLabel">Active Sessions</h4> + <div className='modal fade' ref='modal' id='activity-log' tabIndex='-1' role='dialog' aria-hidden='true'> + <div className='modal-dialog modal-lg'> + <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> + <h4 className='modal-title' id='myModalLabel'>Active Sessions</h4> </div> - <p className="session-help-text">Sessions are created when you log in with your email and password to a new browser on a device. Sessions let you use Mattermost for up to 30 days without having to log in again. If you want to log out sooner, use the "Logout" button below to end a session.</p> - <div ref="modalBody" className="modal-body"> - { !this.state.sessions.loading ? - <div> - <form role="form"> - { activityList } - </form> - { server_error } - </div> - : - <LoadingScreen /> - } + <p className='session-help-text'>Sessions are created when you log in with your email and password to a new browser on a device. Sessions let you use Mattermost for up to 30 days without having to log in again. If you want to log out sooner, use the 'Logout' button below to end a session.</p> + <div ref='modalBody' className='modal-body'> + {content} </div> </div> </div> diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx index 06d7fc3e8..dcff5b89d 100644 --- a/web/react/components/edit_channel_modal.jsx +++ b/web/react/components/edit_channel_modal.jsx @@ -14,7 +14,7 @@ module.exports = React.createClass({ Client.updateChannelDesc(data, function(data) { this.setState({ server_error: "" }); - AsyncClient.getChannels(true); + AsyncClient.getChannel(this.state.channel_id); $(this.refs.modal.getDOMNode()).modal('hide'); }.bind(this), function(err) { diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index b7ea5734f..c36c908d2 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -113,6 +113,14 @@ module.exports = React.createClass({ fileSizeString = utils.fileSizeToString(this.state.fileSize); } + var filenameString = decodeURIComponent(utils.getFileName(filename)); + var trimmedFilename; + if (filenameString.length > 35) { + trimmedFilename = filenameString.substring(0, Math.min(35, filenameString.length)) + "..."; + } else { + trimmedFilename = filenameString; + } + return ( <div className="post-image__column" key={filename}> <a className="post-image__thumbnail" href="#" onClick={this.props.handleImageClick} @@ -120,7 +128,7 @@ module.exports = React.createClass({ {thumbnail} </a> <div className="post-image__details"> - <div className="post-image__name">{decodeURIComponent(utils.getFileName(filename))}</div> + <div data-toggle="tooltip" title={filenameString} className="post-image__name">{trimmedFilename}</div> <div> <span className="post-image__type">{fileInfo.ext.toUpperCase()}</span> <span className="post-image__size">{fileSizeString}</span> diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx index b90fa4fd3..c1fab669c 100644 --- a/web/react/components/file_upload.jsx +++ b/web/react/components/file_upload.jsx @@ -7,6 +7,13 @@ var ChannelStore = require('../stores/channel_store.jsx'); var utils = require('../utils/utils.jsx'); module.exports = React.createClass({ + displayName: 'FileUpload', + propTypes: { + onUploadError: React.PropTypes.func, + getFileCount: React.PropTypes.func, + onFileUpload: React.PropTypes.func, + onUploadStart: React.PropTypes.func + }, getInitialState: function() { return {requests: {}}; }, @@ -21,7 +28,7 @@ module.exports = React.createClass({ // This looks redundant, but must be done this way due to // setState being an asynchronous call var numFiles = 0; - for(var i = 0; i < files.length; i++) { + for (var i = 0; i < files.length; i++) { if (files[i].size <= Constants.MAX_FILE_SIZE) { numFiles++; } @@ -51,11 +58,11 @@ module.exports = React.createClass({ var request = client.uploadFile(formData, function(data) { var parsedData = $.parseJSON(data); - this.props.onFileUpload(parsedData['filenames'], parsedData['client_ids'], channelId); + this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); var requests = this.state.requests; - for (var i = 0; i < parsedData['client_ids'].length; i++) { - delete requests[parsedData['client_ids'][i]]; + for (var i = 0; i < parsedData.client_ids.length; i++) { + delete requests[parsedData.client_ids[i]]; } this.setState({requests: requests}); }.bind(this), @@ -100,12 +107,9 @@ module.exports = React.createClass({ if (items) { for (var i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1) { - var ext = items[i].type.split('/')[1].toLowerCase(); - if (ext === 'jpeg') { - ext = 'jpg'; - } + var testExt = items[i].type.split('/')[1].toLowerCase(); - if (Constants.IMAGE_TYPES.indexOf(ext) < 0) { + if (Constants.IMAGE_TYPES.indexOf(testExt) < 0) { continue; } @@ -113,7 +117,7 @@ module.exports = React.createClass({ } } - var numToUpload = Math.min(Constants.MAX_UPLOAD_FILES - self.props.getFileCount(channelId), numItems); + var numToUpload = Math.min(Constants.MAX_UPLOAD_FILES - self.props.getFileCount(ChannelStore.getCurrentId()), numItems); if (numItems > numToUpload) { self.props.onUploadError('Uploads limited to ' + Constants.MAX_UPLOAD_FILES + ' files maximum. Please use additional posts for more files.'); @@ -124,9 +128,6 @@ module.exports = React.createClass({ var file = items[i].getAsFile(); var ext = items[i].type.split('/')[1].toLowerCase(); - if (ext === 'jpeg') { - ext = 'jpg'; - } if (Constants.IMAGE_TYPES.indexOf(ext) < 0) { continue; @@ -161,11 +162,11 @@ module.exports = React.createClass({ var request = client.uploadFile(formData, function(data) { var parsedData = $.parseJSON(data); - self.props.onFileUpload(parsedData['filenames'], parsedData['client_ids'], channelId); + self.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); var requests = self.state.requests; - for (var i = 0; i < parsedData['client_ids'].length; i++) { - delete requests[parsedData['client_ids'][i]]; + for (var i = 0; i < parsedData.client_ids.length; i++) { + delete requests[parsedData.client_ids[i]]; } self.setState({requests: requests}); }, diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 75538c8fe..5b6924891 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. var utils = require('../utils/utils.jsx'); +var ConfigStore = require('../stores/config_store.jsx'); var Client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); var ConfirmModal = require('./confirm_modal.jsx'); @@ -35,6 +36,10 @@ module.exports = React.createClass({ }); }, handleSubmit: function(e) { + if (!this.state.emailEnabled) { + return; + } + var inviteIds = this.state.inviteIds; var count = inviteIds.length; var invites = []; @@ -147,12 +152,18 @@ module.exports = React.createClass({ idCount: 0, emailErrors: {}, firstNameErrors: {}, - lastNameErrors: {} + lastNameErrors: {}, + emailEnabled: !ConfigStore.getSettingAsBoolean('ByPassEmail', false) }; }, render: function() { var currentUser = UserStore.getCurrentUser(); + var inputDisabled = ''; + if (!this.state.emailEnabled) { + inputDisabled = 'disabled'; + } + if (currentUser != null) { var inviteSections = []; var inviteIds = this.state.inviteIds; @@ -195,13 +206,13 @@ module.exports = React.createClass({ nameFields = (<div className='row--invite'> <div className='col-sm-6'> <div className={firstNameClass}> - <input type='text' className='form-control' ref={'first_name' + index} placeholder='First name' maxLength='64' /> + <input type='text' className='form-control' ref={'first_name' + index} placeholder='First name' maxLength='64' disabled={!this.state.emailEnabled}/> {firstNameError} </div> </div> <div className='col-sm-6'> <div className={lastNameClass}> - <input type='text' className='form-control' ref={'last_name' + index} placeholder='Last name' maxLength='64' /> + <input type='text' className='form-control' ref={'last_name' + index} placeholder='Last name' maxLength='64' disabled={!this.state.emailEnabled}/> {lastNameError} </div> </div> @@ -212,7 +223,7 @@ module.exports = React.createClass({ <div key={'key' + index}> {removeButton} <div className={emailClass}> - <input onKeyUp={this.displayNameKeyUp} type='text' ref={'email' + index} className='form-control' placeholder='email@domain.com' maxLength='64' /> + <input onKeyUp={this.displayNameKeyUp} type='text' ref={'email' + index} className='form-control' placeholder='email@domain.com' maxLength='64' disabled={!this.state.emailEnabled}/> {emailError} </div> {nameFields} @@ -225,6 +236,45 @@ module.exports = React.createClass({ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; } + var content = null; + var sendButton = null; + if (this.state.emailEnabled) { + content = ( + <div> + {serverError} + <button type='button' className='btn btn-default' onClick={this.addInviteFields}>Add another</button> + <br/> + <br/> + <span>People invited automatically join Town Square channel.</span> + </div> + ); + + sendButton = <button onClick={this.handleSubmit} type='button' className='btn btn-primary'>Send Invitations</button> + } else { + var teamInviteLink = null; + if (currentUser && this.props.teamType === 'O') { + var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id; + var link = <a href='#' data-toggle='modal' data-target='#get_link' data-title='Team Invite' data-value={linkUrl} onClick={ + function() { + $('#invite_member').modal('hide'); + } + }>Team Invite Link</a>; + + teamInviteLink = ( + <p> + You can also invite people using the {link}. + </p> + ); + } + + content = ( + <div> + <p>Email is currently disabled for your team, and email invitations cannot be sent. Contact your system administrator to enable email and email invitations.</p> + {teamInviteLink} + </div> + ); + } + return ( <div> <div className='modal fade' ref='modal' id='invite_member' tabIndex='-1' role='dialog' aria-hidden='true'> @@ -238,15 +288,11 @@ module.exports = React.createClass({ <form role='form'> {inviteSections} </form> - {serverError} - <button type='button' className='btn btn-default' onClick={this.addInviteFields}>Add another</button> - <br/> - <br/> - <span>People invited automatically join Town Square channel.</span> + {content} </div> <div className='modal-footer'> <button type='button' className='btn btn-default' data-dismiss='modal'>Cancel</button> - <button onClick={this.handleSubmit} type='button' className='btn btn-primary'>Send Invitations</button> + {sendButton} </div> </div> </div> diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index fe0a47777..eba4f06f4 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -50,7 +50,7 @@ module.exports = React.createClass({ var redirect = utils.getUrlParameter("redirect"); if (redirect) { - window.location.pathname = decodeURI(redirect); + window.location.pathname = decodeURIComponent(redirect); } else { window.location.pathname = '/' + name + '/channels/town-square'; } diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx index 007476f9b..5261ed6a7 100644 --- a/web/react/components/more_channels.jsx +++ b/web/react/components/more_channels.jsx @@ -1,34 +1,32 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var asyncClient = require('../utils/async_client.jsx'); -var UserStore = require('../stores/user_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var LoadingScreen = require('./loading_screen.jsx'); function getStateFromStores() { - return { - channels: ChannelStore.getMoreAll(), - server_error: null - }; + return { + channels: ChannelStore.getMoreAll(), + serverError: null + }; } module.exports = React.createClass({ - displayName: "MoreChannelsModal", + displayName: 'MoreChannelsModal', componentDidMount: function() { ChannelStore.addMoreChangeListener(this._onChange); - $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function (e) { + $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function shown() { asyncClient.getMoreChannels(true); }); var self = this; - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { + $(this.refs.modal.getDOMNode()).on('show.bs.modal', function show(e) { var button = e.relatedTarget; - self.setState({ channel_type: $(button).attr('data-channeltype') }); + self.setState({channelType: $(button).attr('data-channeltype')}); }); }, componentWillUnmount: function() { @@ -42,18 +40,17 @@ module.exports = React.createClass({ }, getInitialState: function() { var initState = getStateFromStores(); - initState.channel_type = ""; + initState.channelType = ''; return initState; }, - handleJoin: function(e) { - var self = this; - client.joinChannel(e, - function(data) { - $(self.refs.modal.getDOMNode()).modal('hide'); - asyncClient.getChannels(true); + handleJoin: function(id) { + client.joinChannel(id, + function() { + $(this.refs.modal.getDOMNode()).modal('hide'); + asyncClient.getChannel(id); }.bind(this), function(err) { - this.state.server_error = err.message; + this.state.serverError = err.message; this.setState(this.state); }.bind(this) ); @@ -62,52 +59,66 @@ module.exports = React.createClass({ $(this.refs.modal.getDOMNode()).modal('hide'); }, render: function() { - var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null; + var serverError; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } + var outter = this; var moreChannels; - if (this.state.channels != null) - moreChannels = this.state.channels; + if (this.state.channels != null) { + var channels = this.state.channels; + if (!channels.loading) { + if (channels.length) { + moreChannels = ( + <table className='more-channel-table table'> + <tbody> + {channels.map(function cMap(channel) { + return ( + <tr key={channel.id}> + <td> + <p className='more-channel-name'>{channel.display_name}</p> + <p className='more-channel-description'>{channel.description}</p> + </td> + <td className='td--action'><button onClick={outter.handleJoin.bind(outter, channel.id)} className='btn btn-primary'>Join</button></td> + </tr> + ); + })} + </tbody> + </table> + ); + } else { + moreChannels = ( + <div className='no-channel-message'> + <p className='primary-message'>No more channels to join</p> + <p className='secondary-message'>Click 'Create New Channel' to make a new one</p> + </div> + ); + } + } else { + moreChannels = <LoadingScreen />; + } + } return ( - <div className="modal fade" id="more_channels" ref="modal" 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"> - <span aria-hidden="true">×</span> - <span className="sr-only">Close</span> + <div className='modal fade' id='more_channels' ref='modal' 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'> + <span aria-hidden='true'>×</span> + <span className='sr-only'>Close</span> </button> - <h4 className="modal-title">More Channels</h4> - <button data-toggle="modal" data-target="#new_channel" data-channeltype={this.state.channel_type} type="button" className="btn btn-primary channel-create-btn" onClick={this.handleNewChannel}>Create New Channel</button> + <h4 className='modal-title'>More Channels</h4> + <button data-toggle='modal' data-target='#new_channel' data-channeltype={this.state.channelType} type='button' className='btn btn-primary channel-create-btn' onClick={this.handleNewChannel}>Create New Channel</button> </div> - <div className="modal-body"> - {!moreChannels.loading ? - (moreChannels.length ? - <table className="more-channel-table table"> - <tbody> - {moreChannels.map(function(channel) { - return ( - <tr key={channel.id}> - <td> - <p className="more-channel-name">{channel.display_name}</p> - <p className="more-channel-description">{channel.description}</p> - </td> - <td className="td--action"><button onClick={outter.handleJoin.bind(outter, channel.id)} className="btn btn-primary">Join</button></td> - </tr> - ) - })} - </tbody> - </table> - : <div className="no-channel-message"> - <p className="primary-message">No more channels to join</p> - <p className="secondary-message">Click 'Create New Channel' to make a new one</p> - </div>) - : <LoadingScreen /> } - { server_error } + <div className='modal-body'> + {moreChannels} + {serverError} </div> - <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Close</button> + <div className='modal-footer'> + <button type='button' className='btn btn-default' data-dismiss='modal'>Close</button> </div> </div> </div> diff --git a/web/react/components/new_channel.jsx b/web/react/components/new_channel.jsx index c22147022..b00376758 100644 --- a/web/react/components/new_channel.jsx +++ b/web/react/components/new_channel.jsx @@ -55,16 +55,16 @@ module.exports = React.createClass({ channel.description = this.refs.channel_desc.getDOMNode().value.trim(); channel.type = this.state.channelType; - var self = this; client.createChannel(channel, - function() { + function(data) { + $(this.refs.modal.getDOMNode()).modal('hide'); + + asyncClient.getChannel(data.id); + utils.switchChannel(data); + this.refs.display_name.getDOMNode().value = ''; this.refs.channel_name.getDOMNode().value = ''; this.refs.channel_desc.getDOMNode().value = ''; - - $(self.refs.modal.getDOMNode()).modal('hide'); - window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; - asyncClient.getChannels(true); }.bind(this), function(err) { state.serverError = err.message; diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx index 26593b7fa..93cb6ef21 100644 --- a/web/react/components/rename_channel_modal.jsx +++ b/web/react/components/rename_channel_modal.jsx @@ -63,12 +63,14 @@ module.exports = React.createClass({ Client.updateChannel(channel, function(data, text, req) { + $(this.refs.modal.getDOMNode()).modal('hide'); + + AsyncClient.getChannel(channel.id); + utils.updateTabTitle(channel.display_name); + utils.updateAddressBar(channel.name); + this.refs.display_name.getDOMNode().value = ""; this.refs.channel_name.getDOMNode().value = ""; - - $('#' + this.props.modalId).modal('hide'); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + this.state.channel_name; - AsyncClient.getChannels(true); }.bind(this), function(err) { state.server_error = err.message; diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index fe73cbcf7..a8496b385 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -102,7 +102,7 @@ function getStateFromStores() { } readDirectChannels = readDirectChannels.slice(index); - showDirectChannels.sort(function(a, b) { + showDirectChannels.sort(function directSort(a, b) { if (a.display_name < b.display_name) { return -1; } @@ -114,7 +114,7 @@ function getStateFromStores() { } return { - active_id: currentId, + activeId: currentId, channels: ChannelStore.getAll(), members: members, showDirectChannels: showDirectChannels, @@ -128,6 +128,7 @@ module.exports = React.createClass({ ChannelStore.addChangeListener(this.onChange); UserStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange); + TeamStore.addChangeListener(this.onChange); SocketStore.addChangeListener(this.onSocketChange); $('.nav-pills__container').perfectScrollbar(); @@ -146,6 +147,7 @@ module.exports = React.createClass({ ChannelStore.removeChangeListener(this.onChange); UserStore.removeChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onChange); + TeamStore.removeChangeListener(this.onChange); SocketStore.removeChangeListener(this.onSocketChange); }, onChange: function() { @@ -157,9 +159,11 @@ module.exports = React.createClass({ onSocketChange: function(msg) { if (msg.action === 'posted') { if (ChannelStore.getCurrentId() === msg.channel_id) { - AsyncClient.getChannels(true, window.isActive); + if (window.isActive) { + AsyncClient.updateLastViewedAt(); + } } else { - AsyncClient.getChannels(true); + AsyncClient.getChannels(); } if (UserStore.getCurrentId() !== msg.user_id) { @@ -214,12 +218,12 @@ module.exports = React.createClass({ } } } else if (msg.action === 'viewed') { - if (ChannelStore.getCurrentId() != msg.channel_id) { - AsyncClient.getChannels(true); + if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) { + AsyncClient.getChannel(msg.channel_id); } } else if (msg.action === 'user_added') { if (UserStore.getCurrentId() === msg.user_id) { - AsyncClient.getChannels(true); + AsyncClient.getChannel(msg.channel_id); } } else if (msg.action === 'user_removed') { if (msg.user_id === UserStore.getCurrentId()) { @@ -282,7 +286,7 @@ module.exports = React.createClass({ }, render: function() { var members = this.state.members; - var activeId = this.state.active_id; + var activeId = this.state.activeId; var badgesActive = false; // keep track of the first and last unread channels so we can use them to set the unread indicators @@ -294,7 +298,7 @@ module.exports = React.createClass({ var channelMember = members[channel.id]; var linkClass = ''; - if (channel.id === self.state.active_id) { + if (channel.id === activeId) { linkClass = 'active'; } @@ -346,15 +350,16 @@ module.exports = React.createClass({ // set up click handler to switch channels (or create a new channel for non-existant ones) var clickHandler = null; - var href; + var href = '#'; + var teamURL = TeamStore.getCurrentTeamUrl(); if (!channel.fake) { clickHandler = function(e) { e.preventDefault(); utils.switchChannel(channel); }; - href = '#'; - } else { - href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; + } + if (channel.fake && teamURL){ + href = teamURL + '/channels/' + channel.name; } return ( diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index cc3f255ee..761c06e74 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -96,7 +96,7 @@ var NavbarDropdown = React.createClass({ <span className='dropdown__icon' dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> </a> <ul className='dropdown-menu' role='menu'> - <li><a href='#' data-toggle='modal' data-target='#user_settings1'>Account Settings</a></li> + <li><a href='#' data-toggle='modal' data-target='#user_settings'>Account Settings</a></li> {teamSettings} {inviteLink} {teamLink} diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index 2439719a1..d221ca840 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -72,7 +72,7 @@ module.exports = React.createClass({ <div className='nav-pills__container'> <ul className='nav nav-pills nav-stacked'> - <li><a href='#' data-toggle='modal' data-target='#user_settings1'><i className='glyphicon glyphicon-cog'></i>Account Settings</a></li> + <li><a href='#' data-toggle='modal' data-target='#user_settings'><i className='glyphicon glyphicon-cog'></i>Account Settings</a></li> {teamSettingsLink} {inviteLink} {teamLink} diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx index 3f35a5912..e27fcd19d 100644 --- a/web/react/components/signup_team_complete.jsx +++ b/web/react/components/signup_team_complete.jsx @@ -1,8 +1,8 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - var utils = require('../utils/utils.jsx'); +var ConfigStore = require('../stores/config_store.jsx'); var client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); @@ -11,111 +11,132 @@ var constants = require('../utils/constants.jsx'); WelcomePage = React.createClass({ submitNext: function (e) { if (!BrowserStore.isLocalStorageSupported()) { - this.setState({ storage_error: "This service requires local storage to be enabled. Please enable it or exit private browsing."} ); + this.setState({storageError: 'This service requires local storage to be enabled. Please enable it or exit private browsing.'}); return; } e.preventDefault(); - this.props.state.wizard = "team_display_name"; + this.props.state.wizard = 'team_display_name'; this.props.updateParent(this.props.state); }, handleDiffEmail: function (e) { e.preventDefault(); - this.setState({ use_diff: true }); + this.setState({useDiff: true}); }, handleDiffSubmit: function (e) { e.preventDefault(); - var state = { use_diff: true, server_error: "" }; + var state = {useDiff: true, serverError: ''}; var email = this.refs.email.getDOMNode().value.trim().toLowerCase(); if (!email || !utils.isEmail(email)) { - state.email_error = "Please enter a valid email address"; + state.emailError = 'Please enter a valid email address'; this.setState(state); return; - } - else if (!BrowserStore.isLocalStorageSupported()) { - state.email_error = "This service requires local storage to be enabled. Please enable it or exit private browsing."; + } else if (!BrowserStore.isLocalStorageSupported()) { + state.emailError = 'This service requires local storage to be enabled. Please enable it or exit private browsing.'; this.setState(state); return; - } - else { - state.email_error = ""; + } else { + state.emailError = ''; } client.signupTeam(email, function(data) { - if (data["follow_link"]) { - window.location.href = data["follow_link"]; + if (data['follow_link']) { + window.location.href = data['follow_link']; } else { - this.props.state.wizard = "finished"; + this.props.state.wizard = 'finished'; this.props.updateParent(this.props.state); - window.location.href = "/signup_team_confirm/?email=" + encodeURIComponent(team.email); + window.location.href = '/signup_team_confirm/?email=' + encodeURIComponent(team.email); } }.bind(this), function(err) { - this.state.server_error = err.message; + this.state.serverError = err.message; this.setState(this.state); }.bind(this) ); }, getInitialState: function() { - return { use_diff: false }; + return {useDiff: false}; }, handleKeyPress: function(event) { - if (event.keyCode == 13) { + if (event.keyCode === 13) { this.submitNext(event); } }, componentWillMount: function() { - document.addEventListener("keyup", this.handleKeyPress, false); + document.addEventListener('keyup', this.handleKeyPress, false); }, componentWillUnmount: function() { - document.removeEventListener("keyup", this.handleKeyPress, false); + document.removeEventListener('keyup', this.handleKeyPress, false); }, render: function() { - client.track('signup', 'signup_team_01_welcome'); - var storage_error = this.state.storage_error ? <label className="control-label">{ this.state.storage_error }</label> : null; - var email_error = this.state.email_error ? <label className="control-label">{ this.state.email_error }</label> : null; - var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className="control-label">{ this.state.server_error }</label></div> : null; + var storageError = null; + if (this.state.storageError) { + storageError = <label className='control-label'>{this.state.storageError}</label>; + } + + var emailError = null; + var emailDivClass = 'form-group'; + if (this.state.emailError) { + emailError = <label className='control-label'>{this.state.emailError}</label>; + emailDivClass += ' has-error'; + } + + var serverError = null; + if (this.state.serverError) { + serverError = ( + <div className='form-group has-error'> + <label className='control-label'>{this.state.serverError}</label> + </div> + ); + } + + var differentEmailLinkClass = ''; + var emailDivContainerClass = 'hidden'; + if (this.state.useDiff) { + differentEmailLinkClass = 'hidden'; + emailDivContainerClass = ''; + } return ( <div> <p> - <img className="signup-team-logo" src="/static/images/logo.png" /> - <h3 className="sub-heading">Welcome to:</h3> - <h1 className="margin--top-none">{config.SiteName}</h1> + <img className='signup-team-logo' src='/static/images/logo.png' /> + <h3 className='sub-heading'>Welcome to:</h3> + <h1 className='margin--top-none'>{config.SiteName}</h1> </p> - <p className="margin--less">Let's set up your new team</p> + <p className='margin--less'>Let's set up your new team</p> <p> Please confirm your email address:<br /> - <div className="inner__content"> - <div className="block--gray">{ this.props.state.team.email }</div> + <div className='inner__content'> + <div className='block--gray'>{this.props.state.team.email}</div> </div> </p> - <p className="margin--extra color--light"> + <p className='margin--extra color--light'> Your account will administer the new team site. <br /> You can add other administrators later. </p> - <div className="form-group"> - <button className="btn-primary btn form-group" type="submit" onClick={this.submitNext}><i className="glyphicon glyphicon-ok"></i>Yes, this address is correct</button> - { storage_error } + <div className='form-group'> + <button className='btn-primary btn form-group' type='submit' onClick={this.submitNext}><i className='glyphicon glyphicon-ok'></i>Yes, this address is correct</button> + {storageError} </div> <hr /> - <div className={ this.state.use_diff ? "" : "hidden" }> - <div className={ email_error ? "form-group has-error" : "form-group" }> - <div className="row"> - <div className="col-sm-9"> - <input type="email" ref="email" className="form-control" placeholder="Email Address" maxLength="128" /> + <div className={emailDivContainerClass}> + <div className={emailDivClass}> + <div className='row'> + <div className='col-sm-9'> + <input type='email' ref='email' className='form-control' placeholder='Email Address' maxLength='128' /> </div> </div> - { email_error } + {emailError} </div> - { server_error } - <button className="btn btn-md btn-primary" type="button" onClick={this.handleDiffSubmit} type="submit">Use this instead</button> + {serverError} + <button className='btn btn-md btn-primary' type='button' onClick={this.handleDiffSubmit} type='submit'>Use this instead</button> </div> - <a href="#" onClick={this.handleDiffEmail} className={ this.state.use_diff ? "hidden" : "" }>Use a different email</a> + <a href='#' onClick={this.handleDiffEmail} className={differentEmailLinkClass}>Use a different email</a> </div> ); } @@ -124,7 +145,7 @@ WelcomePage = React.createClass({ TeamDisplayNamePage = React.createClass({ submitBack: function (e) { e.preventDefault(); - this.props.state.wizard = "welcome"; + this.props.state.wizard = 'welcome'; this.props.updateParent(this.props.state); }, submitNext: function (e) { @@ -132,17 +153,17 @@ TeamDisplayNamePage = React.createClass({ var display_name = this.refs.name.getDOMNode().value.trim(); if (!display_name) { - this.setState({name_error: "This field is required"}); + this.setState({nameError: 'This field is required'}); return; } - this.props.state.wizard = "team_url"; + this.props.state.wizard = 'team_url'; this.props.state.team.display_name = display_name; this.props.state.team.name = utils.cleanUpUrlable(display_name); this.props.updateParent(this.props.state); }, getInitialState: function() { - return { }; + return {}; }, handleFocus: function(e) { e.preventDefault(); @@ -150,31 +171,35 @@ TeamDisplayNamePage = React.createClass({ e.currentTarget.select(); }, render: function() { - client.track('signup', 'signup_team_02_name'); - var name_error = this.state.name_error ? <label className="control-label">{ this.state.name_error }</label> : null; + var nameError = null; + var nameDivClass = 'form-group'; + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameDivClass += ' has-error'; + } return ( <div> <form> - <img className="signup-team-logo" src="/static/images/logo.png" /> + <img className='signup-team-logo' src='/static/images/logo.png' /> - <h2>{utils.toTitleCase(strings.Team) + " Name"}</h2> - <div className={ name_error ? "form-group has-error" : "form-group" }> - <div className="row"> - <div className="col-sm-9"> - <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.display_name} autoFocus={true} onFocus={this.handleFocus} /> + <h2>{utils.toTitleCase(strings.Team) + ' Name'}</h2> + <div className={nameDivClass}> + <div className='row'> + <div className='col-sm-9'> + <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' defaultValue={this.props.state.team.display_name} autoFocus={true} onFocus={this.handleFocus} /> + </div> + </div> + {nameError} </div> - </div> - { name_error } - </div> - <div>{"Name your " + strings.Team + " in any language. Your " + strings.Team + " name shows in menus and headings."}</div> - <button type="submit" className="btn btn-primary margin--extra" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button> - <div className="margin--extra"> - <a href="#" onClick={this.submitBack}>Back to previous step</a> - </div> - </form> + <div>{'Name your ' + strings.Team + ' in any language. Your ' + strings.Team + ' name shows in menus and headings.'}</div> + <button type='submit' className='btn btn-primary margin--extra' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button> + <div className='margin--extra'> + <a href='#' onClick={this.submitBack}>Back to previous step</a> + </div> + </form> </div> ); } @@ -183,7 +208,7 @@ TeamDisplayNamePage = React.createClass({ TeamURLPage = React.createClass({ submitBack: function (e) { e.preventDefault(); - this.props.state.wizard = "team_display_name"; + this.props.state.wizard = 'team_display_name'; this.props.updateParent(this.props.state); }, submitNext: function (e) { @@ -191,25 +216,24 @@ TeamURLPage = React.createClass({ var name = this.refs.name.getDOMNode().value.trim(); if (!name) { - this.setState({name_error: "This field is required"}); + this.setState({nameError: 'This field is required'}); return; } - var cleaned_name = utils.cleanUpUrlable(name); + var cleanedName = utils.cleanUpUrlable(name); var urlRegex = /^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$/g; - if (cleaned_name != name || !urlRegex.test(name)) { - this.setState({name_error: "Must be lowercase alphanumeric characters"}); + if (cleanedName !== name || !urlRegex.test(name)) { + this.setState({nameError: 'Must be lowercase alphanumeric characters'}); return; - } - else if (cleaned_name.length <= 3 || cleaned_name.length > 15) { - this.setState({name_error: "Name must be 4 or more characters up to a maximum of 15"}) + } else if (cleanedName.length <= 3 || cleanedName.length > 15) { + this.setState({nameError: 'Name must be 4 or more characters up to a maximum of 15'}); return; } for (var index = 0; index < constants.RESERVED_TEAM_NAMES.length; index++) { - if (cleaned_name.indexOf(constants.RESERVED_TEAM_NAMES[index]) == 0) { - this.setState({name_error: "This team name is unavailable"}) + if (cleanedName.indexOf(constants.RESERVED_TEAM_NAMES[index]) === 0) { + this.setState({nameError: 'This team name is unavailable'}); return; } } @@ -218,28 +242,27 @@ TeamURLPage = React.createClass({ function(data) { if (!data) { if (config.AllowSignupDomainsWizard) { - this.props.state.wizard = "allowed_domains"; + this.props.state.wizard = 'allowed_domains'; } else { - this.props.state.wizard = "send_invites"; + this.props.state.wizard = 'send_invites'; this.props.state.team.type = 'O'; } this.props.state.team.name = name; this.props.updateParent(this.props.state); - } - else { - this.state.name_error = "This URL is unavailable. Please try another."; + } else { + this.state.nameError = 'This URL is unavailable. Please try another.'; this.setState(this.state); } }.bind(this), function(err) { - this.state.name_error = err.message; + this.state.nameError = err.message; this.setState(this.state); }.bind(this) ); }, getInitialState: function() { - return { }; + return {}; }, handleFocus: function(e) { e.preventDefault(); @@ -247,40 +270,44 @@ TeamURLPage = React.createClass({ e.currentTarget.select(); }, render: function() { - $('body').tooltip( {selector: '[data-toggle=tooltip]', trigger: 'hover click'} ); client.track('signup', 'signup_team_03_url'); - var name_error = this.state.name_error ? <label className="control-label">{ this.state.name_error }</label> : null; + var nameError = null; + var nameDivClass = 'form-group'; + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameDivClass += ' has-error'; + } return ( <div> <form> - <img className="signup-team-logo" src="/static/images/logo.png" /> - <h2>{utils.toTitleCase(strings.Team) + " URL"}</h2> - <div className={ name_error ? "form-group has-error" : "form-group" }> - <div className="row"> - <div className="col-sm-11"> - <div className="input-group input-group--limit"> - <span data-toggle="tooltip" title={ utils.getWindowLocationOrigin() + "/" } className="input-group-addon">{ utils.getWindowLocationOrigin() + "/" }</span> - <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.name} autoFocus={true} onFocus={this.handleFocus}/> + <img className='signup-team-logo' src='/static/images/logo.png' /> + <h2>{utils.toTitleCase(strings.Team) + ' URL'}</h2> + <div className={nameDivClass}> + <div className='row'> + <div className='col-sm-11'> + <div className='input-group input-group--limit'> + <span data-toggle='tooltip' title={utils.getWindowLocationOrigin() + '/'} className='input-group-addon'>{utils.getWindowLocationOrigin() + '/'}</span> + <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' defaultValue={this.props.state.team.name} autoFocus={true} onFocus={this.handleFocus}/> + </div> + </div> </div> + {nameError} </div> - </div> - { name_error } - </div> - <p>{"Choose the web address of your new " + strings.Team + ":"}</p> - <ul className="color--light"> - <li>Short and memorable is best</li> - <li>Use lowercase letters, numbers and dashes</li> - <li>Must start with a letter and can't end in a dash</li> - </ul> - <button type="submit" className="btn btn-primary margin--extra" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button> - <div className="margin--extra"> - <a href="#" onClick={this.submitBack}>Back to previous step</a> - </div> - </form> + <p>{'Choose the web address of your new ' + strings.Team + ':'}</p> + <ul className='color--light'> + <li>Short and memorable is best</li> + <li>Use lowercase letters, numbers and dashes</li> + <li>Must start with a letter and can't end in a dash</li> + </ul> + <button type='submit' className='btn btn-primary margin--extra' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button> + <div className='margin--extra'> + <a href='#' onClick={this.submitBack}>Back to previous step</a> + </div> + </form> </div> ); } @@ -289,14 +316,14 @@ TeamURLPage = React.createClass({ AllowedDomainsPage = React.createClass({ submitBack: function (e) { e.preventDefault(); - this.props.state.wizard = "team_url"; + this.props.state.wizard = 'team_url'; this.props.updateParent(this.props.state); }, submitNext: function (e) { e.preventDefault(); if (this.refs.open_network.getDOMNode().checked) { - this.props.state.wizard = "send_invites"; + this.props.state.wizard = 'send_invites'; this.props.state.team.type = 'O'; this.props.updateParent(this.props.state); return; @@ -304,65 +331,72 @@ AllowedDomainsPage = React.createClass({ if (this.refs.allow.getDOMNode().checked) { var name = this.refs.name.getDOMNode().value.trim(); - var domainRegex = /^\w+\.\w+$/ + var domainRegex = /^\w+\.\w+$/; if (!name) { - this.setState({name_error: "This field is required"}); + this.setState({nameError: 'This field is required'}); return; } - if(!name.trim().match(domainRegex)) { - this.setState({name_error: "The domain doesn't appear valid"}); + if (!name.trim().match(domainRegex)) { + this.setState({nameError: 'The domain doesn\'t appear valid'}); return; } - this.props.state.wizard = "send_invites"; + this.props.state.wizard = 'send_invites'; this.props.state.team.allowed_domains = name; this.props.state.team.type = 'I'; this.props.updateParent(this.props.state); - } - else { - this.props.state.wizard = "send_invites"; + } else { + this.props.state.wizard = 'send_invites'; this.props.state.team.type = 'I'; this.props.updateParent(this.props.state); } }, getInitialState: function() { - return { }; + return {}; }, render: function() { - client.track('signup', 'signup_team_04_allow_domains'); - var name_error = this.state.name_error ? <label className="control-label">{ this.state.name_error }</label> : null; + var nameError = null; + var nameDivClass = 'form-group'; + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameDivClass += ' has-error'; + } return ( <div> <form> - <img className="signup-team-logo" src="/static/images/logo.png" /> - <h2>Email Domain</h2> - <p> - <div className="checkbox"><label><input type="checkbox" ref="allow" defaultChecked />{" Allow sign up and " + strings.Team + " discovery with a " + strings.Company + " email address."}</label></div> - </p> - <p>{"Check this box to allow your " + strings.Team + " members to sign up using their " + strings.Company + " email addresses if you share the same domain--otherwise, you need to invite everyone yourself."}</p> - <h4>{"Your " + strings.Team + "'s domain for emails"}</h4> - <div className={ name_error ? "form-group has-error" : "form-group" }> - <div className="row"> - <div className="col-sm-9"> - <div className="input-group"> - <span className="input-group-addon">@</span> - <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.allowed_domains} autoFocus={true} onFocus={this.handleFocus}/> + <img className='signup-team-logo' src='/static/images/logo.png' /> + <h2>Email Domain</h2> + <p> + <div className='checkbox'> + <label><input type='checkbox' ref='allow' defaultChecked />{' Allow sign up and ' + strings.Team + ' discovery with a ' + strings.Company + ' email address.'}</label> + </div> + </p> + <p>{'Check this box to allow your ' + strings.Team + ' members to sign up using their ' + strings.Company + ' email addresses if you share the same domain--otherwise, you need to invite everyone yourself.'}</p> + <h4>{'Your ' + strings.Team + '\'s domain for emails'}</h4> + <div className={nameDivClass}> + <div className='row'> + <div className='col-sm-9'> + <div className='input-group'> + <span className='input-group-addon'>@</span> + <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' defaultValue={this.props.state.team.allowed_domains} autoFocus={true} onFocus={this.handleFocus}/> + </div> + </div> </div> + {nameError} </div> - </div> - { name_error } - </div> - <p>To allow signups from multiple domains, separate each with a comma.</p> - <p> - <div className="checkbox"><label><input type="checkbox" ref="open_network" defaultChecked={this.props.state.team.type == 'O'} /> Allow anyone to signup to this domain without an invitation.</label></div> - </p> - <button type="button" className="btn btn-default" onClick={this.submitBack}><i className="glyphicon glyphicon-chevron-left"></i> Back</button> - <button type="submit" className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button> - </form> + <p>To allow signups from multiple domains, separate each with a comma.</p> + <p> + <div className='checkbox'> + <label><input type='checkbox' ref='open_network' defaultChecked={this.props.state.team.type === 'O'} /> Allow anyone to signup to this domain without an invitation.</label> + </div> + </p> + <button type='button' className='btn btn-default' onClick={this.submitBack}><i className='glyphicon glyphicon-chevron-left'></i> Back</button> + <button type='submit' className='btn-primary btn' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button> + </form> </div> ); } @@ -370,10 +404,10 @@ AllowedDomainsPage = React.createClass({ EmailItem = React.createClass({ getInitialState: function() { - return { }; + return {}; }, getValue: function() { - return this.refs.email.getDOMNode().value.trim() + return this.refs.email.getDOMNode().value.trim(); }, validate: function(teamEmail) { var email = this.refs.email.getDOMNode().value.trim().toLowerCase(); @@ -383,43 +417,44 @@ EmailItem = React.createClass({ } if (!utils.isEmail(email)) { - this.state.email_error = "Please enter a valid email address"; + this.state.emailError = 'Please enter a valid email address'; this.setState(this.state); return false; - } - else if (email === teamEmail) { - this.state.email_error = "Please use a different email than the one used at signup"; + } else if (email === teamEmail) { + this.state.emailError = 'Please use a different email than the one used at signup'; this.setState(this.state); return false; - } - else { - this.state.email_error = ""; + } else { + this.state.emailError = ''; this.setState(this.state); return true; } }, render: function() { - - var email_error = this.state.email_error ? <label className="control-label">{ this.state.email_error }</label> : null; + var emailError = null; + var emailDivClass = 'form-group'; + if (this.state.emailError) { + emailError = <label className='control-label'>{ this.state.emailError }</label>; + emailDivClass += ' has-error'; + } return ( - <div className={ email_error ? "form-group has-error" : "form-group" }> - <input autoFocus={this.props.focus} type="email" ref="email" className="form-control" placeholder="Email Address" defaultValue={this.props.email} maxLength="128" /> - { email_error } + <div className={emailDivClass}> + <input autoFocus={this.props.focus} type='email' ref='email' className='form-control' placeholder='Email Address' defaultValue={this.props.email} maxLength='128' /> + {emailError} </div> ); } }); - SendInivtesPage = React.createClass({ submitBack: function (e) { e.preventDefault(); if (config.AllowSignupDomainsWizard) { - this.props.state.wizard = "allowed_domains"; + this.props.state.wizard = 'allowed_domains'; } else { - this.props.state.wizard = "team_url"; + this.props.state.wizard = 'team_url'; } this.props.updateParent(this.props.state); @@ -428,69 +463,93 @@ SendInivtesPage = React.createClass({ e.preventDefault(); var valid = true; - var emails = []; - for (var i = 0; i < this.props.state.invites.length; i++) { - if (!this.refs['email_' + i].validate(this.props.state.team.email)) { - valid = false; - } else { - emails.push(this.refs['email_' + i].getValue()); + if (this.state.emailEnabled) { + var emails = []; + + for (var i = 0; i < this.props.state.invites.length; i++) { + if (!this.refs['email_' + i].validate(this.props.state.team.email)) { + valid = false; + } else { + emails.push(this.refs['email_' + i].getValue()); + } } - } - if (!valid) { - return; + if (valid) { + this.props.state.invites = emails; + } } - this.props.state.wizard = "username"; - this.props.state.invites = emails; - this.props.updateParent(this.props.state); + if (valid) { + this.props.state.wizard = 'username'; + this.props.updateParent(this.props.state); + } }, submitAddInvite: function (e) { e.preventDefault(); - this.props.state.wizard = "send_invites"; - if (this.props.state.invites == null || this.props.state.invites.length == 0) { + this.props.state.wizard = 'send_invites'; + if (!this.props.state.invites) { this.props.state.invites = []; } - this.props.state.invites.push(""); + this.props.state.invites.push(''); this.props.updateParent(this.props.state); }, submitSkip: function (e) { e.preventDefault(); - this.props.state.wizard = "username"; + this.props.state.wizard = 'username'; this.props.updateParent(this.props.state); }, getInitialState: function() { - return { }; + return { + emailEnabled: !ConfigStore.getSettingAsBoolean('ByPassEmail', false) + }; }, render: function() { - client.track('signup', 'signup_team_05_send_invites'); - var name_error = this.state.name_error ? <label className="control-label">{ this.state.name_error }</label> : null; + var content = null; + var bottomContent = null; - var emails = []; + if (this.state.emailEnabled) { + var emails = []; - for (var i = 0; i < this.props.state.invites.length; i++) { - if (i == 0) { - emails.push(<EmailItem focus={true} key={i} ref={'email_' + i} email={this.props.state.invites[i]} />); - } else { - emails.push(<EmailItem focus={false} key={i} ref={'email_' + i} email={this.props.state.invites[i]} />); + for (var i = 0; i < this.props.state.invites.length; i++) { + if (i === 0) { + emails.push(<EmailItem focus={true} key={i} ref={'email_' + i} email={this.props.state.invites[i]} />); + } else { + emails.push(<EmailItem focus={false} key={i} ref={'email_' + i} email={this.props.state.invites[i]} />); + } } + + content = ( + <div> + {emails} + <div className='form-group text-right'><a href='#' onClick={this.submitAddInvite}>Add Invitation</a></div> + </div> + ); + + bottomContent = ( + <p className='color--light'>{'if you prefer, you can invite ' + strings.Team + ' members later'}<br /> and <a href='#' onClick={this.submitSkip}>skip this step</a> for now.</p> + ); + } else { + content = ( + <div className='form-group color--light'>Email is currently disabled for your team, and emails cannot be sent. Contact your system administrator to enable email and email invitations.</div> + ); } return ( <div> <form> - <img className="signup-team-logo" src="/static/images/logo.png" /> - <h2>{"Invite " + utils.toTitleCase(strings.Team) + " Members"}</h2> - { emails } - <div className="form-group text-right"><a href="#" onClick={this.submitAddInvite}>Add Invitation</a></div> - <div className="form-group"><button type="submit" className="btn-primary btn" onClick={this.submitNext}>Next<i className="glyphicon glyphicon-chevron-right"></i></button></div> + <img className='signup-team-logo' src='/static/images/logo.png' /> + <h2>{'Invite ' + utils.toTitleCase(strings.Team) + ' Members'}</h2> + {content} + <div className='form-group'> + <button type='submit' className='btn-primary btn' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button> + </div> </form> - <p className="color--light">{"if you prefer, you can invite " + strings.Team + " members later"}<br /> and <a href="#" onClick={this.submitSkip}>skip this step</a> for now.</p> - <div className="margin--extra"> - <a href="#" onClick={this.submitBack}>Back to previous step</a> + {bottomContent} + <div className='margin--extra'> + <a href='#' onClick={this.submitBack}>Back to previous step</a> </div> </div> ); @@ -508,12 +567,12 @@ UsernamePage = React.createClass({ var name = this.refs.name.getDOMNode().value.trim(); - var username_error = utils.isValidUsername(name); - if (username_error === 'Cannot use a reserved word as a username.') { - this.setState({name_error: 'This username is reserved, please choose a new one.'}); + var usernameError = utils.isValidUsername(name); + if (usernameError === 'Cannot use a reserved word as a username.') { + this.setState({nameError: 'This username is reserved, please choose a new one.'}); return; - } else if (username_error) { - this.setState({name_error: "Username must begin with a letter, and contain 3 to 15 characters in total, which may be numbers, lowercase letters, or any of the symbols '.', '-', or '_'"}); + } else if (usernameError) { + this.setState({nameError: 'Username must begin with a letter, and contain 3 to 15 characters in total, which may be numbers, lowercase letters, or any of the symbols \'.\', \'-\', or \'_\''}); return; } @@ -527,31 +586,36 @@ UsernamePage = React.createClass({ render: function() { client.track('signup', 'signup_team_06_username'); - var name_error = this.state.name_error ? <label className='control-label'>{this.state.name_error}</label> : null; + var nameError = null; + var nameDivClass = 'form-group'; + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameDivClass += ' has-error'; + } return ( <div> <form> - <img className='signup-team-logo' src='/static/images/logo.png' /> - <h2 className='margin--less'>Your username</h2> - <h5 className='color--light'>{'Select a memorable username that makes it easy for ' + strings.Team + 'mates to identify you:'}</h5> - <div className='inner__content margin--extra'> - <div className={name_error ? 'form-group has-error' : 'form-group'}> - <div className='row'> - <div className='col-sm-11'> - <h5><strong>Choose your username</strong></h5> - <input autoFocus={true} type='text' ref='name' className='form-control' placeholder='' defaultValue={this.props.state.user.username} maxLength='128' /> - <div className='color--light form__hint'>Usernames must begin with a letter and contain 3 to 15 characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'</div> + <img className='signup-team-logo' src='/static/images/logo.png' /> + <h2 className='margin--less'>Your username</h2> + <h5 className='color--light'>{'Select a memorable username that makes it easy for ' + strings.Team + 'mates to identify you:'}</h5> + <div className='inner__content margin--extra'> + <div className={nameDivClass}> + <div className='row'> + <div className='col-sm-11'> + <h5><strong>Choose your username</strong></h5> + <input autoFocus={true} type='text' ref='name' className='form-control' placeholder='' defaultValue={this.props.state.user.username} maxLength='128' /> + <div className='color--light form__hint'>Usernames must begin with a letter and contain 3 to 15 characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'</div> + </div> + </div> + {nameError} </div> </div> - {name_error} + <button type='submit' className='btn btn-primary margin--extra' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button> + <div className='margin--extra'> + <a href='#' onClick={this.submitBack}>Back to previous step</a> </div> - </div> - <button type='submit' className='btn btn-primary margin--extra' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button> - <div className='margin--extra'> - <a href='#' onClick={this.submitBack}>Back to previous step</a> - </div> - </form> + </form> </div> ); } @@ -560,7 +624,7 @@ UsernamePage = React.createClass({ PasswordPage = React.createClass({ submitBack: function (e) { e.preventDefault(); - this.props.state.wizard = "username"; + this.props.state.wizard = 'username'; this.props.updateParent(this.props.state); }, submitNext: function (e) { @@ -568,11 +632,11 @@ PasswordPage = React.createClass({ var password = this.refs.password.getDOMNode().value.trim(); if (!password || password.length < 5) { - this.setState({password_error: "Please enter at least 5 characters"}); + this.setState({passwordError: 'Please enter at least 5 characters'}); return; } - this.setState({password_error: null, server_error: null}); + this.setState({passwordError: null, serverError: null}); $('#finish-button').button('loading'); var teamSignup = JSON.parse(JSON.stringify(this.props.state)); teamSignup.user.password = password; @@ -582,13 +646,12 @@ PasswordPage = React.createClass({ client.createTeamFromSignup(teamSignup, function(data) { - client.track('signup', 'signup_team_08_complete'); var props = this.props; $('#sign-up-button').button('reset'); - props.state.wizard = "finished"; + props.state.wizard = 'finished'; props.updateParent(props.state, true); window.location.href = utils.getWindowLocationOrigin() + '/' + props.state.team.name + '/login?email=' + encodeURIComponent(teamSignup.team.email); @@ -601,55 +664,63 @@ PasswordPage = React.createClass({ // window.location.href = '/channels/town-square'; // }.bind(ctl), // function(err) { - // this.setState({name_error: err.message}); + // this.setState({nameError: err.message}); // }.bind(ctl) // ); }.bind(this), function(err) { - this.setState({server_error: err.message}); + this.setState({serverError: err.message}); $('#sign-up-button').button('reset'); }.bind(this) ); }, getInitialState: function() { - return { }; + return {}; }, render: function() { - client.track('signup', 'signup_team_07_password'); - var password_error = this.state.password_error ? <div className="form-group has-error"><label className="control-label">{ this.state.password_error }</label></div> : null; - var server_error = this.state.server_error ? <div className="form-group has-error"><label className="control-label">{ this.state.server_error }</label></div> : null; + var passwordError = null; + var passwordDivStyle = 'form-group'; + if (this.state.passwordError) { + passwordError = <div className='form-group has-error'><label className='control-label'>{this.state.passwordError}</label></div>; + passwordDivStyle = ' has-error'; + } + + var serverError = null; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } return ( <div> <form> - <img className="signup-team-logo" src="/static/images/logo.png" /> - <h2 className="margin--less">Your password</h2> - <h5 className="color--light">Select a password that you'll use to login with your email address:</h5> - <div className="inner__content margin--extra"> - <h5><strong>Email</strong></h5> - <div className="block--gray form-group">{this.props.state.team.email}</div> - <div className={ password_error ? "form-group has-error" : "form-group" }> - <div className="row"> - <div className="col-sm-11"> - <h5><strong>Choose your password</strong></h5> - <input autoFocus={true} type="password" ref="password" className="form-control" placeholder="" maxLength="128" /> - <div className="color--light form__hint">Passwords must contain 5 to 50 characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.</div> + <img className='signup-team-logo' src='/static/images/logo.png' /> + <h2 className='margin--less'>Your password</h2> + <h5 className='color--light'>Select a password that you'll use to login with your email address:</h5> + <div className='inner__content margin--extra'> + <h5><strong>Email</strong></h5> + <div className='block--gray form-group'>{this.props.state.team.email}</div> + <div className={passwordDivStyle}> + <div className='row'> + <div className='col-sm-11'> + <h5><strong>Choose your password</strong></h5> + <input autoFocus={true} type='password' ref='password' className='form-control' placeholder='' maxLength='128' /> + <div className='color--light form__hint'>Passwords must contain 5 to 50 characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.</div> + </div> + </div> + {passwordError} + {serverError} </div> </div> - { password_error } - { server_error } + <div className='form-group'> + <button type='submit' className='btn btn-primary margin--extra' id='finish-button' data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Creating ' + strings.Team + '...'} onClick={this.submitNext}>Finish</button> </div> - </div> - <div className="form-group"> - <button type="submit" className="btn btn-primary margin--extra" id="finish-button" data-loading-text={"<span class='glyphicon glyphicon-refresh glyphicon-refresh-animate'></span> Creating "+strings.Team+"..."} onClick={this.submitNext}>Finish</button> - </div> - <p>By proceeding to create your account and use { config.SiteName }, you agree to our <a href={ config.TermsLink }>Terms of Service</a> and <a href={ config.PrivacyLink }>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p> - <div className="margin--extra"> - <a href="#" onClick={this.submitBack}>Back to previous step</a> - </div> - </form> + <p>By proceeding to create your account and use {config.SiteName}, you agree to our <a href={config.TermsLink}>Terms of Service</a> and <a href={config.PrivacyLink}>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p> + <div className='margin--extra'> + <a href='#' onClick={this.submitBack}>Back to previous step</a> + </div> + </form> </div> ); } @@ -668,14 +739,14 @@ module.exports = React.createClass({ if (!props) { props = {}; - props.wizard = "welcome"; + props.wizard = 'welcome'; props.team = {}; props.team.email = this.props.email; - props.team.allowed_domains = ""; + props.team.allowed_domains = ''; props.invites = []; - props.invites.push(""); - props.invites.push(""); - props.invites.push(""); + props.invites.push(''); + props.invites.push(''); + props.invites.push(''); props.user = {}; props.hash = this.props.hash; props.data = this.props.data; @@ -684,36 +755,34 @@ module.exports = React.createClass({ return props; }, render: function() { - if (this.state.wizard == "welcome") { - return <WelcomePage state={this.state} updateParent={this.updateParent} /> + if (this.state.wizard === 'welcome') { + return <WelcomePage state={this.state} updateParent={this.updateParent} />; } - if (this.state.wizard == "team_display_name") { - return <TeamDisplayNamePage state={this.state} updateParent={this.updateParent} /> + if (this.state.wizard === 'team_display_name') { + return <TeamDisplayNamePage state={this.state} updateParent={this.updateParent} />; } - if (this.state.wizard == "team_url") { - return <TeamURLPage state={this.state} updateParent={this.updateParent} /> + if (this.state.wizard === 'team_url') { + return <TeamURLPage state={this.state} updateParent={this.updateParent} />; } - if (this.state.wizard == "allowed_domains") { - return <AllowedDomainsPage state={this.state} updateParent={this.updateParent} /> + if (this.state.wizard === 'allowed_domains') { + return <AllowedDomainsPage state={this.state} updateParent={this.updateParent} />; } - if (this.state.wizard == "send_invites") { - return <SendInivtesPage state={this.state} updateParent={this.updateParent} /> + if (this.state.wizard === 'send_invites') { + return <SendInivtesPage state={this.state} updateParent={this.updateParent} />; } - if (this.state.wizard == "username") { - return <UsernamePage state={this.state} updateParent={this.updateParent} /> + if (this.state.wizard === 'username') { + return <UsernamePage state={this.state} updateParent={this.updateParent} />; } - if (this.state.wizard == "password") { - return <PasswordPage state={this.state} updateParent={this.updateParent} /> + if (this.state.wizard === 'password') { + return <PasswordPage state={this.state} updateParent={this.updateParent} />; } return (<div>You've already completed the signup process for this invitation or this invitation has expired.</div>); } }); - - diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx index 66d8c9366..8f29bbe57 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -106,11 +106,11 @@ var NotificationsTab = React.createClass({ }, componentDidMount: function() { UserStore.addChangeListener(this._onChange); - $('#user_settings1').on('hidden.bs.modal', this.handleClose); + $('#user_settings').on('hidden.bs.modal', this.handleClose); }, componentWillUnmount: function() { UserStore.removeChangeListener(this._onChange); - $('#user_settings1').off('hidden.bs.modal', this.handleClose); + $('#user_settings').off('hidden.bs.modal', this.handleClose); this.props.updateSection(''); }, _onChange: function() { @@ -517,27 +517,34 @@ var SecurityTab = React.createClass({ this.setState({confirmPassword: e.target.value}); }, handleHistoryOpen: function() { - $('#user_settings1').modal('hide'); + this.setState({willReturn: true}); + $("#user_settings").modal('hide'); }, handleDevicesOpen: function() { - $('#user_settings1').modal('hide'); + this.setState({willReturn: true}); + $("#user_settings").modal('hide'); }, handleClose: function() { $(this.getDOMNode()).find('.form-control').each(function() { this.value = ''; }); this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); - this.props.updateTab('general'); + + if (!this.state.willReturn) { + this.props.updateTab('general'); + } else { + this.setState({willReturn: false}); + } }, componentDidMount: function() { - $('#user_settings1').on('hidden.bs.modal', this.handleClose); + $('#user_settings').on('hidden.bs.modal', this.handleClose); }, componentWillUnmount: function() { - $('#user_settings1').off('hidden.bs.modal', this.handleClose); + $('#user_settings').off('hidden.bs.modal', this.handleClose); this.props.updateSection(''); }, getInitialState: function() { - return {currentPassword: '', newPassword: '', confirmPassword: ''}; + return {currentPassword: '', newPassword: '', confirmPassword: '', willReturn: false}; }, render: function() { var serverError = this.state.serverError ? this.state.serverError : null; @@ -815,10 +822,10 @@ var GeneralTab = React.createClass({ this.props.updateSection(''); }, componentDidMount: function() { - $('#user_settings1').on('hidden.bs.modal', this.handleClose); + $('#user_settings').on('hidden.bs.modal', this.handleClose); }, componentWillUnmount: function() { - $('#user_settings1').off('hidden.bs.modal', this.handleClose); + $('#user_settings').off('hidden.bs.modal', this.handleClose); }, getInitialState: function() { var user = this.props.user; @@ -1097,7 +1104,7 @@ var AppearanceTab = React.createClass({ if (this.props.activeSection === "theme") { $(this.refs[this.state.theme].getDOMNode()).addClass('active-border'); } - $('#user_settings1').on('hidden.bs.modal', this.handleClose); + $('#user_settings').on('hidden.bs.modal', this.handleClose); }, componentDidUpdate: function() { if (this.props.activeSection === "theme") { @@ -1106,7 +1113,7 @@ var AppearanceTab = React.createClass({ } }, componentWillUnmount: function() { - $('#user_settings1').off('hidden.bs.modal', this.handleClose); + $('#user_settings').off('hidden.bs.modal', this.handleClose); this.props.updateSection(''); }, getInitialState: function() { diff --git a/web/react/components/user_settings_modal.jsx b/web/react/components/user_settings_modal.jsx index 702e7ad7a..7181c4020 100644 --- a/web/react/components/user_settings_modal.jsx +++ b/web/react/components/user_settings_modal.jsx @@ -32,7 +32,7 @@ module.exports = React.createClass({ tabs.push({name: "appearance", ui_name: "Appearance", icon: "glyphicon glyphicon-wrench"}); return ( - <div className="modal fade" ref="modal" id="user_settings1" role="dialog" tabIndex="-1" aria-hidden="true"> + <div className="modal fade" ref="modal" id="user_settings" role="dialog" tabIndex="-1" aria-hidden="true"> <div className="modal-dialog settings-modal"> <div className="modal-content"> <div className="modal-header"> @@ -64,4 +64,3 @@ module.exports = React.createClass({ ); } }); - diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index dc85b53e5..2b7f64030 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -6,6 +6,13 @@ var utils = require('../utils/utils.jsx'); module.exports = React.createClass({ displayName: 'ViewImageModal', + propTypes: { + filenames: React.PropTypes.array, + modalId: React.PropTypes.string, + channelId: React.PropTypes.string, + userId: React.PropTypes.string, + startId: React.PropTypes.number + }, canSetState: false, handleNext: function() { var id = this.state.imgId + 1; @@ -56,8 +63,8 @@ module.exports = React.createClass({ progress[id] = img.completedPercentage; self.setState({progress: progress}); }); - img.onload = function(imgid) { - return function() { + img.onload = function onload(imgid) { + return function onloadReturn() { var loaded = self.state.loaded; loaded[imgid] = true; self.setState({loaded: loaded}); @@ -83,21 +90,21 @@ module.exports = React.createClass({ }, componentDidMount: function() { var self = this; - $('#' + this.props.modalId).on('shown.bs.modal', function() { + $('#' + this.props.modalId).on('shown.bs.modal', function onModalShow() { self.setState({viewed: true}); self.loadImage(self.state.imgId); }); - $(this.refs.modal.getDOMNode()).click(function(e) { + $(this.refs.modal.getDOMNode()).click(function onModalClick(e) { if (e.target === this || e.target === self.refs.imageBody.getDOMNode()) { $('.image_modal').modal('hide'); } }); $(this.refs.imageWrap.getDOMNode()).hover( - function() { + function onModalHover() { $(self.refs.imageFooter.getDOMNode()).addClass('footer--show'); - }, function() { + }, function offModalHover() { $(self.refs.imageFooter.getDOMNode()).removeClass('footer--show'); } ); @@ -117,10 +124,14 @@ module.exports = React.createClass({ data.user_id = this.props.userId; data.filename = this.props.filenames[this.state.imgId]; Client.getPublicLink(data, - function(serverData) { - window.open(serverData.public_link); + function sucess(serverData) { + if (utils.isMobile()) { + window.location.href = serverData.public_link; + } else { + window.open(serverData.public_link); + } }, - function() { + function error() { } ); }, @@ -145,7 +156,7 @@ module.exports = React.createClass({ getInitialState: function() { var loaded = []; var progress = []; - for (var i = 0; i < this.props.filenames.length; i ++) { + for (var i = 0; i < this.props.filenames.length; i++) { loaded.push(false); progress.push(0); } @@ -198,7 +209,7 @@ module.exports = React.createClass({ if (!(filename in this.state.fileSizes)) { var self = this; - utils.getFileSize(utils.getFileUrl(filename), function(fileSize) { + utils.getFileSize(utils.getFileUrl(filename), function fileSizeOp(fileSize) { if (self.canSetState) { var fileSizes = self.state.fileSizes; fileSizes[filename] = fileSize; @@ -210,14 +221,20 @@ module.exports = React.createClass({ } else { // display a progress indicator when the preview for an image is still loading var percentage = Math.floor(this.state.progress[this.state.imgId]); - content = ( - <div> - <img className='loader-image' src='/static/images/load.gif' /> - { percentage > 0 ? - <span className='loader-percent' >{'Previewing ' + percentage + '%'}</span> - : ''} - </div> - ); + if (percentage) { + content = ( + <div> + <img className='loader-image' src='/static/images/load.gif' /> + <span className='loader-percent' >{'Previewing ' + percentage + '%'}</span> + </div> + ); + } else { + content = ( + <div> + <img className='loader-image' src='/static/images/load.gif' /> + </div> + ); + } bgClass = 'black-bg'; } @@ -256,7 +273,7 @@ module.exports = React.createClass({ <div className='modal-close' data-dismiss='modal'></div> {content} <div ref='imageFooter' className='modal-button-bar'> - <span className='pull-left text'>{'Image ' + (this.state.imgId + 1) + ' of ' + this.props.filenames.length}</span> + <span className='pull-left text'>{'File ' + (this.state.imgId + 1) + ' of ' + this.props.filenames.length}</span> <div className='image-links'> {publicLink} <a href={fileUrl} download={name} className='text'>Download</a> diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 90d90b29f..929499715 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -1,7 +1,6 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Navbar = require('../components/navbar.jsx'); var Sidebar = require('../components/sidebar.jsx'); @@ -36,21 +35,23 @@ var AccessHistoryModal = require('../components/access_history_modal.jsx'); var ActivityLogModal = require('../components/activity_log_modal.jsx'); var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx') +var AsyncClient = require('../utils/async_client.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; global.window.setup_channel_page = function(team_name, team_type, team_id, channel_name, channel_id) { + AsyncClient.getConfig(); AppDispatcher.handleViewAction({ - type: ActionTypes.CLICK_CHANNEL, - name: channel_name, - id: channel_id + type: ActionTypes.CLICK_CHANNEL, + name: channel_name, + id: channel_id }); AppDispatcher.handleViewAction({ - type: ActionTypes.CLICK_TEAM, - id: team_id + type: ActionTypes.CLICK_TEAM, + id: team_id }); React.render( @@ -99,7 +100,7 @@ global.window.setup_channel_page = function(team_name, team_type, team_id, chann ); React.render( - <MemberInviteModal />, + <MemberInviteModal teamType={team_type} />, document.getElementById('invite_member_modal') ); @@ -194,17 +195,17 @@ global.window.setup_channel_page = function(team_name, team_type, team_id, chann ); React.render( - <MentionList id="post_textbox" />, + <MentionList id='post_textbox' />, document.getElementById('post_mention_tab') ); React.render( - <MentionList id="reply_textbox" />, + <MentionList id='reply_textbox' />, document.getElementById('reply_mention_tab') ); React.render( - <MentionList id="edit_textbox" />, + <MentionList id='edit_textbox' />, document.getElementById('edit_mention_tab') ); diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx index e982f5a79..37c441d4f 100644 --- a/web/react/pages/signup_team.jsx +++ b/web/react/pages/signup_team.jsx @@ -1,11 +1,15 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var SignupTeam =require('../components/signup_team.jsx'); +var SignupTeam = require('../components/signup_team.jsx'); + +var AsyncClient = require('../utils/async_client.jsx'); global.window.setup_signup_team_page = function() { + AsyncClient.getConfig(); + React.render( <SignupTeam />, document.getElementById('signup-team') ); -};
\ No newline at end of file +}; diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index a97f13391..f7c23841c 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -10,212 +10,264 @@ var ActionTypes = Constants.ActionTypes; var BrowserStore = require('../stores/browser_store.jsx'); - var CHANGE_EVENT = 'change'; var MORE_CHANGE_EVENT = 'change'; var EXTRA_INFO_EVENT = 'extra_info'; var ChannelStore = assign({}, EventEmitter.prototype, { - _current_id: null, - emitChange: function() { - this.emit(CHANGE_EVENT); - }, - addChangeListener: function(callback) { - this.on(CHANGE_EVENT, callback); - }, - removeChangeListener: function(callback) { - this.removeListener(CHANGE_EVENT, callback); - }, - emitMoreChange: function() { - this.emit(MORE_CHANGE_EVENT); - }, - addMoreChangeListener: function(callback) { - this.on(MORE_CHANGE_EVENT, callback); - }, - removeMoreChangeListener: function(callback) { - this.removeListener(MORE_CHANGE_EVENT, callback); - }, - emitExtraInfoChange: function() { - this.emit(EXTRA_INFO_EVENT); - }, - addExtraInfoChangeListener: function(callback) { - this.on(EXTRA_INFO_EVENT, callback); - }, - removeExtraInfoChangeListener: function(callback) { - this.removeListener(EXTRA_INFO_EVENT, callback); - }, - findFirstBy: function(field, value) { - var channels = this._getChannels(); - for (var i = 0; i < channels.length; i++) { - if (channels[i][field] == value) { - return channels[i]; - } - } + currentId: null, + emitChange: function() { + this.emit(CHANGE_EVENT); + }, + addChangeListener: function(callback) { + this.on(CHANGE_EVENT, callback); + }, + removeChangeListener: function(callback) { + this.removeListener(CHANGE_EVENT, callback); + }, + emitMoreChange: function() { + this.emit(MORE_CHANGE_EVENT); + }, + addMoreChangeListener: function(callback) { + this.on(MORE_CHANGE_EVENT, callback); + }, + removeMoreChangeListener: function(callback) { + this.removeListener(MORE_CHANGE_EVENT, callback); + }, + emitExtraInfoChange: function() { + this.emit(EXTRA_INFO_EVENT); + }, + addExtraInfoChangeListener: function(callback) { + this.on(EXTRA_INFO_EVENT, callback); + }, + removeExtraInfoChangeListener: function(callback) { + this.removeListener(EXTRA_INFO_EVENT, callback); + }, + findFirstBy: function(field, value) { + var channels = this.pGetChannels(); + for (var i = 0; i < channels.length; i++) { + if (channels[i][field] === value) { + return channels[i]; + } + } - return null; - }, - get: function(id) { - return this.findFirstBy('id', id); - }, - getMember: function(id) { - return this.getAllMembers()[id]; - }, - getByName: function(name) { - return this.findFirstBy('name', name); - }, - getAll: function() { - return this._getChannels(); - }, - getAllMembers: function() { - return this._getChannelMembers(); - }, - getMoreAll: function() { - return this._getMoreChannels(); - }, - setCurrentId: function(id) { - this._current_id = id; - }, - setLastVisitedName: function(name) { - if (name == null) - BrowserStore.removeItem("last_visited_name"); - else - BrowserStore.setItem("last_visited_name", name); - }, - getLastVisitedName: function() { - return BrowserStore.getItem("last_visited_name"); - }, - resetCounts: function(id) { - var cm = this._getChannelMembers(); - for (var cmid in cm) { - if (cm[cmid].channel_id == id) { - var c = this.get(id); - if (c) { - cm[cmid].msg_count = this.get(id).total_msg_count; - cm[cmid].mention_count = 0; - } - break; - } - } - this._storeChannelMembers(cm); - }, - getCurrentId: function() { - return this._current_id; - }, - getCurrent: function() { - var currentId = this.getCurrentId(); - - if (currentId) - return this.get(currentId); - else - return null; - }, - getCurrentMember: function() { - var currentId = ChannelStore.getCurrentId(); - - if (currentId) - return this.getAllMembers()[currentId]; - else - return null; - }, - setChannelMember: function(member) { - var members = this._getChannelMembers(); - members[member.channel_id] = member; - this._storeChannelMembers(members); - this.emitChange(); - }, - getCurrentExtraInfo: function() { - var currentId = ChannelStore.getCurrentId(); - var extra = null; - - if (currentId) - extra = this._getExtraInfos()[currentId]; - - if (extra == null) - extra = {members: []}; - - return extra; - }, - getExtraInfo: function(channel_id) { - var extra = null; - - if (channel_id) - extra = this._getExtraInfos()[channel_id]; - - if (extra == null) - extra = {members: []}; - - return extra; - }, - _storeChannels: function(channels) { - BrowserStore.setItem("channels", channels); - }, - _getChannels: function() { - return BrowserStore.getItem("channels", []); - }, - _storeChannelMembers: function(channelMembers) { - BrowserStore.setItem("channel_members", channelMembers); - }, - _getChannelMembers: function() { - return BrowserStore.getItem("channel_members", {}); - }, - _storeMoreChannels: function(channels) { - BrowserStore.setItem("more_channels", channels); - }, - _getMoreChannels: function() { - var channels = BrowserStore.getItem("more_channels"); - - if (channels == null) { - channels = {}; - channels.loading = true; - } + return null; + }, + get: function(id) { + return this.findFirstBy('id', id); + }, + getMember: function(id) { + return this.getAllMembers()[id]; + }, + getByName: function(name) { + return this.findFirstBy('name', name); + }, + getAll: function() { + return this.pGetChannels(); + }, + getAllMembers: function() { + return this.pGetChannelMembers(); + }, + getMoreAll: function() { + return this.pGetMoreChannels(); + }, + setCurrentId: function(id) { + this.currentId = id; + }, + setLastVisitedName: function(name) { + if (name == null) { + BrowserStore.removeItem('last_visited_name'); + } else { + BrowserStore.setItem('last_visited_name', name); + } + }, + getLastVisitedName: function() { + return BrowserStore.getItem('last_visited_name'); + }, + resetCounts: function(id) { + var cm = this.pGetChannelMembers(); + for (var cmid in cm) { + if (cm[cmid].channel_id === id) { + var c = this.get(id); + if (c) { + cm[cmid].msg_count = this.get(id).total_msg_count; + cm[cmid].mention_count = 0; + } + break; + } + } + this.pStoreChannelMembers(cm); + }, + getCurrentId: function() { + return this.currentId; + }, + getCurrent: function() { + var currentId = this.getCurrentId(); + + if (currentId) { + return this.get(currentId); + } else { + return null; + } + }, + getCurrentMember: function() { + var currentId = ChannelStore.getCurrentId(); + + if (currentId) { + return this.getAllMembers()[currentId]; + } else { + return null; + } + }, + setChannelMember: function(member) { + var members = this.pGetChannelMembers(); + members[member.channel_id] = member; + this.pStoreChannelMembers(members); + this.emitChange(); + }, + getCurrentExtraInfo: function() { + var currentId = ChannelStore.getCurrentId(); + var extra = null; + + if (currentId) { + extra = this.pGetExtraInfos()[currentId]; + } + + if (extra == null) { + extra = {members: []}; + } + + return extra; + }, + getExtraInfo: function(channelId) { + var extra = null; + + if (channelId) { + extra = this.pGetExtraInfos()[channelId]; + } + + if (extra == null) { + extra = {members: []}; + } + + return extra; + }, + pStoreChannel: function(channel) { + var channels = this.pGetChannels(); + var found; + + for (var i = 0; i < channels.length; i++) { + if (channels[i].id === channel.id) { + channels[i] = channel; + found = true; + break; + } + } + + if (!found) { + channels.push(channel); + } - return channels; - }, - _storeExtraInfos: function(extraInfos) { - BrowserStore.setItem("extra_infos", extraInfos); - }, - _getExtraInfos: function() { - return BrowserStore.getItem("extra_infos", {}); - }, - isDefault: function(channel) { - return channel.name == Constants.DEFAULT_CHANNEL; - } + channels.sort(function chanSort(a, b) { + if (a.display_name.toLowerCase() < b.display_name.toLowerCase()) { + return -1; + } + if (a.display_name.toLowerCase() > b.display_name.toLowerCase()) { + return 1; + } + return 0; + }); + + this.pStoreChannels(channels); + }, + pStoreChannels: function(channels) { + BrowserStore.setItem('channels', channels); + }, + pGetChannels: function() { + return BrowserStore.getItem('channels', []); + }, + pStoreChannelMember: function(channelMember) { + var members = this.pGetChannelMembers(); + members[channelMember.channel_id] = channelMember; + this.pStoreChannelMembers(members); + }, + pStoreChannelMembers: function(channelMembers) { + BrowserStore.setItem('channel_members', channelMembers); + }, + pGetChannelMembers: function() { + return BrowserStore.getItem('channel_members', {}); + }, + pStoreMoreChannels: function(channels) { + BrowserStore.setItem('more_channels', channels); + }, + pGetMoreChannels: function() { + var channels = BrowserStore.getItem('more_channels'); + + if (channels == null) { + channels = {}; + channels.loading = true; + } + + return channels; + }, + pStoreExtraInfos: function(extraInfos) { + BrowserStore.setItem('extra_infos', extraInfos); + }, + pGetExtraInfos: function() { + return BrowserStore.getItem('extra_infos', {}); + }, + isDefault: function(channel) { + return channel.name === Constants.DEFAULT_CHANNEL; + } }); ChannelStore.dispatchToken = AppDispatcher.register(function(payload) { - var action = payload.action; - - switch(action.type) { - - case ActionTypes.CLICK_CHANNEL: - ChannelStore.setCurrentId(action.id); - ChannelStore.setLastVisitedName(action.name); - ChannelStore.resetCounts(action.id); - ChannelStore.emitChange(); - break; - - case ActionTypes.RECIEVED_CHANNELS: - ChannelStore._storeChannels(action.channels); - ChannelStore._storeChannelMembers(action.members); - var currentId = ChannelStore.getCurrentId(); - if (currentId) ChannelStore.resetCounts(currentId); - ChannelStore.emitChange(); - break; - - case ActionTypes.RECIEVED_MORE_CHANNELS: - ChannelStore._storeMoreChannels(action.channels); - ChannelStore.emitMoreChange(); - break; - - case ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO: - var extra_infos = ChannelStore._getExtraInfos(); - extra_infos[action.extra_info.id] = action.extra_info; - ChannelStore._storeExtraInfos(extra_infos); - ChannelStore.emitExtraInfoChange(); - break; - - default: - } + 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.emitChange(); + break; + + case ActionTypes.RECIEVED_CHANNELS: + ChannelStore.pStoreChannels(action.channels); + ChannelStore.pStoreChannelMembers(action.members); + currentId = ChannelStore.getCurrentId(); + if (currentId) { + ChannelStore.resetCounts(currentId); + } + ChannelStore.emitChange(); + break; + + case ActionTypes.RECIEVED_CHANNEL: + ChannelStore.pStoreChannel(action.channel); + ChannelStore.pStoreChannelMember(action.member); + currentId = ChannelStore.getCurrentId(); + if (currentId) { + ChannelStore.resetCounts(currentId); + } + ChannelStore.emitChange(); + break; + + case ActionTypes.RECIEVED_MORE_CHANNELS: + ChannelStore.pStoreMoreChannels(action.channels); + ChannelStore.emitMoreChange(); + break; + + case ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO: + var extraInfos = ChannelStore.pGetExtraInfos(); + extraInfos[action.extra_info.id] = action.extra_info; + ChannelStore.pStoreExtraInfos(extraInfos); + ChannelStore.emitExtraInfoChange(); + break; + + default: + } }); -module.exports = ChannelStore;
\ No newline at end of file +module.exports = ChannelStore; diff --git a/web/react/stores/config_store.jsx b/web/react/stores/config_store.jsx new file mode 100644 index 000000000..7ff177b35 --- /dev/null +++ b/web/react/stores/config_store.jsx @@ -0,0 +1,56 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +var EventEmitter = require('events').EventEmitter; +var assign = require('object-assign'); + +var BrowserStore = require('../stores/browser_store.jsx'); + +var Constants = require('../utils/constants.jsx'); +var ActionTypes = Constants.ActionTypes; + +var CHANGE_EVENT = 'change'; + +var ConfigStore = assign({}, EventEmitter.prototype, { + emitChange: function emitChange() { + this.emit(CHANGE_EVENT); + }, + addChangeListener: function addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + }, + removeChangeListener: function removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + }, + getSetting: function getSetting(key, defaultValue) { + return BrowserStore.getItem('config_' + key, defaultValue); + }, + getSettingAsBoolean: function getSettingAsNumber(key, defaultValue) { + var value = ConfigStore.getSetting(key, defaultValue); + + if (typeof value !== 'string') { + return !!value; + } else { + return value === 'true'; + } + }, + updateStoredSettings: function updateStoredSettings(settings) { + for (var key in settings) { + BrowserStore.setItem('config_' + key, settings[key]); + } + } +}); + +ConfigStore.dispatchToken = AppDispatcher.register(function registry(payload) { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECIEVED_CONFIG: + ConfigStore.updateStoredSettings(action.settings); + ConfigStore.emitChange(); + break; + default: + } +}); + +module.exports = ConfigStore; diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx index e6380d19e..3f2248c44 100644 --- a/web/react/stores/team_store.jsx +++ b/web/react/stores/team_store.jsx @@ -13,89 +13,94 @@ var CHANGE_EVENT = 'change'; var utils; function getWindowLocationOrigin() { - if (!utils) utils = require('../utils/utils.jsx'); + if (!utils) { + utils = require('../utils/utils.jsx'); + } return utils.getWindowLocationOrigin(); } var TeamStore = assign({}, EventEmitter.prototype, { - emitChange: function() { - this.emit(CHANGE_EVENT); - }, - addChangeListener: function(callback) { - this.on(CHANGE_EVENT, callback); - }, - removeChangeListener: function(callback) { - this.removeListener(CHANGE_EVENT, callback); - }, - get: function(id) { - var c = this._getTeams(); - return c[id]; - }, - getByName: function(name) { - var current = null; - var t = this._getTeams(); + emitChange: function() { + this.emit(CHANGE_EVENT); + }, + addChangeListener: function(callback) { + this.on(CHANGE_EVENT, callback); + }, + removeChangeListener: function(callback) { + this.removeListener(CHANGE_EVENT, callback); + }, + get: function(id) { + var c = this.pGetTeams(); + return c[id]; + }, + getByName: function(name) { + var t = this.pGetTeams(); - for (id in t) { - if (t[id].name == name) { - return t[id]; + for (var id in t) { + if (t[id].name === name) { + return t[id]; + } } - } - return null; - }, - getAll: function() { - return this._getTeams(); - }, - setCurrentId: function(id) { - if (id == null) - BrowserStore.removeItem("current_team_id"); - else - BrowserStore.setItem("current_team_id", id); - }, - getCurrentId: function() { - return BrowserStore.getItem("current_team_id"); - }, - getCurrent: function() { - var currentId = TeamStore.getCurrentId(); + return null; + }, + getAll: function() { + return this.pGetTeams(); + }, + setCurrentId: function(id) { + if (id === null) { + BrowserStore.removeItem('current_team_id'); + } else { + BrowserStore.setItem('current_team_id', id); + } + }, + getCurrentId: function() { + return BrowserStore.getItem('current_team_id'); + }, + getCurrent: function() { + var currentId = TeamStore.getCurrentId(); - if (currentId != null) - return this.get(currentId); - else - return null; - }, - getCurrentTeamUrl: function() { - return getWindowLocationOrigin() + "/" + this.getCurrent().name; - }, - storeTeam: function(team) { - var teams = this._getTeams(); - teams[team.id] = team; - this._storeTeams(teams); - }, - _storeTeams: function(teams) { - BrowserStore.setItem("user_teams", teams); - }, - _getTeams: function() { - return BrowserStore.getItem("user_teams", {}); - } + if (currentId !== null) { + return this.get(currentId); + } + return null; + }, + getCurrentTeamUrl: function() { + if (this.getCurrent()) { + return getWindowLocationOrigin() + '/' + this.getCurrent().name; + } + return null; + }, + storeTeam: function(team) { + var teams = this.pGetTeams(); + teams[team.id] = team; + this.pStoreTeams(teams); + }, + pStoreTeams: function(teams) { + BrowserStore.setItem('user_teams', teams); + }, + pGetTeams: function() { + return BrowserStore.getItem('user_teams', {}); + } }); -TeamStore.dispatchToken = AppDispatcher.register(function(payload) { - var action = payload.action; +TeamStore.dispatchToken = AppDispatcher.register(function registry(payload) { + var action = payload.action; - switch(action.type) { + switch (action.type) { - case ActionTypes.CLICK_TEAM: - TeamStore.setCurrentId(action.id); - TeamStore.emitChange(); - break; + case ActionTypes.CLICK_TEAM: + TeamStore.setCurrentId(action.id); + TeamStore.emitChange(); + break; - case ActionTypes.RECIEVED_TEAM: - TeamStore.storeTeam(action.team); - TeamStore.emitChange(); - break; + case ActionTypes.RECIEVED_TEAM: + TeamStore.storeTeam(action.team); + TeamStore.emitChange(); + break; - default: - } + default: + } }); module.exports = TeamStore; diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index f35b0f6cc..0b87bbd7b 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -4,6 +4,7 @@ var client = require('./client.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); +var ConfigStore = require('../stores/config_store.jsx'); var PostStore = require('../stores/post_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var utils = require('./utils.jsx'); @@ -14,100 +15,171 @@ var ActionTypes = Constants.ActionTypes; // Used to track in progress async calls var callTracker = {}; -var dispatchError = function(err, method) { +function dispatchError(err, method) { AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_ERROR, err: err, method: method }); -}; +} +module.exports.dispatchError = dispatchError; -var isCallInProgress = function(callName) { - if (!(callName in callTracker)) return false; +function isCallInProgress(callName) { + if (!(callName in callTracker)) { + return false; + } - if (callTracker[callName] === 0) return false; + if (callTracker[callName] === 0) { + return false; + } if (utils.getTimestamp() - callTracker[callName] > 5000) { - console.log("AsyncClient call " + callName + " expired after more than 5 seconds"); + console.log('AsyncClient call ' + callName + ' expired after more than 5 seconds'); return false; } return true; -}; +} -module.exports.dispatchError = dispatchError; +function getChannels(force, updateLastViewed, checkVersion) { + var channels = ChannelStore.getAll(); -module.exports.getChannels = function(force, updateLastViewed, checkVersion) { - if (isCallInProgress("getChannels")) return; + if (channels.length === 0 || force) { + if (isCallInProgress('getChannels')) { + return; + } + + callTracker.getChannels = utils.getTimestamp(); - if (ChannelStore.getAll().length == 0 || force) { - callTracker["getChannels"] = utils.getTimestamp(); client.getChannels( function(data, textStatus, xhr) { - callTracker["getChannels"] = 0; - - if (updateLastViewed && ChannelStore.getCurrentId() != null) { - module.exports.updateLastViewedAt(); - } + callTracker.getChannels = 0; if (checkVersion) { - var serverVersion = xhr.getResponseHeader("X-Version-ID"); + var serverVersion = xhr.getResponseHeader('X-Version-ID'); if (!UserStore.getLastVersion()) { UserStore.setLastVersion(serverVersion); } - if (serverVersion != UserStore.getLastVersion()) { + if (serverVersion !== UserStore.getLastVersion()) { UserStore.setLastVersion(serverVersion); window.location.href = window.location.href; - console.log("Detected version update refreshing the page"); + console.log('Detected version update refreshing the page'); } } - if (xhr.status === 304 || !data) return; + if (xhr.status === 304 || !data) { + return; + } AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_CHANNELS, channels: data.channels, members: data.members }); + }, + function(err) { + callTracker.getChannels = 0; + dispatchError(err, 'getChannels'); + } + ); + } else { + if (isCallInProgress('getChannelCounts')) { + return; + } + + callTracker.getChannelCounts = utils.getTimestamp(); + + client.getChannelCounts( + function(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) { + 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(err) { - callTracker["getChannels"] = 0; - dispatchError(err, "getChannels"); + callTracker.getChannelCounts = 0; + dispatchError(err, 'getChannelCounts'); } ); } + + if (updateLastViewed && ChannelStore.getCurrentId() != null) { + module.exports.updateLastViewedAt(); + } } +module.exports.getChannels = getChannels; + +function getChannel(id) { + if (isCallInProgress('getChannel' + id)) { + return; + } + + callTracker['getChannel' + id] = utils.getTimestamp(); + + client.getChannel(id, + function(data, textStatus, xhr) { + callTracker['getChannel' + id] = 0; + + if (xhr.status === 304 || !data) { + return; + } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_CHANNEL, + channel: data.channel, + member: data.member + }); + }, + function(err) { + callTracker['getChannel' + id] = 0; + dispatchError(err, 'getChannel'); + } + ); +} +module.exports.getChannel = getChannel; module.exports.updateLastViewedAt = function() { - if (isCallInProgress("updateLastViewed")) return; + if (isCallInProgress('updateLastViewed')) return; if (ChannelStore.getCurrentId() == null) return; - callTracker["updateLastViewed"] = utils.getTimestamp(); + callTracker['updateLastViewed'] = utils.getTimestamp(); client.updateLastViewedAt( ChannelStore.getCurrentId(), function(data) { - callTracker["updateLastViewed"] = 0; + callTracker['updateLastViewed'] = 0; }, function(err) { - callTracker["updateLastViewed"] = 0; - dispatchError(err, "updateLastViewedAt"); + callTracker['updateLastViewed'] = 0; + dispatchError(err, 'updateLastViewedAt'); } ); } module.exports.getMoreChannels = function(force) { - if (isCallInProgress("getMoreChannels")) return; + if (isCallInProgress('getMoreChannels')) return; if (ChannelStore.getMoreAll().loading || force) { - callTracker["getMoreChannels"] = utils.getTimestamp(); + callTracker['getMoreChannels'] = utils.getTimestamp(); client.getMoreChannels( function(data, textStatus, xhr) { - callTracker["getMoreChannels"] = 0; + callTracker['getMoreChannels'] = 0; if (xhr.status === 304 || !data) return; @@ -118,8 +190,8 @@ module.exports.getMoreChannels = function(force) { }); }, function(err) { - callTracker["getMoreChannels"] = 0; - dispatchError(err, "getMoreChannels"); + callTracker['getMoreChannels'] = 0; + dispatchError(err, 'getMoreChannels'); } ); } @@ -129,15 +201,15 @@ module.exports.getChannelExtraInfo = function(force) { var channelId = ChannelStore.getCurrentId(); if (channelId != null) { - if (isCallInProgress("getChannelExtraInfo_"+channelId)) return; + if (isCallInProgress('getChannelExtraInfo_'+channelId)) return; var minMembers = ChannelStore.getCurrent() && ChannelStore.getCurrent().type === 'D' ? 1 : 0; if (ChannelStore.getCurrentExtraInfo().members.length <= minMembers || force) { - callTracker["getChannelExtraInfo_"+channelId] = utils.getTimestamp(); + callTracker['getChannelExtraInfo_'+channelId] = utils.getTimestamp(); client.getChannelExtraInfo( channelId, function(data, textStatus, xhr) { - callTracker["getChannelExtraInfo_"+channelId] = 0; + callTracker['getChannelExtraInfo_'+channelId] = 0; if (xhr.status === 304 || !data) return; @@ -147,8 +219,8 @@ module.exports.getChannelExtraInfo = function(force) { }); }, function(err) { - callTracker["getChannelExtraInfo_"+channelId] = 0; - dispatchError(err, "getChannelExtraInfo"); + callTracker['getChannelExtraInfo_'+channelId] = 0; + dispatchError(err, 'getChannelExtraInfo'); } ); } @@ -156,12 +228,12 @@ module.exports.getChannelExtraInfo = function(force) { } module.exports.getProfiles = function() { - if (isCallInProgress("getProfiles")) return; + if (isCallInProgress('getProfiles')) return; - callTracker["getProfiles"] = utils.getTimestamp(); + callTracker['getProfiles'] = utils.getTimestamp(); client.getProfiles( function(data, textStatus, xhr) { - callTracker["getProfiles"] = 0; + callTracker['getProfiles'] = 0; if (xhr.status === 304 || !data) return; @@ -171,20 +243,20 @@ module.exports.getProfiles = function() { }); }, function(err) { - callTracker["getProfiles"] = 0; - dispatchError(err, "getProfiles"); + callTracker['getProfiles'] = 0; + dispatchError(err, 'getProfiles'); } ); } module.exports.getSessions = function() { - if (isCallInProgress("getSessions")) return; + if (isCallInProgress('getSessions')) return; - callTracker["getSessions"] = utils.getTimestamp(); + callTracker['getSessions'] = utils.getTimestamp(); client.getSessions( UserStore.getCurrentId(), function(data, textStatus, xhr) { - callTracker["getSessions"] = 0; + callTracker['getSessions'] = 0; if (xhr.status === 304 || !data) return; @@ -194,20 +266,20 @@ module.exports.getSessions = function() { }); }, function(err) { - callTracker["getSessions"] = 0; - dispatchError(err, "getSessions"); + callTracker['getSessions'] = 0; + dispatchError(err, 'getSessions'); } ); } module.exports.getAudits = function() { - if (isCallInProgress("getAudits")) return; + if (isCallInProgress('getAudits')) return; - callTracker["getAudits"] = utils.getTimestamp(); + callTracker['getAudits'] = utils.getTimestamp(); client.getAudits( UserStore.getCurrentId(), function(data, textStatus, xhr) { - callTracker["getAudits"] = 0; + callTracker['getAudits'] = 0; if (xhr.status === 304 || !data) return; @@ -217,22 +289,22 @@ module.exports.getAudits = function() { }); }, function(err) { - callTracker["getAudits"] = 0; - dispatchError(err, "getAudits"); + callTracker['getAudits'] = 0; + dispatchError(err, 'getAudits'); } ); } module.exports.findTeams = function(email) { - if (isCallInProgress("findTeams_"+email)) return; + if (isCallInProgress('findTeams_'+email)) return; var user = UserStore.getCurrentUser(); if (user) { - callTracker["findTeams_"+email] = utils.getTimestamp(); + callTracker['findTeams_'+email] = utils.getTimestamp(); client.findTeams( user.email, function(data, textStatus, xhr) { - callTracker["findTeams_"+email] = 0; + callTracker['findTeams_'+email] = 0; if (xhr.status === 304 || !data) return; @@ -242,21 +314,21 @@ module.exports.findTeams = function(email) { }); }, function(err) { - callTracker["findTeams_"+email] = 0; - dispatchError(err, "findTeams"); + callTracker['findTeams_'+email] = 0; + dispatchError(err, 'findTeams'); } ); } } module.exports.search = function(terms) { - if (isCallInProgress("search_"+String(terms))) return; + if (isCallInProgress('search_'+String(terms))) return; - callTracker["search_"+String(terms)] = utils.getTimestamp(); + callTracker['search_'+String(terms)] = utils.getTimestamp(); client.search( terms, function(data, textStatus, xhr) { - callTracker["search_"+String(terms)] = 0; + callTracker['search_'+String(terms)] = 0; if (xhr.status === 304 || !data) return; @@ -266,8 +338,8 @@ module.exports.search = function(terms) { }); }, function(err) { - callTracker["search_"+String(terms)] = 0; - dispatchError(err, "search"); + callTracker['search_'+String(terms)] = 0; + dispatchError(err, 'search'); } ); } @@ -276,7 +348,7 @@ module.exports.getPosts = function(force, id, maxPosts) { if (PostStore.getCurrentPosts() == null || force) { var channelId = id ? id : ChannelStore.getCurrentId(); - if (isCallInProgress("getPosts_"+channelId)) return; + if (isCallInProgress('getPosts_'+channelId)) return; var post_list = PostStore.getCurrentPosts(); @@ -291,7 +363,7 @@ module.exports.getPosts = function(force, id, maxPosts) { } if (channelId != null) { - callTracker["getPosts_"+channelId] = utils.getTimestamp(); + callTracker['getPosts_'+channelId] = utils.getTimestamp(); client.getPosts( channelId, 0, @@ -308,23 +380,25 @@ module.exports.getPosts = function(force, id, maxPosts) { module.exports.getProfiles(); }, function(err) { - dispatchError(err, "getPosts"); + dispatchError(err, 'getPosts'); }, function() { - callTracker["getPosts_"+channelId] = 0; + callTracker['getPosts_'+channelId] = 0; } ); } } } -module.exports.getMe = function() { - if (isCallInProgress("getMe")) return; +function getMe() { + if (isCallInProgress('getMe')) { + return; + } - callTracker["getMe"] = utils.getTimestamp(); + callTracker.getMe = utils.getTimestamp(); client.getMe( function(data, textStatus, xhr) { - callTracker["getMe"] = 0; + callTracker.getMe = 0; if (xhr.status === 304 || !data) return; @@ -334,19 +408,20 @@ module.exports.getMe = function() { }); }, function(err) { - callTracker["getMe"] = 0; - dispatchError(err, "getMe"); + callTracker.getMe = 0; + dispatchError(err, 'getMe'); } ); } +module.exports.getMe = getMe; module.exports.getStatuses = function() { - if (isCallInProgress("getStatuses")) return; + if (isCallInProgress('getStatuses')) return; - callTracker["getStatuses"] = utils.getTimestamp(); + callTracker['getStatuses'] = utils.getTimestamp(); client.getStatuses( function(data, textStatus, xhr) { - callTracker["getStatuses"] = 0; + callTracker['getStatuses'] = 0; if (xhr.status === 304 || !data) return; @@ -356,19 +431,19 @@ module.exports.getStatuses = function() { }); }, function(err) { - callTracker["getStatuses"] = 0; - dispatchError(err, "getStatuses"); + callTracker['getStatuses'] = 0; + dispatchError(err, 'getStatuses'); } ); } module.exports.getMyTeam = function() { - if (isCallInProgress("getMyTeam")) return; + if (isCallInProgress('getMyTeam')) return; - callTracker["getMyTeam"] = utils.getTimestamp(); + callTracker['getMyTeam'] = utils.getTimestamp(); client.getMyTeam( function(data, textStatus, xhr) { - callTracker["getMyTeam"] = 0; + callTracker['getMyTeam'] = 0; if (xhr.status === 304 || !data) return; @@ -378,8 +453,33 @@ module.exports.getMyTeam = function() { }); }, function(err) { - callTracker["getMyTeam"] = 0; - dispatchError(err, "getMyTeam"); + callTracker['getMyTeam'] = 0; + dispatchError(err, 'getMyTeam'); + } + ); +} + +function getConfig() { + if (isCallInProgress('getConfig')) { + return; + } + + callTracker['getConfig'] = utils.getTimestamp(); + client.getConfig( + function(data, textStatus, xhr) { + callTracker['getConfig'] = 0; + + if (data && xhr.status !== 304) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_CONFIG, + settings: data + }); + } + }, + function(err) { + callTracker['getConfig'] = 0; + dispatchError(err, 'getConfig'); } ); } +module.exports.getConfig = getConfig; diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 6a1f7c820..5aab80d01 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -540,18 +540,34 @@ module.exports.updateLastViewedAt = function(channelId, success, error) { }); }; -module.exports.getChannels = function(success, error) { +function getChannels(success, error) { $.ajax({ - url: "/api/v1/channels/", + url: '/api/v1/channels/', dataType: 'json', type: 'GET', success: success, ifModified: true, error: function(xhr, status, err) { - e = handleError("getChannels", xhr, status, err); + var e = handleError('getChannels', xhr, status, err); error(e); } }); +} +module.exports.getChannels = getChannels; + +module.exports.getChannel = function(id, success, error) { + $.ajax({ + url: "/api/v1/channels/" + id + "/", + dataType: 'json', + type: 'GET', + success: success, + error: function(xhr, status, err) { + e = handleError("getChannel", xhr, status, err); + error(e); + } + }); + + module.exports.track('api', 'api_channel_get'); }; module.exports.getMoreChannels = function(success, error) { @@ -568,6 +584,21 @@ module.exports.getMoreChannels = function(success, error) { }); }; +function getChannelCounts(success, error) { + $.ajax({ + url: '/api/v1/channels/counts', + dataType: 'json', + type: 'GET', + success: success, + ifModified: true, + error: function(xhr, status, err) { + var e = handleError('getChannelCounts', xhr, status, err); + error(e); + } + }); +} +module.exports.getChannelCounts = getChannelCounts; + module.exports.getChannelExtraInfo = function(id, success, error) { $.ajax({ url: "/api/v1/channels/" + id + "/extra_info", @@ -849,3 +880,18 @@ module.exports.updateValetFeature = function(data, success, error) { module.exports.track('api', 'api_teams_update_valet_feature'); }; + +function getConfig(success, error) { + $.ajax({ + url: '/api/v1/config/get_all', + dataType: 'json', + type: 'GET', + ifModified: true, + success: success, + error: function(xhr, status, err) { + var e = handleError('getConfig', xhr, status, err); + error(e); + } + }); +}; +module.exports.getConfig = getConfig; diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index bed0ec556..508de9185 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -10,6 +10,7 @@ module.exports = { CLICK_CHANNEL: null, CREATE_CHANNEL: null, RECIEVED_CHANNELS: null, + RECIEVED_CHANNEL: null, RECIEVED_MORE_CHANNELS: null, RECIEVED_CHANNEL_EXTRA_INFO: null, @@ -30,6 +31,8 @@ module.exports = { CLICK_TEAM: null, RECIEVED_TEAM: null, + + RECIEVED_CONFIG: null }), PayloadSources: keyMirror({ @@ -38,7 +41,7 @@ module.exports = { }), SPECIAL_MENTIONS: ['all', 'channel'], CHARACTER_LIMIT: 4000, - IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png'], + IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'], AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac'], VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'], SPREADSHEET_TYPES: ['ppt', 'pptx', 'csv'], diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index bc1009135..2312fe225 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -734,20 +734,19 @@ module.exports.isValidUsername = function (name) { return error; } -module.exports.switchChannel = function(channel, teammate_name) { +function switchChannel(channel, teammateName) { AppDispatcher.handleViewAction({ type: ActionTypes.CLICK_CHANNEL, name: channel.name, id: channel.id }); - var teamURL = window.location.href.split('/channels')[0]; - history.replaceState('data', '', teamURL + '/channels/' + channel.name); + updateAddressBar(channel.name); - if (channel.type === 'D' && teammate_name) { - document.title = teammate_name + " " + document.title.substring(document.title.lastIndexOf("-")); + if (channel.type === 'D' && teammateName) { + updateTabTitle(teammateName); } else { - document.title = channel.display_name + " " + document.title.substring(document.title.lastIndexOf("-")); + updateTabTitle(channel.display_name); } AsyncClient.getChannels(true, true, true); @@ -761,6 +760,18 @@ module.exports.switchChannel = function(channel, teammate_name) { return false; } +module.exports.switchChannel = switchChannel; + +function updateTabTitle(name) { + document.title = name + ' ' + document.title.substring(document.title.lastIndexOf('-')); +} +module.exports.updateTabTitle = updateTabTitle; + +function updateAddressBar(channelName) { + var teamURL = window.location.href.split('/channels')[0]; + history.replaceState('data', '', teamURL + '/channels/' + channelName); +} +module.exports.updateAddressBar = updateAddressBar; module.exports.isMobile = function() { return screen.width <= 768; diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss index 65775f01e..ca06d7def 100644 --- a/web/sass-files/sass/partials/_files.scss +++ b/web/sass-files/sass/partials/_files.scss @@ -115,7 +115,6 @@ height: 100px; float: left; margin: 5px 10px 5px 0; - display: table; border: 1px solid lightgrey; .post__load { height: 100%; @@ -137,16 +136,16 @@ } } .post-image__thumbnail { - display: table-cell; - vertical-align: top; + float: left; width: 50%; height: 100%; cursor: zoom-in; cursor: -webkit-zoom-in; } .post-image__details { - display: table-cell; - vertical-align: top; + float: left; + @include clearfix; + word-break: break-word; width: 50%; height: 100%; background: white; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 47b2b6bd7..f28df1f89 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -431,9 +431,6 @@ } } } - #user_settings { - border-right: none; - } body { &.white { .inner__wrap { diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index 1fb078bb9..0262ef60c 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -143,12 +143,6 @@ } } -#user_settings { - padding: 0 0.5em; - border-right: 1px solid #ddd; - max-width: 800px; -} - .channel-settings { padding: 0 10px; } |