Convering client to react-router.

Этот коммит содержится в:
Christopher Speller
2016-02-08 07:26:10 -05:00
родитель 08f0800ade
Коммит d7cdcf082f
183 изменённых файлов: 3433 добавлений и 5091 удалений

Просмотреть файл

@@ -127,10 +127,9 @@ package:
cp -RL web/static/help $(DIST_PATH)/web/static
cp -RL web/static/images $(DIST_PATH)/web/static
cp -RL web/static/js/jquery-dragster $(DIST_PATH)/web/static/js/
cp -RL web/templates $(DIST_PATH)/web
cp -RL templates $(DIST_PATH)
mkdir -p $(DIST_PATH)/api
cp -RL api/templates $(DIST_PATH)/api
cp -RL i18n $(DIST_PATH)
cp build/MIT-COMPILED-LICENSE.md $(DIST_PATH)
@@ -140,17 +139,17 @@ package:
mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js
mv $(DIST_PATH)/web/static/js/libs.min.js $(DIST_PATH)/web/static/js/libs-$(BUILD_NUMBER).min.js
sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
rm $(DIST_PATH)/web/templates/*.bak
sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html
sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html
sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/templates/head.html
sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/templates/head.html
sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/templates/head.html
sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/templates/head.html
sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/templates/head.html
sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/templates/head.html
sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html
sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html
rm $(DIST_PATH)/templates/*.bak
sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv'

Просмотреть файл

@@ -4,47 +4,15 @@
package api
import (
"bytes"
l4g "github.com/alecthomas/log4go"
"net/http"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"html/template"
"net/http"
_ "github.com/cloudfoundry/jibber_jabber"
_ "github.com/nicksnyder/go-i18n/i18n"
)
var ServerTemplates *template.Template
type ServerTemplatePage Page
func NewServerTemplatePage(templateName, locale string) *ServerTemplatePage {
return &ServerTemplatePage{
TemplateName: templateName,
Props: make(map[string]string),
Extra: make(map[string]string),
Html: make(map[string]template.HTML),
ClientCfg: utils.ClientCfg,
Locale: locale,
}
}
func (me *ServerTemplatePage) Render() string {
var text bytes.Buffer
T := utils.GetUserTranslations(me.Locale)
me.Props["Footer"] = T("api.templates.email_footer")
me.Html["EmailInfo"] = template.HTML(T("api.templates.email_info",
map[string]interface{}{"SupportEmail": me.ClientCfg["SupportEmail"], "SiteName": me.ClientCfg["SiteName"]}))
if err := ServerTemplates.ExecuteTemplate(&text, me.TemplateName, me); err != nil {
l4g.Error(utils.T("api.api.render.error"), me.TemplateName, err)
}
return text.String()
}
func InitApi() {
r := Srv.Router.PathPrefix("/api/v1").Subrouter()
InitUser(r)
@@ -60,12 +28,7 @@ func InitApi() {
InitPreference(r)
InitLicense(r)
templatesDir := utils.FindDir("api/templates")
l4g.Debug(utils.T("api.api.init.parsing_templates.debug"), templatesDir)
var err error
if ServerTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil {
l4g.Error(utils.T("api.api.init.parsing_templates.error"), err)
}
utils.InitHTML()
}
func HandleEtag(etag string, w http.ResponseWriter, r *http.Request) bool {

Просмотреть файл

@@ -5,11 +5,9 @@ package api
import (
"fmt"
"html/template"
"net"
"net/http"
"net/url"
"strconv"
"strings"
l4g "github.com/alecthomas/log4go"
@@ -31,33 +29,16 @@ var allowedMethods []string = []string{
}
type Context struct {
Session model.Session
RequestId string
IpAddress string
Path string
Err *model.AppError
teamURLValid bool
teamURL string
siteURL string
SessionTokenIndex int64
T goi18n.TranslateFunc
Locale string
}
type Page struct {
TemplateName string
Props map[string]string
Extra map[string]string
Html map[string]template.HTML
ClientCfg map[string]string
ClientLicense map[string]string
User *model.User
Team *model.Team
Channel *model.Channel
Preferences *model.Preferences
PostID string
SessionTokenIndex int64
Locale string
Session model.Session
RequestId string
IpAddress string
Path string
Err *model.AppError
teamURLValid bool
teamURL string
siteURL string
T goi18n.TranslateFunc
Locale string
}
func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
@@ -121,37 +102,8 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Attempt to parse the token from the cookie
if len(token) == 0 {
tokens := GetMultiSessionCookieTokens(r)
if len(tokens) > 0 {
// If there is only 1 token in the cookie then just use it like normal
if len(tokens) == 1 {
token = tokens[0]
} else {
// If it is a multi-session token then find the correct session
sessionTokenIndexStr := r.URL.Query().Get(model.SESSION_TOKEN_INDEX)
sessionTokenIndex := int64(-1)
if len(sessionTokenIndexStr) > 0 {
if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil {
sessionTokenIndex = index
}
} else {
sessionTokenIndexStr := r.Header.Get(model.HEADER_MM_SESSION_TOKEN_INDEX)
if len(sessionTokenIndexStr) > 0 {
if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil {
sessionTokenIndex = index
}
}
}
if sessionTokenIndex >= 0 && sessionTokenIndex < int64(len(tokens)) {
token = tokens[sessionTokenIndex]
c.SessionTokenIndex = sessionTokenIndex
} else {
c.SessionTokenIndex = -1
}
}
} else {
c.SessionTokenIndex = -1
if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
token = cookie.Value
}
}
@@ -185,8 +137,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if session == nil || session.IsExpired() {
c.RemoveSessionCookie(w, r)
c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token)
c.Err.StatusCode = http.StatusUnauthorized
if h.requireUser || h.requireSystemAdmin {
c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token)
c.Err.StatusCode = http.StatusUnauthorized
}
} else if !session.IsOAuth && isTokenFromQueryString {
c.Err = model.NewLocAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token)
c.Err.StatusCode = http.StatusUnauthorized
@@ -390,22 +344,6 @@ func (c *Context) IsTeamAdmin() bool {
}
func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
// multiToken := ""
// if oldMultiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
// multiToken = oldMultiCookie.Value
// }
// multiCookie := &http.Cookie{
// Name: model.SESSION_COOKIE_TOKEN,
// Value: strings.TrimSpace(strings.Replace(multiToken, c.Session.Token, "", -1)),
// Path: "/",
// MaxAge: model.SESSION_TIME_WEB_IN_SECS,
// HttpOnly: true,
// }
//http.SetCookie(w, multiCookie)
cookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
Value: "",
@@ -538,23 +476,25 @@ func IsPrivateIpAddress(ipAddress string) bool {
}
func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) {
props := make(map[string]string)
props["Message"] = err.Message
props["Details"] = err.DetailedError
T, locale := utils.GetTranslationsAndLocale(w, r)
page := utils.NewHTMLTemplate("error", locale)
page.Props["Message"] = err.Message
page.Props["Details"] = err.DetailedError
pathParts := strings.Split(r.URL.Path, "/")
if len(pathParts) > 1 {
props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1]
page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1]
} else {
props["SiteURL"] = GetProtocol(r) + "://" + r.Host
page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host
}
T, _ := utils.GetTranslationsAndLocale(w, r)
props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
props["Link"] = T("api.templates.error.link")
page.Props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
page.Props["Link"] = T("api.templates.error.link")
w.WriteHeader(err.StatusCode)
ServerTemplates.ExecuteTemplate(w, "error.html", Page{Props: props, ClientCfg: utils.ClientCfg})
if rErr := page.RenderToWriter(w); rErr != nil {
l4g.Error("Failed to create error page: " + rErr.Error() + ", Original error: " + err.Error())
}
}
func Handle404(w http.ResponseWriter, r *http.Request) {
@@ -588,29 +528,6 @@ func GetSession(token string) *model.Session {
return session
}
func GetMultiSessionCookieTokens(r *http.Request) []string {
if multiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
multiToken := multiCookie.Value
if len(multiToken) > 0 {
return strings.Split(multiToken, " ")
}
}
return []string{}
}
func FindMultiSessionForTeamId(r *http.Request, teamId string) (int64, *model.Session) {
for index, token := range GetMultiSessionCookieTokens(r) {
s := GetSession(token)
if s != nil && !s.IsExpired() && s.TeamId == teamId {
return int64(index), s
}
}
return -1, nil
}
func AddSessionToCache(session *model.Session) {
sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60))
}

Просмотреть файл

@@ -20,6 +20,7 @@ func InitLicense(r *mux.Router) {
sr := r.PathPrefix("/license").Subrouter()
sr.Handle("/add", ApiAdminSystemRequired(addLicense)).Methods("POST")
sr.Handle("/remove", ApiAdminSystemRequired(removeLicense)).Methods("POST")
sr.Handle("/client_config", ApiAppHandler(getClientLicenceConfig)).Methods("GET")
}
func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -130,3 +131,22 @@ func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) {
rdata["status"] = "ok"
w.Write([]byte(model.MapToJson(rdata)))
}
func getClientLicenceConfig(c *Context, w http.ResponseWriter, r *http.Request) {
config := utils.ClientLicense
var etag string
if config["IsLicensed"] == "false" {
etag = model.Etag(config["IsLicensed"])
} else {
etag = model.Etag(config["IsLicensed"], config["IssuedAt"])
}
if HandleEtag(etag, w, r) {
return
}
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.Write([]byte(model.MapToJson(config)))
}

22
api/license_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,22 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"testing"
)
func TestGetLicenceConfig(t *testing.T) {
Setup()
if result, err := Client.GetClientLicenceConfig(); err != nil {
t.Fatal(err)
} else {
cfg := result.Data.(map[string]string)
if _, ok := cfg["IsLicensed"]; !ok {
t.Fatal(cfg)
}
}
}

Просмотреть файл

@@ -5,12 +5,15 @@ package api
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"net/http"
"net/url"
)
func InitOAuth(r *mux.Router) {
@@ -20,6 +23,17 @@ func InitOAuth(r *mux.Router) {
sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST")
sr.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET")
sr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET")
sr.Handle("/{service:[A-Za-z]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET")
sr.Handle("/{service:[A-Za-z]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET")
sr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET")
sr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST")
// Also handle this a the old routes remove soon apiv2?
mr := Srv.Router
mr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET")
mr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST")
mr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET")
}
func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -163,3 +177,328 @@ func GetAuthData(code string) *model.AuthData {
return result.Data.(*model.AuthData)
}
}
func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
uri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete"
if body, team, props, err := AuthorizeOAuthUser(service, code, state, uri); err != nil {
c.Err = err
return
} else {
action := props["action"]
switch action {
case model.OAUTH_ACTION_SIGNUP:
CreateOAuthUser(c, w, r, service, body, team)
if c.Err == nil {
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_LOGIN:
LoginByOAuth(c, w, r, service, body, team)
if c.Err == nil {
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_EMAIL_TO_SSO:
CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"])
if c.Err == nil {
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_SSO_TO_EMAIL:
LoginByOAuth(c, w, r, service, body, team)
if c.Err == nil {
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect)
}
break
default:
LoginByOAuth(c, w, r, service, body, team)
if c.Err == nil {
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect)
}
break
}
}
}
func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
responseType := r.URL.Query().Get("response_type")
clientId := r.URL.Query().Get("client_id")
redirect := r.URL.Query().Get("redirect_uri")
scope := r.URL.Query().Get("scope")
state := r.URL.Query().Get("state")
if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 {
c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "")
return
}
var app *model.OAuthApp
if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
c.Err = result.Err
return
} else {
app = result.Data.(*model.OAuthApp)
}
var team *model.Team
if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
page := utils.NewHTMLTemplate("authorize", c.Locale)
page.Props["Title"] = c.T("web.authorize_oauth.title")
page.Props["TeamName"] = team.Name
page.Props["AppName"] = app.Name
page.Props["ResponseType"] = responseType
page.Props["ClientId"] = clientId
page.Props["RedirectUri"] = redirect
page.Props["Scope"] = scope
page.Props["State"] = state
if err := page.RenderToWriter(w); err != nil {
c.SetUnknownError(page.TemplateName, err.Error())
}
}
func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
c.LogAudit("attempt")
r.ParseForm()
grantType := r.FormValue("grant_type")
if grantType != model.ACCESS_TOKEN_GRANT_TYPE {
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "")
return
}
clientId := r.FormValue("client_id")
if len(clientId) != 26 {
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "")
return
}
secret := r.FormValue("client_secret")
if len(secret) == 0 {
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "")
return
}
code := r.FormValue("code")
if len(code) == 0 {
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "")
return
}
redirectUri := r.FormValue("redirect_uri")
achan := Srv.Store.OAuth().GetApp(clientId)
tchan := Srv.Store.OAuth().GetAccessDataByAuthCode(code)
authData := GetAuthData(code)
if authData == nil {
c.LogAudit("fail - invalid auth code")
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
return
}
uchan := Srv.Store.User().Get(authData.UserId)
if authData.IsExpired() {
c.LogAudit("fail - auth code expired")
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
return
}
if authData.RedirectUri != redirectUri {
c.LogAudit("fail - redirect uri provided did not match previous redirect uri")
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "")
return
}
if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) {
c.LogAudit("fail - auth code is invalid")
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
return
}
var app *model.OAuthApp
if result := <-achan; result.Err != nil {
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "")
return
} else {
app = result.Data.(*model.OAuthApp)
}
if !model.ComparePassword(app.ClientSecret, secret) {
c.LogAudit("fail - invalid client credentials")
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "")
return
}
callback := redirectUri
if len(callback) == 0 {
callback = app.CallbackUrls[0]
}
if result := <-tchan; result.Err != nil {
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "")
return
} else if result.Data != nil {
c.LogAudit("fail - auth code has been used previously")
accessData := result.Data.(*model.AccessData)
// Revoke access token, related auth code, and session from DB as well as from cache
if err := RevokeAccessToken(accessData.Token); err != nil {
l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message)
}
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "")
return
}
var user *model.User
if result := <-uchan; result.Err != nil {
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "")
return
} else {
user = result.Data.(*model.User)
}
session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true}
if result := <-Srv.Store.Session().Save(session); result.Err != nil {
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "")
return
} else {
session = result.Data.(*model.Session)
AddSessionToCache(session)
}
accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback}
if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil {
l4g.Error(result.Err)
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "")
return
}
accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
c.LogAuditWithUserId(user.Id, "success")
w.Write([]byte(accessRsp.ToJson()))
}
func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
loginHint := r.URL.Query().Get("login_hint")
teamName := r.URL.Query().Get("team")
if len(teamName) == 0 {
c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName)
c.Err.StatusCode = http.StatusBadRequest
return
}
// Make sure team exists
if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
return
}
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_LOGIN
if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil {
c.Err = err
return
} else {
http.Redirect(w, r, authUrl, http.StatusFound)
}
}
func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
teamName := r.URL.Query().Get("team")
if !utils.Cfg.TeamSettings.EnableUserCreation {
c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
if len(teamName) == 0 {
c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName)
c.Err.StatusCode = http.StatusBadRequest
return
}
hash := r.URL.Query().Get("h")
var team *model.Team
if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
if IsVerifyHashRequired(nil, team, hash) {
data := r.URL.Query().Get("d")
props := model.MapFromJson(strings.NewReader(data))
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "")
return
}
t, err := strconv.ParseInt(props["time"], 10, 64)
if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "")
return
}
if team.Id != props["id"] {
c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data)
return
}
}
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_SIGNUP
if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil {
c.Err = err
return
} else {
http.Redirect(w, r, authUrl, http.StatusFound)
}
}

Просмотреть файл

@@ -419,7 +419,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team
// copy the context and create a mock session for posting the message
mockSession := model.Session{UserId: hook.CreatorId, TeamId: hook.TeamId, IsOAuth: false}
newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, 0, c.T, c.Locale}
newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, c.T, c.Locale}
if text, ok := respProps["text"]; ok {
if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil {
@@ -604,12 +604,13 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
year := fmt.Sprintf("%d", tm.Year())
zone, _ := tm.Zone()
subjectPage := NewServerTemplatePage("post_subject", profileMap[id].Locale)
subjectPage := utils.NewHTMLTemplate("post_subject", profileMap[id].Locale)
subjectPage.Props["Subject"] = userLocale("api.templates.post_subject",
map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
"Month": month[:3], "Day": day, "Year": year})
subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName
bodyPage := NewServerTemplatePage("post_body", profileMap[id].Locale)
bodyPage := utils.NewHTMLTemplate("post_body", profileMap[id].Locale)
bodyPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message)
bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name

Просмотреть файл

@@ -29,13 +29,12 @@ func InitTeam(r *mux.Router) {
sr.Handle("/create_with_ldap", ApiAppHandler(createTeamWithLdap)).Methods("POST")
sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST")
sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST")
sr.Handle("/all", ApiUserRequired(getAll)).Methods("GET")
sr.Handle("/all", ApiAppHandler(getAll)).Methods("GET")
sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST")
sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST")
sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST")
sr.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST")
sr.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST")
sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET")
sr.Handle("/get_invite_info", ApiAppHandler(getInviteInfo)).Methods("POST")
// These should be moved to the global admain console
sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST")
sr.Handle("/export_team", ApiUserRequired(exportTeam)).Methods("GET")
@@ -60,11 +59,11 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
subjectPage := NewServerTemplatePage("signup_team_subject", c.Locale)
subjectPage := utils.NewHTMLTemplate("signup_team_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.signup_team_subject",
map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
bodyPage := NewServerTemplatePage("signup_team_body", c.Locale)
bodyPage := utils.NewHTMLTemplate("signup_team_body", c.Locale)
bodyPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage.Props["Title"] = c.T("api.templates.signup_team_body.title")
bodyPage.Props["Button"] = c.T("api.templates.signup_team_body.button")
@@ -86,7 +85,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
}
if !utils.Cfg.EmailSettings.RequireEmailVerification {
m["follow_link"] = bodyPage.Props["Link"]
m["follow_link"] = fmt.Sprintf("/signup_team_complete/?d=%s&h=%s", url.QueryEscape(data), url.QueryEscape(hash))
}
w.Header().Set("Access-Control-Allow-Origin", " *")
@@ -147,7 +146,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
data := map[string]string{"follow_link": c.GetSiteURL() + "/" + rteam.Name + "/signup/" + service}
data := map[string]string{"follow_link": c.GetSiteURL() + "/api/v1/oauth/" + service + "/signup?team=" + rteam.Name}
w.Write([]byte(model.MapToJson(data)))
}
@@ -391,10 +390,6 @@ func isTeamCreationAllowed(c *Context, email string) bool {
}
func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.HasSystemAdminPermissions("getLogs") {
return
}
if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
c.Err = result.Err
return
@@ -403,6 +398,9 @@ func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
m := make(map[string]*model.Team)
for _, v := range teams {
m[v.Id] = v
if !c.IsSystemAdmin() {
m[v.Id].SanitizeForNotLoggedIn()
}
}
w.Write([]byte(model.TeamMapToJson(m)))
@@ -473,74 +471,6 @@ func FindTeamByName(c *Context, name string, all string) bool {
return false
}
func findTeams(c *Context, w http.ResponseWriter, r *http.Request) {
m := model.MapFromJson(r.Body)
email := strings.ToLower(strings.TrimSpace(m["email"]))
if email == "" {
c.SetInvalidParam("findTeam", "email")
return
}
if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil {
c.Err = result.Err
return
} else {
teams := result.Data.([]*model.Team)
m := make(map[string]*model.Team)
for _, v := range teams {
v.Sanitize()
m[v.Id] = v
}
w.Write([]byte(model.TeamMapToJson(m)))
}
}
func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) {
m := model.MapFromJson(r.Body)
email := strings.ToLower(strings.TrimSpace(m["email"]))
if email == "" {
c.SetInvalidParam("findTeam", "email")
return
}
siteURL := c.GetSiteURL()
subjectPage := NewServerTemplatePage("find_teams_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.find_teams_subject",
map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
bodyPage := NewServerTemplatePage("find_teams_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.find_teams_body.title")
bodyPage.Props["Found"] = c.T("api.templates.find_teams_body.found")
bodyPage.Props["NotFound"] = c.T("api.templates.find_teams_body.not_found")
if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil {
c.Err = result.Err
} else {
teams := result.Data.([]*model.Team)
// the template expects Props to be a map with team names as the keys and the team url as the value
props := make(map[string]string)
for _, team := range teams {
props[team.Name] = c.GetTeamURLFromTeam(team)
}
bodyPage.Extra = props
if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
l4g.Error(utils.T("api.team.email_teams.sending.error"), err)
}
w.Write([]byte(model.MapToJson(m)))
}
}
func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) {
invites := model.InvitesFromJson(r.Body)
if len(invites.Invites) == 0 {
@@ -600,11 +530,11 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str
senderRole = c.T("api.team.invite_members.member")
}
subjectPage := NewServerTemplatePage("invite_subject", c.Locale)
subjectPage := utils.NewHTMLTemplate("invite_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.invite_subject",
map[string]interface{}{"SenderName": sender, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]})
bodyPage := NewServerTemplatePage("invite_body", c.Locale)
bodyPage := utils.NewHTMLTemplate("invite_body", c.Locale)
bodyPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage.Props["Title"] = c.T("api.templates.invite_body.title")
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.invite_body.info",
@@ -813,3 +743,25 @@ func exportTeam(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(result)))
}
}
func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) {
m := model.MapFromJson(r.Body)
inviteId := m["invite_id"]
if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
c.Err = result.Err
return
} else {
team := result.Data.(*model.Team)
if !(team.Type == model.TEAM_OPEN) {
c.Err = model.NewLocAppError("getInviteInfo", "api.team.get_invite_info.not_open_team", nil, "id="+inviteId)
return
}
result := map[string]string{}
result["display_name"] = team.DisplayName
result["name"] = team.Name
result["id"] = team.Id
w.Write([]byte(model.MapToJson(result)))
}
}

Просмотреть файл

@@ -108,37 +108,10 @@ func TestCreateTeam(t *testing.T) {
}
}
func TestFindTeamByEmail(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() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
if r1, err := Client.FindTeams(user.Email); err != nil {
t.Fatal(err)
} else {
teams := r1.Data.(map[string]*model.Team)
if teams[team.Id].Name != team.Name {
t.Fatal()
}
if teams[team.Id].DisplayName != team.DisplayName {
t.Fatal()
}
}
if _, err := Client.FindTeams("missing"); err != nil {
t.Fatal(err)
}
}
func TestGetAllTeams(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN, AllowTeamListing: true}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
@@ -147,8 +120,22 @@ func TestGetAllTeams(t *testing.T) {
Client.LoginByEmail(team.Name, user.Email, "pwd")
if _, err := Client.GetAllTeams(); err == nil {
t.Fatal("you shouldn't have permissions")
enableIncomingHooks := *utils.Cfg.TeamSettings.EnableTeamListing
defer func() {
*utils.Cfg.TeamSettings.EnableTeamListing = enableIncomingHooks
}()
*utils.Cfg.TeamSettings.EnableTeamListing = true
if r1, err := Client.GetAllTeams(); err != nil {
t.Fatal(err)
} else {
teams := r1.Data.(map[string]*model.Team)
if teams[team.Id].Name != team.Name {
t.Fatal()
}
if teams[team.Id].Email != "" {
t.Fatal("Non admin users shoudn't get full listings")
}
}
c := &Context{}
@@ -165,6 +152,9 @@ func TestGetAllTeams(t *testing.T) {
if teams[team.Id].Name != team.Name {
t.Fatal()
}
if teams[team.Id].Email != team.Email {
t.Fatal()
}
}
}
@@ -207,75 +197,6 @@ func TestTeamPermDelete(t *testing.T) {
Client.ClearOAuthToken()
}
/*
XXXXXX investigate and fix failing test
func TestFindTeamByDomain(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() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
if r1, err := Client.FindTeamByDomain(team.Name, false); err != nil {
t.Fatal(err)
} else {
val := r1.Data.(bool)
if !val {
t.Fatal("should be a valid domain")
}
}
if r1, err := Client.FindTeamByDomain(team.Name, true); err != nil {
t.Fatal(err)
} else {
val := r1.Data.(bool)
if !val {
t.Fatal("should be a valid domain")
}
}
if r1, err := Client.FindTeamByDomain("a"+model.NewId()+"a", false); err != nil {
t.Fatal(err)
} else {
val := r1.Data.(bool)
if val {
t.Fatal("shouldn't be a valid domain")
}
}
}
*/
func TestFindTeamByEmailSend(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() + "success+test@simulator.amazonses.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")
if _, err := Client.FindTeamsSendEmail(user.Email); err != nil {
t.Fatal(err)
} else {
}
if _, err := Client.FindTeamsSendEmail("missing"); err != nil {
// It should actually succeed at sending the email since it doesn't exist
if !strings.Contains(err.DetailedError, "Failed to add to email address") {
t.Fatal(err)
}
}
}
func TestInviteMembers(t *testing.T) {
Setup()

Просмотреть файл

@@ -1 +0,0 @@
{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}

Просмотреть файл

@@ -1 +0,0 @@
{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}

Просмотреть файл

@@ -1,37 +0,0 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title><span class='fa fa-chevron-left'></span>Back - Error</title>
<link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
<link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet">
<script src="/static/js/react-with-addons-0.13.3.min.js"></script>
<script src="/static/js/jquery-1.11.1.min.js"></script>
<script src="/static/js/bootstrap-3.3.5.min.js"></script>
<script src="/static/js/react-bootstrap-0.25.1.min.js"></script>
<link id="favicon" rel="icon" href="/static/images/favicon/favicon-16x16.png" type="image/x-icon">
<link rel="shortcut icon" href="/static/images/favicon/favicon-16x16.png" type="image/x-icon">
<link href='/static/css/google-fonts.css' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body class="white error">
<div class="container-fluid">
<div class="error__container">
<div class="error__icon"><i class="fa fa-exclamation-triangle"></i></div>
<h2>{{.Props.Title}}</h2>
<p>{{ .Props.Message }}</p>
<a href="{{.Props.SiteURL}}">{{.Props.Link}}</a>
</div>
</div>
</body>
<script>
var details = "{{ .Details }}";
if (details.length > 0) {
console.log("error details: " + details);
}
</script>
</html>

Просмотреть файл

@@ -1,52 +0,0 @@
{{define "find_teams_body"}}
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;">
<tr>
<td>
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
<tr>
<td style="border: 1px solid #ddd;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
<img src="{{.ClientCfg.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
<h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
<p>{{ if .Extra }}
{{.Props.Found}}<br>
{{range $index, $element := .Extra}}
<a href="{{ $element }}" style="text-decoration: none; color:#2389D7;">{{ $index }}</a><br>
{{ end }}
{{ else }}
{{.Props.NotFound}}
{{ end }}
</p>
</td>
</tr>
<tr>
{{template "email_info" . }}
</tr>
</table>
</td>
</tr>
<tr>
{{template "email_footer" . }}
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
{{end}}

Просмотреть файл

@@ -1 +0,0 @@
{{define "find_teams_subject"}}{{.Props.Subject}}{{end}}

Просмотреть файл

@@ -1 +0,0 @@
{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}

Просмотреть файл

@@ -53,10 +53,13 @@ func InitUser(r *mux.Router) {
sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST")
sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST")
sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).Methods("POST")
sr.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST")
sr.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST")
sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
sr.Handle("/me_logged_in", ApiAppHandler(getMeLoggedIn)).Methods("GET")
sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST")
sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET")
sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET")
@@ -315,10 +318,10 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service
func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) {
go func() {
subjectPage := NewServerTemplatePage("welcome_subject", c.Locale)
subjectPage := utils.NewHTMLTemplate("welcome_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.welcome_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName})
bodyPage := NewServerTemplatePage("welcome_body", c.Locale)
bodyPage := utils.NewHTMLTemplate("welcome_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.welcome_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName})
bodyPage.Props["Info"] = c.T("api.templates.welcome_body.info")
@@ -328,7 +331,7 @@ func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayN
bodyPage.Props["TeamURL"] = teamURL
if !verified {
link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email)
link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email)
bodyPage.Props["VerifyUrl"] = link
}
@@ -380,13 +383,13 @@ func addDirectChannelsAndForget(user *model.User) {
func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) {
go func() {
link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail)
link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail)
subjectPage := NewServerTemplatePage("verify_subject", c.Locale)
subjectPage := utils.NewHTMLTemplate("verify_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.verify_subject",
map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
bodyPage := NewServerTemplatePage("verify_body", c.Locale)
bodyPage := utils.NewHTMLTemplate("verify_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.verify_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName})
bodyPage.Props["Info"] = c.T("api.templates.verify_body.info")
@@ -621,31 +624,17 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User,
w.Header().Set(model.HEADER_TOKEN, session.Token)
tokens := GetMultiSessionCookieTokens(r)
multiToken := ""
seen := make(map[string]string)
seen[session.TeamId] = session.TeamId
for _, token := range tokens {
s := GetSession(token)
if s != nil && !s.IsExpired() && seen[s.TeamId] == "" {
multiToken += " " + token
seen[s.TeamId] = s.TeamId
}
}
multiToken = strings.TrimSpace(multiToken + " " + session.Token)
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
multiSessionCookie := &http.Cookie{
sessionCookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
Value: multiToken,
Value: session.Token,
Path: "/",
MaxAge: maxAge,
Expires: expiresAt,
HttpOnly: true,
}
http.SetCookie(w, multiSessionCookie)
http.SetCookie(w, sessionCookie)
c.Session = *session
c.LogAuditWithUserId(user.Id, "success")
@@ -902,6 +891,26 @@ func getMe(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
func getMeLoggedIn(c *Context, w http.ResponseWriter, r *http.Request) {
data := make(map[string]string)
data["logged_in"] = "false"
data["team_name"] = ""
if len(c.Session.UserId) != 0 {
teamChan := Srv.Store.Team().Get(c.Session.TeamId)
var team *model.Team
if tr := <-teamChan; tr.Err != nil {
c.Err = tr.Err
return
} else {
team = tr.Data.(*model.Team)
}
data["logged_in"] = "true"
data["team_name"] = team.Name
}
w.Write([]byte(model.MapToJson(data)))
}
func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
@@ -1622,12 +1631,12 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
data := model.MapToJson(newProps)
hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt))
link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash))
link := fmt.Sprintf("%s/reset_password_complete?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash))
subjectPage := NewServerTemplatePage("reset_subject", c.Locale)
subjectPage := utils.NewHTMLTemplate("reset_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.reset_subject")
bodyPage := NewServerTemplatePage("reset_body", c.Locale)
bodyPage := utils.NewHTMLTemplate("reset_body", c.Locale)
bodyPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage.Props["Title"] = c.T("api.templates.reset_body.title")
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.reset_body.info"))
@@ -1743,11 +1752,11 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) {
go func() {
subjectPage := NewServerTemplatePage("password_change_subject", c.Locale)
subjectPage := utils.NewHTMLTemplate("password_change_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.password_change_subject",
map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
bodyPage := NewServerTemplatePage("password_change_body", c.Locale)
bodyPage := utils.NewHTMLTemplate("password_change_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.password_change_body.title")
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.password_change_body.info",
@@ -1763,11 +1772,12 @@ func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamUR
func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) {
go func() {
subjectPage := NewServerTemplatePage("email_change_subject", c.Locale)
subjectPage := utils.NewHTMLTemplate("email_change_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.email_change_subject",
map[string]interface{}{"TeamDisplayName": teamDisplayName})
subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName
bodyPage := NewServerTemplatePage("email_change_body", c.Locale)
bodyPage := utils.NewHTMLTemplate("email_change_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title")
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.email_change_body.info",
@@ -1785,11 +1795,12 @@ func SendEmailChangeVerifyEmailAndForget(c *Context, userId, newUserEmail, teamN
link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail)
subjectPage := NewServerTemplatePage("email_change_verify_subject", c.Locale)
subjectPage := utils.NewHTMLTemplate("email_change_verify_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.email_change_verify_subject",
map[string]interface{}{"TeamDisplayName": teamDisplayName})
subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName
bodyPage := NewServerTemplatePage("email_change_verify_body", c.Locale)
bodyPage := utils.NewHTMLTemplate("email_change_verify_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.email_change_verify_body.title")
bodyPage.Props["Info"] = c.T("api.templates.email_change_verify_body.info",
@@ -1918,7 +1929,7 @@ func GetAuthorizationCode(c *Context, service, teamName string, props map[string
props["team"] = teamName
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props)))
redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8)
redirectUri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete"
authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state)
@@ -2216,11 +2227,11 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) {
go func() {
subjectPage := NewServerTemplatePage("signin_change_subject", c.Locale)
subjectPage := utils.NewHTMLTemplate("signin_change_subject", c.Locale)
subjectPage.Props["Subject"] = c.T("api.templates.singin_change_email.subject",
map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]})
bodyPage := NewServerTemplatePage("signin_change_body", c.Locale)
bodyPage := utils.NewHTMLTemplate("signin_change_body", c.Locale)
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["Title"] = c.T("api.templates.signin_change_email.body.title")
bodyPage.Html["Info"] = template.HTML(c.T("api.templates.singin_change_email.body.info",
@@ -2232,3 +2243,68 @@ func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL,
}()
}
func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
userId := props["uid"]
if len(userId) != 26 {
c.SetInvalidParam("verifyEmail", "uid")
return
}
hashedId := props["hid"]
if len(hashedId) == 0 {
c.SetInvalidParam("verifyEmail", "hid")
return
}
if model.ComparePassword(hashedId, userId) {
if c.Err = (<-Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil {
return
} else {
c.LogAudit("Email Verified")
return
}
}
c.Err = model.NewLocAppError("verifyEmail", "api.user.verify_email.bad_link.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
}
func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
teamName := props["team_name"]
if len(teamName) == 0 {
c.SetInvalidParam("resendVerification", "team_name")
return
}
email := props["email"]
if len(email) == 0 {
c.SetInvalidParam("resendVerification", "email")
return
}
var team *model.Team
if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil {
c.Err = result.Err
return
} else {
user := result.Data.(*model.User)
if user.LastActivityAt > 0 {
SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
} else {
SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
}
}
}

Просмотреть файл

@@ -1263,3 +1263,38 @@ func TestSwitchToEmail(t *testing.T) {
t.Fatal("should have failed - wrong user")
}
}
func TestMeLoggedIn(t *testing.T) {
Setup()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
Client.AuthToken = "invalid"
if result, err := Client.GetMeLoggedIn(); err != nil {
t.Fatal(err)
} else {
meLoggedIn := result.Data.(map[string]string)
if val, ok := meLoggedIn["logged_in"]; !ok || val != "false" {
t.Fatal("Got: " + val)
}
}
Client.LoginByEmail(team.Name, user.Email, user.Password)
if result, err := Client.GetMeLoggedIn(); err != nil {
t.Fatal(err)
} else {
meLoggedIn := result.Data.(map[string]string)
if val, ok := meLoggedIn["logged_in"]; !ok || val != "true" {
t.Fatal("Got: " + val)
}
}
}

Просмотреть файл

@@ -1623,6 +1623,10 @@
"id": "api.user.upload_profile_user.upload_profile.app_error",
"translation": "Couldn't upload profile image"
},
{
"id": "api.user.verify_email.bad_link.app_error",
"translation": "Bad verify email link."
},
{
"id": "api.web_conn.new_web_conn.last_activity.error",
"translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v"
@@ -3399,22 +3403,6 @@
"id": "web.find_team.title",
"translation": "Find Team"
},
{
"id": "web.footer.about",
"translation": "About"
},
{
"id": "web.footer.help",
"translation": "Help"
},
{
"id": "web.footer.privacy",
"translation": "Privacy"
},
{
"id": "web.footer.terms",
"translation": "Terms"
},
{
"id": "web.get_access_token.bad_client_id.app_error",
"translation": "invalid_request: Bad client_id"
@@ -3547,10 +3535,6 @@
"id": "web.root.home_title",
"translation": "Home"
},
{
"id": "web.root.singup_info",
"translation": "All team communication in one place, searchable and accessible anywhere"
},
{
"id": "web.root.singup_title",
"translation": "Signup"
@@ -3606,5 +3590,9 @@
{
"id": "web.watcher_fail.error",
"translation": "Failed to add directory to watcher %v"
},
{
"id": "api.team.get_invite_info.not_open_team",
"translation": "Invite is invalid because this is not an open team."
}
]

Просмотреть файл

@@ -3399,22 +3399,6 @@
"id": "web.find_team.title",
"translation": "Encontrar Equipo"
},
{
"id": "web.footer.about",
"translation": "Acerca"
},
{
"id": "web.footer.help",
"translation": "Ayuda"
},
{
"id": "web.footer.privacy",
"translation": "Privacidad"
},
{
"id": "web.footer.terms",
"translation": "Términos"
},
{
"id": "web.get_access_token.bad_client_id.app_error",
"translation": "invalid_request: client_id malo"
@@ -3547,10 +3531,6 @@
"id": "web.root.home_title",
"translation": "Inicio"
},
{
"id": "web.root.singup_info",
"translation": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte"
},
{
"id": "web.root.singup_title",
"translation": "Registrar"

Просмотреть файл

@@ -16,19 +16,17 @@ import (
)
const (
HEADER_REQUEST_ID = "X-Request-ID"
HEADER_VERSION_ID = "X-Version-ID"
HEADER_ETAG_SERVER = "ETag"
HEADER_ETAG_CLIENT = "If-None-Match"
HEADER_FORWARDED = "X-Forwarded-For"
HEADER_REAL_IP = "X-Real-IP"
HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
HEADER_TOKEN = "token"
HEADER_BEARER = "BEARER"
HEADER_AUTH = "Authorization"
HEADER_MM_SESSION_TOKEN_INDEX = "X-MM-TokenIndex"
SESSION_TOKEN_INDEX = "session_token_index"
API_URL_SUFFIX = "/api/v1"
HEADER_REQUEST_ID = "X-Request-ID"
HEADER_VERSION_ID = "X-Version-ID"
HEADER_ETAG_SERVER = "ETag"
HEADER_ETAG_CLIENT = "If-None-Match"
HEADER_FORWARDED = "X-Forwarded-For"
HEADER_REAL_IP = "X-Real-IP"
HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
HEADER_TOKEN = "token"
HEADER_BEARER = "BEARER"
HEADER_AUTH = "Authorization"
API_URL_SUFFIX = "/api/v1"
)
type Result struct {
@@ -179,29 +177,6 @@ func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppErro
}
}
func (c *Client) FindTeams(email string) (*Result, *AppError) {
m := make(map[string]string)
m["email"] = email
if r, err := c.DoApiPost("/teams/find_teams", MapToJson(m)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil
}
}
func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) {
m := make(map[string]string)
m["email"] = email
if r, err := c.DoApiPost("/teams/email_teams", MapToJson(m)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil
}
}
func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) {
if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil {
return nil, err
@@ -938,7 +913,7 @@ func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (*
}
func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) {
if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil {
if r, err := c.DoApiPost("/oauth/access_token", data.Encode()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -1057,3 +1032,21 @@ func (c *Client) MockSession(sessionToken string) {
c.AuthToken = sessionToken
c.AuthType = HEADER_BEARER
}
func (c *Client) GetClientLicenceConfig() (*Result, *AppError) {
if r, err := c.DoApiGet("/license/client_config", "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
func (c *Client) GetMeLoggedIn() (*Result, *AppError) {
if r, err := c.DoApiGet("/users/me_logged_in", "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}

Просмотреть файл

@@ -9,7 +9,7 @@ import (
)
const (
SESSION_COOKIE_TOKEN = "MMTOKEN"
SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
SESSION_CACHE_SIZE = 10000
SESSION_PROP_PLATFORM = "platform"
SESSION_PROP_OS = "os"

Просмотреть файл

@@ -232,3 +232,10 @@ func (o *Team) Sanitize() {
o.Email = ""
o.AllowedDomains = ""
}
func (o *Team) SanitizeForNotLoggedIn() {
o.Email = ""
o.AllowedDomains = ""
o.CompanyName = ""
o.InviteId = ""
}

Просмотреть файл

Просмотреть файл

1
templates/email_change_subject.html Обычный файл
Просмотреть файл

@@ -0,0 +1 @@
{{define "email_change_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}

1
templates/email_change_verify_subject.html Обычный файл
Просмотреть файл

@@ -0,0 +1 @@
{{define "email_change_verify_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}

Просмотреть файл

Просмотреть файл

24
templates/error.html Обычный файл
Просмотреть файл

@@ -0,0 +1,24 @@
{{define "error"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body class="white error">
<div class="container-fluid">
<div class="error__container">
<div class="error__icon">
<i class="fa fa-exclamation-triangle"/>
</div>
<h2>{{.Props.Title}}</h2>
<p>{{ .Props.Message }}</p>
<a href="{{.Props.SiteURL}}">{{.Props.Link}}</a>
</div>
</div>
</body>
<script>
var details = {{ .Props.Details }};
if (details.length > 0) {
console.log("error details: " + details);
}
</script>
</html>
{{end}}

92
templates/head.html Обычный файл
Просмотреть файл

@@ -0,0 +1,92 @@
{{define "head"}}
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<title>{{ .Props.Title }}</title>
<!-- iOS add to homescreen -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="{{ .Props.Title }}">
<meta name="application-name" content="{{ .Props.Title }}">
<meta name="format-detection" content="telephone=no">
<!-- iOS add to homescreen -->
<!-- Android add to homescreen -->
<link rel="apple-touch-icon" sizes="57x57" href="/static/images/favicon/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/static/images/favicon/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/static/images/favicon/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/static/images/favicon/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/static/images/favicon/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/static/images/favicon/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/static/images/favicon/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/static/images/favicon/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicon/apple-touch-icon-180x180.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="192x192" href="/static/images/favicon/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon/favicon-16x16.png">
<link rel="manifest" href="/static/config/manifest.json">
<!-- Android add to homescreen -->
<!-- CSS Should always go first -->
<link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
<link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-colorpicker.min.css">
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/css/google-fonts.css">
<link rel="stylesheet" href="/static/css/katex.min.css">
<link rel="stylesheet" class="code_theme" href="">
<script src="/static/js/intl-1.0.0/Intl.js"></script>
<script src="/static/js/intl-1.0.0/locale-data/jsonp/en.js"></script>
<script src="/static/js/intl-1.0.0/locale-data/jsonp/es.js"></script>
<script src="/static/js/intl-1.0.0/locale-data/jsonp/pt.js"></script>
<script src="/static/js/react-0.14.3.js"></script>
<script src="/static/js/react-dom-0.14.3.js"></script>
<script src="/static/js/react-intl-2.0.0-beta-2/react-intl.js"></script>
<script src="/static/js/react-intl-2.0.0-beta-2/locale-data/en.js"></script>
<script src="/static/js/react-intl-2.0.0-beta-2/locale-data/es.js"></script>
<script src="/static/js/react-intl-2.0.0-beta-2/locale-data/pt.js"></script>
<script src="/static/js/jquery-2.1.4.js"></script>
<script src="/static/js/bootstrap-3.3.5.js"></script>
<script src="/static/js/bootstrap-colorpicker.min.js"></script>
<script src="/static/js/react-bootstrap-0.28.1.js"></script>
<script src="/static/js/velocity.min.js"></script>
<script src="/static/js/perfect-scrollbar-0.6.7.jquery.min.js"></script>
<script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
<script src="/static/js/babel-polyfill-6.1.18.min.js"></script>
<script src="/static/js/katex.min.js"></script>
<script src="/static/js/Chart.min.js"></script>
<style id="antiClickjack">body{display:none !important;}</style>
<script>
if ('ReactIntl' in window && 'ReactIntlLocaleData' in window) {
Object.keys(ReactIntlLocaleData).forEach(function(lang) {
ReactIntl.addLocaleData(ReactIntlLocaleData[lang]);
});
}
$(window).on('beforeunload', function(){
if (window.SocketStore) {
SocketStore.close();
}
});
</script>
<script src="/static/js/libs.min.js"></script>
<script src="/static/js/bundle.js"></script>
<script type="text/javascript">
if (self === top) {
var blocker = document.getElementById("antiClickjack");
blocker.parentNode.removeChild(blocker);
}
</script>
</head>
{{end}}

Просмотреть файл

Просмотреть файл

Просмотреть файл

1
templates/post_subject.html Обычный файл
Просмотреть файл

@@ -0,0 +1 @@
{{define "post_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}

Просмотреть файл

Просмотреть файл

12
templates/root.html Обычный файл
Просмотреть файл

@@ -0,0 +1,12 @@
{{define "root"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body>
<div id='root'/>
<script>
window.setup_root();
</script>
</body>
</html>
{{end}}

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

97
utils/html.go Обычный файл
Просмотреть файл

@@ -0,0 +1,97 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package utils
import (
"bytes"
"html/template"
"net/http"
l4g "github.com/alecthomas/log4go"
"gopkg.in/fsnotify.v1"
)
// Global storage for templates
var htmlTemplates *template.Template
type HTMLTemplate struct {
TemplateName string
Props map[string]string
Html map[string]template.HTML
Locale string
}
func InitHTML() {
templatesDir := FindDir("templates")
l4g.Debug(T("api.api.init.parsing_templates.debug"), templatesDir)
var err error
if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil {
l4g.Error(T("api.api.init.parsing_templates.error"), err)
}
// Watch the templates folder for changes.
watcher, err := fsnotify.NewWatcher()
if err != nil {
l4g.Error(T("web.create_dir.error"), err)
}
go func() {
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
l4g.Info(T("web.reparse_templates.info"), event.Name)
if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil {
l4g.Error(T("web.parsing_templates.error"), err)
}
}
case err := <-watcher.Errors:
l4g.Error(T("web.dir_fail.error"), err)
}
}
}()
err = watcher.Add(templatesDir)
if err != nil {
l4g.Error(T("web.watcher_fail.error"), err)
}
}
func NewHTMLTemplate(templateName string, locale string) *HTMLTemplate {
return &HTMLTemplate{
TemplateName: templateName,
Props: make(map[string]string),
Html: make(map[string]template.HTML),
Locale: locale,
}
}
func (t *HTMLTemplate) addDefaultProps() {
T := GetUserTranslations(t.Locale)
t.Props["Footer"] = T("api.templates.email_footer")
t.Html["EmailInfo"] = template.HTML(T("api.templates.email_info",
map[string]interface{}{"SupportEmail": Cfg.SupportSettings.SupportEmail, "SiteName": Cfg.TeamSettings.SiteName}))
}
func (t *HTMLTemplate) Render() string {
t.addDefaultProps()
var text bytes.Buffer
if err := htmlTemplates.ExecuteTemplate(&text, t.TemplateName, t); err != nil {
l4g.Error(T("api.api.render.error"), t.TemplateName, err)
}
return text.String()
}
func (t *HTMLTemplate) RenderToWriter(w http.ResponseWriter) error {
t.addDefaultProps()
if err := htmlTemplates.ExecuteTemplate(w, t.TemplateName, t); err != nil {
l4g.Error(T("api.api.render.error"), t.TemplateName, err)
return err
}
return nil
}

Просмотреть файл

@@ -20,7 +20,7 @@ import (
var IsLicensed bool = false
var License *model.License = &model.License{}
var ClientLicense map[string]string = make(map[string]string)
var ClientLicense map[string]string = map[string]string{"IsLicensed": "false"}
var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r

Просмотреть файл

@@ -220,3 +220,33 @@ export function sendEphemeralPost(message, channelId) {
emitPostRecievedEvent(post);
}
export function loadTeamRequiredPage() {
AsyncClient.getAllTeams();
}
export function newLocalizationSelected(locale) {
Client.getTranslations(
locale,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_LOCALE,
locale,
translations: data
});
},
(err) => {
AsyncClient.dispatchError(err, 'getTranslations');
}
);
}
export function viewLoggedIn() {
AsyncClient.getChannels();
AsyncClient.getChannelExtraInfo();
AsyncClient.getMyTeam();
AsyncClient.getMe();
// Clear pending posts (shouldn't have pending posts if we are loading)
PostStore.clearPendingPosts();
}

Просмотреть файл

@@ -8,7 +8,7 @@ const Modal = ReactBootstrap.Modal;
import LoadingScreen from './loading_screen.jsx';
import * as Utils from '../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
import {FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl';
export default class ActivityLogModal extends React.Component {
constructor(props) {
@@ -144,8 +144,21 @@ export default class ActivityLogModal extends React.Component {
id='activity_log.firstTime'
defaultMessage='First time active: {date}, {time}'
values={{
date: firstAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}),
time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'})
date: (
<FormattedDate
value={firstAccessTime}
day='2-digit'
month='long'
year='numeric'
/>
),
time: (
<FormattedTime
value={firstAccessTime}
hour='2-digit'
minute='2-digit'
/>
)
}}
/>
</div>
@@ -206,8 +219,21 @@ export default class ActivityLogModal extends React.Component {
id='activity_log.lastActivity'
defaultMessage='Last activity: {date}, {time}'
values={{
date: lastAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}),
time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'})
date: (
<FormattedDate
value={lastAccessTime}
day='2-digit'
month='long'
year='numeric'
/>
),
time: (
<FormattedTime
value={lastAccessTime}
hour='2-digit'
minute='2-digit'
/>
)
}}
/>
</div>

Просмотреть файл

@@ -6,7 +6,6 @@ import AdminStore from '../../stores/admin_store.jsx';
import TeamStore from '../../stores/team_store.jsx';
import * as AsyncClient from '../../utils/async_client.jsx';
import LoadingScreen from '../loading_screen.jsx';
import * as Utils from '../../utils/utils.jsx';
import EmailSettingsTab from './email_settings.jsx';
import LogSettingsTab from './log_settings.jsx';
@@ -50,11 +49,6 @@ export default class AdminController extends React.Component {
selected: props.tab || 'system_analytics',
selectedTeam: props.teamId || null
};
if (!props.tab) {
var tokenIndex = Utils.getUrlParameter('session_token_index');
history.replaceState(null, null, `/admin_console/${this.state.selected}?session_token_index=${tokenIndex}`);
}
}
componentDidMount() {
@@ -63,6 +57,9 @@ export default class AdminController extends React.Component {
AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
AsyncClient.getAllTeams();
$('[data-toggle="tooltip"]').tooltip();
$('[data-toggle="popover"]').popover();
}
componentWillUnmount() {
@@ -175,7 +172,7 @@ export default class AdminController extends React.Component {
}
return (
<div>
<div id='admin_controller'>
<div
className='sidebar--menu'
id='sidebar-menu'

Просмотреть файл

@@ -2,13 +2,14 @@
// See License.txt for license information.
import * as Utils from '../../utils/utils.jsx';
import * as Client from '../../utils/client.jsx';
import TeamStore from '../../stores/team_store.jsx';
import Constants from '../../utils/constants.jsx';
import {FormattedMessage} from 'mm-intl';
import {Link} from 'react-router';
function getStateFromStores() {
return {currentTeam: TeamStore.getCurrent()};
}
@@ -18,16 +19,9 @@ export default class AdminNavbarDropdown extends React.Component {
super(props);
this.blockToggle = false;
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = getStateFromStores();
}
handleLogoutClick(e) {
e.preventDefault();
Client.logout();
}
componentDidMount() {
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
this.blockToggle = true;
@@ -78,15 +72,12 @@ export default class AdminNavbarDropdown extends React.Component {
</a>
</li>
<li>
<a
href='#'
onClick={this.handleLogoutClick}
>
<Link to={Utils.getTeamURLFromAddressBar() + '/logout'}>
<FormattedMessage
id='admin.nav.logout'
defaultMessage='Logout'
/>
</a>
</Link>
</li>
<li className='divider'></li>
<li>
@@ -116,4 +107,4 @@ export default class AdminNavbarDropdown extends React.Component {
</ul>
);
}
}
}

Просмотреть файл

@@ -3,7 +3,6 @@
import AdminSidebarHeader from './admin_sidebar_header.jsx';
import SelectTeamModal from './select_team_modal.jsx';
import * as Utils from '../../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -30,8 +29,6 @@ export default class AdminSidebar extends React.Component {
handleClick(name, teamId, e) {
e.preventDefault();
this.props.selectTab(name, teamId);
var tokenIndex = Utils.getUrlParameter('session_token_index');
history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}?session_token_index=${tokenIndex}`);
}
isSelected(name, teamId) {
@@ -73,7 +70,6 @@ export default class AdminSidebar extends React.Component {
}
teamSelectedModal(teamId) {
this.props.selectedTeams[teamId] = 'true';
this.setState({showSelectModal: false});
this.props.addSelectedTeam(teamId);
this.forceUpdate();

Просмотреть файл

@@ -3,7 +3,6 @@
import AdminNavbarDropdown from './admin_navbar_dropdown.jsx';
import UserStore from '../../stores/user_store.jsx';
import * as Utils from '../../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -39,7 +38,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()}
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
/>
);
}
@@ -65,4 +64,4 @@ export default class SidebarHeader extends React.Component {
</div>
);
}
}
}

Просмотреть файл

@@ -366,7 +366,7 @@ export default class UserItem extends React.Component {
<td className='row member-div padding--equal'>
<img
className='post-profile-img pull-left'
src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
height='36'
width='36'
/>

Просмотреть файл

@@ -5,7 +5,7 @@ import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import * as Utils from '../utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate, FormattedTime} from 'mm-intl';
const holders = defineMessages({
sessionRevoked: {
@@ -598,8 +598,23 @@ export function formatAuditInfo(audit, formatMessage) {
}
const date = new Date(audit.create_at);
let auditInfo = {};
auditInfo.timestamp = date.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + date.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'});
const auditInfo = {};
auditInfo.timestamp = (
<div>
<FormattedDate
value={date}
day='2-digit'
month='short'
year='numeric'
/>
{' - '}
<FormattedTime
value={date}
hour='2-digit'
minute='2-digit'
/>
</div>
);
auditInfo.userId = audit.user_id;
auditInfo.desc = auditDesc;
auditInfo.ip = audit.ip_address;

Просмотреть файл

@@ -25,40 +25,43 @@ export default class CenterPanel extends React.Component {
constructor(props) {
super(props);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.onChannelChange = this.onChannelChange.bind(this);
this.onUserChange = this.onUserChange.bind(this);
this.getStateFromStores = this.getStateFromStores.bind(this);
this.validState = this.validState.bind(this);
this.onStoresChange = this.onStoresChange.bind(this);
this.state = this.getStateFromStores();
}
getStateFromStores() {
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
this.state = {
showTutorialScreens: tutorialStep === TutorialSteps.INTRO_SCREENS,
return {
showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS,
showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS,
user: UserStore.getCurrentUser(),
channel: ChannelStore.getCurrent(),
profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))
};
}
validState() {
return this.state.user && this.state.channel && this.state.profiles;
}
onStoresChange() {
this.setState(this.getStateFromStores());
}
componentDidMount() {
PreferenceStore.addChangeListener(this.onPreferenceChange);
ChannelStore.addChangeListener(this.onChannelChange);
UserStore.addChangeListener(this.onUserChange);
PreferenceStore.addChangeListener(this.onStoresChange);
ChannelStore.addChangeListener(this.onStoresChange);
UserStore.addChangeListener(this.onStoresChange);
}
componentWillUnmount() {
PreferenceStore.removeChangeListener(this.onPreferenceChange);
ChannelStore.removeChangeListener(this.onChannelChange);
UserStore.removeChangeListener(this.onUserChange);
}
onPreferenceChange() {
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
this.setState({showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS});
}
onChannelChange() {
this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS});
}
onUserChange() {
this.setState({user: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))});
PreferenceStore.removeChangeListener(this.onStoresChange);
ChannelStore.removeChangeListener(this.onStoresChange);
UserStore.removeChangeListener(this.onStoresChange);
}
render() {
const channel = ChannelStore.getCurrent();
if (!this.validState()) {
return null;
}
const channel = this.state.channel;
var handleClick = null;
let postsContainer;
let createPost;

Просмотреть файл

@@ -57,20 +57,33 @@ export default class ChannelHeader extends React.Component {
memberChannel: ChannelStore.getCurrentMember(),
users: extraInfo.members,
userCount: extraInfo.member_count,
searchVisible: SearchStore.getSearchResults() !== null
searchVisible: SearchStore.getSearchResults() !== null,
currentUser: UserStore.getCurrentUser()
};
}
validState() {
if (!this.state.channel ||
!this.state.memberChannel ||
!this.state.users ||
!this.state.userCount ||
!this.state.currentUser) {
return false;
}
return true;
}
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
SearchStore.addSearchChangeListener(this.onListenerChange);
PreferenceStore.addChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
SearchStore.removeSearchChangeListener(this.onListenerChange);
PreferenceStore.removeChangeListener(this.onListenerChange);
UserStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
const newState = this.getStateFromStores();
@@ -98,7 +111,7 @@ export default class ChannelHeader extends React.Component {
searchMentions(e) {
e.preventDefault();
const user = this.props.user;
const user = this.state.currentUser;
let terms = '';
if (user.notify_props && user.notify_props.mention_keys) {
@@ -134,7 +147,7 @@ export default class ChannelHeader extends React.Component {
});
}
render() {
if (this.state.channel === null) {
if (!this.validState()) {
return null;
}
@@ -163,8 +176,8 @@ export default class ChannelHeader extends React.Component {
</Popover>
);
let channelTitle = channel.display_name;
const currentId = this.props.user.id;
const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.props.user.roles);
const currentId = this.state.currentUser.id;
const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.state.currentUser.roles);
const isDirect = (this.state.channel.type === 'D');
if (isDirect) {
@@ -252,7 +265,7 @@ export default class ChannelHeader extends React.Component {
<ToggleModalButton
role='menuitem'
dialogType={ChannelInviteModal}
dialogProps={{channel}}
dialogProps={{channel, currentUser: this.state.currentUser}}
>
<FormattedMessage
id='chanel_header.addMembers'
@@ -331,7 +344,11 @@ export default class ChannelHeader extends React.Component {
<ToggleModalButton
role='menuitem'
dialogType={ChannelNotificationsModal}
dialogProps={{channel}}
dialogProps={{
channel,
channelMember: this.state.memberChannel,
currentUser: this.state.currentUser
}}
>
<FormattedMessage
id='channel_header.notificationPreferences'
@@ -497,5 +514,4 @@ export default class ChannelHeader extends React.Component {
}
ChannelHeader.propTypes = {
user: React.PropTypes.object.isRequired
};

Просмотреть файл

@@ -4,8 +4,8 @@
import FilteredUserList from './filtered_user_list.jsx';
import LoadingScreen from './loading_screen.jsx';
import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx';
import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
@@ -16,18 +16,15 @@ import {FormattedMessage} from 'mm-intl';
const Modal = ReactBootstrap.Modal;
export default class ChannelInviteModal extends React.Component {
constructor() {
super();
constructor(props) {
super(props);
this.onListenerChange = this.onListenerChange.bind(this);
this.handleInvite = this.handleInvite.bind(this);
this.getStateFromStores = this.getStateFromStores.bind(this);
this.createInviteButton = this.createInviteButton.bind(this);
// the state gets populated when the modal is shown
this.state = {
loading: true
};
this.state = this.getStateFromStores();
}
shouldComponentUpdate(nextProps, nextState) {
if (!this.props.show && !nextProps.show) {
@@ -63,6 +60,20 @@ export default class ChannelInviteModal extends React.Component {
};
}
const currentUser = UserStore.getCurrentUser();
if (!currentUser) {
return {
loading: true
};
}
const currentMember = ChannelStore.getCurrentMember();
if (!currentMember) {
return {
loading: true
};
}
const memberIds = extraInfo.members.map((user) => user.id);
var nonmembers = [];
@@ -78,7 +89,9 @@ export default class ChannelInviteModal extends React.Component {
return {
nonmembers,
loading: false
loading: false,
currentUser,
currentMember
};
}
componentWillReceiveProps(nextProps) {
@@ -93,6 +106,11 @@ export default class ChannelInviteModal extends React.Component {
UserStore.removeChangeListener(this.onListenerChange);
}
}
componentWillUnmount() {
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
ChannelStore.removeChangeListener(this.onListenerChange);
UserStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
var newState = this.getStateFromStores();
if (!Utils.areObjectsEqual(this.state, newState)) {
@@ -144,7 +162,6 @@ export default class ChannelInviteModal extends React.Component {
if (Utils.windowHeight() <= 1200) {
maxHeight = Utils.windowHeight() - 300;
}
content = (
<FilteredUserList
style={{maxHeight}}

Просмотреть файл

@@ -1,204 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
/* This is a special React control with the sole purpose of making all the AsyncClient calls
to the server on page load. This is to prevent other React controls from spamming
AsyncClient with requests. */
import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
import SocketStore from '../stores/socket_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import PostStore from '../stores/post_store.jsx';
import UserStore from '../stores/user_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
import {intlShape, injectIntl, defineMessages} from 'mm-intl';
const holders = defineMessages({
socketError: {
id: 'channel_loader.socketError',
defaultMessage: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.'
},
someone: {
id: 'channel_loader.someone',
defaultMessage: 'Someone'
},
posted: {
id: 'channel_loader.posted',
defaultMessage: 'Posted'
},
uploadedImage: {
id: 'channel_loader.uploadedImage',
defaultMessage: ' uploaded an image'
},
uploadedFile: {
id: 'channel_loader.uploadedFile',
defaultMessage: ' uploaded a file'
},
something: {
id: 'channel_loader.something',
defaultMessage: ' did something new'
},
wrote: {
id: 'channel_loader.wrote',
defaultMessage: ' wrote: '
},
connectionError: {
id: 'channel_loader.connection_error',
defaultMessage: 'There appears to be a problem with your internet connection.'
},
unknownError: {
id: 'channel_loader.unknown_error',
defaultMessage: 'We received an unexpected status code from the server.'
}
});
class ChannelLoader extends React.Component {
constructor(props) {
super(props);
this.intervalId = null;
this.onSocketChange = this.onSocketChange.bind(this);
const {formatMessage} = this.props.intl;
SocketStore.setTranslations({
socketError: formatMessage(holders.socketError),
someone: formatMessage(holders.someone),
posted: formatMessage(holders.posted),
uploadedImage: formatMessage(holders.uploadedImage),
uploadedFile: formatMessage(holders.uploadedFile),
something: formatMessage(holders.something),
wrote: formatMessage(holders.wrote)
});
Client.setTranslations({
connectionError: formatMessage(holders.connectionError),
unknownError: formatMessage(holders.unknownError)
});
this.state = {};
}
componentDidMount() {
/* Initial aysnc loads */
AsyncClient.getPosts(ChannelStore.getCurrentId());
AsyncClient.getChannels();
AsyncClient.getChannelExtraInfo();
AsyncClient.findTeams();
AsyncClient.getMyTeam();
setTimeout(() => AsyncClient.getStatuses(), 3000); // temporary until statuses are reworked a bit
/* Perform pending post clean-up */
PostStore.clearPendingPosts();
/* Set up interval functions */
this.intervalId = setInterval(() => AsyncClient.getStatuses(), 30000);
/* Device tracking setup */
var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
if (iOS) {
$('body').addClass('ios');
}
/* Set up tracking for whether the window is active */
window.isActive = true;
$(window).on('focus', function windowFocus() {
AsyncClient.updateLastViewedAt();
ChannelStore.resetCounts(ChannelStore.getCurrentId());
ChannelStore.emitChange();
window.isActive = true;
});
$(window).on('blur', function windowBlur() {
window.isActive = false;
});
/* Start global change listeners setup */
SocketStore.addChangeListener(this.onSocketChange);
/* Update CSS classes to match user theme */
var user = UserStore.getCurrentUser();
if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
Utils.applyTheme(user.theme_props);
} else {
Utils.applyTheme(Constants.THEMES.default);
}
// if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx
const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT);
Utils.applyFont(selectedFont);
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
} else {
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
}
});
$('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after');
$(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before');
} else {
$(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after');
$(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before');
}
});
$('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
} else {
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
}
});
/* Prevent backspace from navigating back a page */
$(window).on('keydown.preventBackspace', (e) => {
if (e.which === 8 && !$(e.target).is('input, textarea')) {
e.preventDefault();
}
});
}
componentWillUnmount() {
clearInterval(this.intervalId);
$(window).off('focus');
$(window).off('blur');
SocketStore.removeChangeListener(this.onSocketChange);
$('body').off('click.userpopover');
$('body').off('mouseenter mouseleave', '.post');
$('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
$('.modal').off('show.bs.modal');
$(window).off('keydown.preventBackspace');
}
onSocketChange(msg) {
if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
UserStore.setStatus(msg.user_id, 'online');
}
}
render() {
return <div/>;
}
}
ChannelLoader.propTypes = {
intl: intlShape.isRequired
};
export default injectIntl(ChannelLoader);

Просмотреть файл

@@ -6,7 +6,6 @@ import SettingItemMin from './setting_item_min.jsx';
import SettingItemMax from './setting_item_max.jsx';
import * as Client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -15,7 +14,6 @@ export default class ChannelNotificationsModal extends React.Component {
constructor(props) {
super(props);
this.onListenerChange = this.onListenerChange.bind(this);
this.updateSection = this.updateSection.bind(this);
this.handleSubmitNotifyLevel = this.handleSubmitNotifyLevel.bind(this);
@@ -26,58 +24,41 @@ export default class ChannelNotificationsModal extends React.Component {
this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
const member = ChannelStore.getMember(props.channel.id);
this.state = {
notifyLevel: member.notify_props.desktop,
markUnreadLevel: member.notify_props.mark_unread,
channelId: ChannelStore.getCurrentId(),
activeSection: ''
activeSection: '',
notifyLevel: '',
unreadLevel: ''
};
}
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
this.onListenerChange();
ChannelStore.addChangeListener(this.onListenerChange);
} else {
ChannelStore.removeChangeListener(this.onListenerChange);
}
}
onListenerChange() {
const curChannelId = ChannelStore.getCurrentId();
if (!curChannelId) {
return;
}
const newState = {channelId: curChannelId};
const member = ChannelStore.getMember(curChannelId);
if (member.notify_props.desktop !== this.state.notifyLevel || member.notify_props.mark_unread !== this.state.mark_unread) {
newState.notifyLevel = member.notify_props.desktop;
newState.markUnreadLevel = member.notify_props.mark_unread;
}
this.setState(newState);
}
updateSection(section) {
this.setState({activeSection: section});
}
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
this.setState({
notifyLevel: nextProps.channelMember.notify_props.desktop,
unreadLevel: nextProps.channelMember.notify_props.mark_unread
});
}
}
handleSubmitNotifyLevel() {
var channelId = this.state.channelId;
var channelId = this.props.channel.id;
var notifyLevel = this.state.notifyLevel;
if (ChannelStore.getMember(channelId).notify_props.desktop === notifyLevel) {
if (this.props.channelMember.notify_props.desktop === notifyLevel) {
this.updateSection('');
return;
}
var data = {};
data.channel_id = channelId;
data.user_id = UserStore.getCurrentId();
data.user_id = this.props.currentUser.id;
data.desktop = notifyLevel;
//TODO: This should be moved to event_helpers
Client.updateNotifyProps(data,
() => {
// YUCK
var member = ChannelStore.getMember(channelId);
member.notify_props.desktop = notifyLevel;
ChannelStore.setChannelMember(member);
@@ -92,11 +73,8 @@ export default class ChannelNotificationsModal extends React.Component {
this.setState({notifyLevel});
}
createNotifyLevelSection(serverError) {
var handleUpdateSection;
const user = UserStore.getCurrentUser();
const globalNotifyLevel = user.notify_props.desktop;
// Get glabal user setting for notifications
const globalNotifyLevel = this.props.currentUser.notify_props.desktop;
let globalNotifyLevelName;
if (globalNotifyLevel === 'all') {
globalNotifyLevelName = (
@@ -128,13 +106,15 @@ export default class ChannelNotificationsModal extends React.Component {
/>
);
const notificationLevel = this.state.notifyLevel;
if (this.state.activeSection === 'desktop') {
var notifyActive = [false, false, false, false];
if (this.state.notifyLevel === 'default') {
const notifyActive = [false, false, false, false];
if (notificationLevel === 'default') {
notifyActive[0] = true;
} else if (this.state.notifyLevel === 'all') {
} else if (notificationLevel === 'all') {
notifyActive[1] = true;
} else if (this.state.notifyLevel === 'mention') {
} else if (notificationLevel === 'mention') {
notifyActive[2] = true;
} else {
notifyActive[3] = true;
@@ -196,7 +176,7 @@ export default class ChannelNotificationsModal extends React.Component {
</div>
);
handleUpdateSection = function updateSection(e) {
const handleUpdateSection = function updateSection(e) {
this.updateSection('');
this.onListenerChange();
e.preventDefault();
@@ -224,7 +204,7 @@ export default class ChannelNotificationsModal extends React.Component {
}
var describe;
if (this.state.notifyLevel === 'default') {
if (notificationLevel === 'default') {
describe = (
<FormattedMessage
id='channel_notifications.globalDefault'
@@ -233,45 +213,44 @@ export default class ChannelNotificationsModal extends React.Component {
}}
/>
);
} else if (this.state.notifyLevel === 'mention') {
} else if (notificationLevel === 'mention') {
describe = (<FormattedMessage id='channel_notifications.onlyMentions'/>);
} else if (this.state.notifyLevel === 'all') {
} else if (notificationLevel === 'all') {
describe = (<FormattedMessage id='channel_notifications.allActivity'/>);
} else {
describe = (<FormattedMessage id='channel_notifications.never'/>);
}
handleUpdateSection = function updateSection(e) {
this.updateSection('desktop');
e.preventDefault();
}.bind(this);
return (
<SettingItemMin
title={sendDesktop}
describe={describe}
updateSection={handleUpdateSection}
updateSection={() => {
this.updateSection('desktop');
}}
/>
);
}
handleSubmitMarkUnreadLevel() {
const channelId = this.state.channelId;
const markUnreadLevel = this.state.markUnreadLevel;
const channelId = this.props.channel.id;
const markUnreadLevel = this.state.unreadLevel;
if (ChannelStore.getMember(channelId).notify_props.mark_unread === markUnreadLevel) {
if (this.props.channelMember.notify_props.mark_unread === markUnreadLevel) {
this.updateSection('');
return;
}
const data = {
channel_id: channelId,
user_id: UserStore.getCurrentId(),
user_id: this.props.currentUser.id,
mark_unread: markUnreadLevel
};
//TODO: This should be fixed, moved to event_helpers
Client.updateNotifyProps(data,
() => {
// Yuck...
var member = ChannelStore.getMember(channelId);
member.notify_props.mark_unread = markUnreadLevel;
ChannelStore.setChannelMember(member);
@@ -283,8 +262,8 @@ export default class ChannelNotificationsModal extends React.Component {
);
}
handleUpdateMarkUnreadLevel(markUnreadLevel) {
this.setState({markUnreadLevel});
handleUpdateMarkUnreadLevel(unreadLevel) {
this.setState({unreadLevel});
}
createMarkUnreadLevelSection(serverError) {
@@ -303,7 +282,7 @@ export default class ChannelNotificationsModal extends React.Component {
<label>
<input
type='radio'
checked={this.state.markUnreadLevel === 'all'}
checked={this.state.unreadLevel === 'all'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
/>
<FormattedMessage
@@ -317,7 +296,7 @@ export default class ChannelNotificationsModal extends React.Component {
<label>
<input
type='radio'
checked={this.state.markUnreadLevel === 'mention'}
checked={this.state.unreadLevel === 'mention'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
/>
<FormattedMessage id='channel_notifications.onlyMentions'/>
@@ -355,7 +334,7 @@ export default class ChannelNotificationsModal extends React.Component {
} else {
let describe;
if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') {
if (!this.state.unreadLevel || this.state.unreadLevel === 'all') {
describe = (
<FormattedMessage
id='channel_notifications.allUnread'
@@ -430,5 +409,7 @@ export default class ChannelNotificationsModal extends React.Component {
ChannelNotificationsModal.propTypes = {
show: React.PropTypes.bool.isRequired,
onHide: React.PropTypes.func.isRequired,
channel: React.PropTypes.object.isRequired
channel: React.PropTypes.object.isRequired,
channelMember: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired
};

Просмотреть файл

@@ -2,34 +2,11 @@
// See License.txt for license information.
import CenterPanel from '../components/center_panel.jsx';
import Sidebar from '../components/sidebar.jsx';
import SidebarRight from '../components/sidebar_right.jsx';
import SidebarRightMenu from '../components/sidebar_right_menu.jsx';
export default class ChannelView extends React.Component {
render() {
return (
<div className='container-fluid'>
<div
className='sidebar--right'
id='sidebar-right'
>
<SidebarRight/>
</div>
<div
className='sidebar--menu'
id='sidebar-menu'
>
<SidebarRightMenu/>
</div>
<div
className='sidebar--left'
id='sidebar-left'
>
<Sidebar/>
</div>
<CenterPanel/>
</div>
<CenterPanel/>
);
}
}
@@ -37,4 +14,5 @@ ChannelView.defaultProps = {
};
ChannelView.propTypes = {
params: React.PropTypes.object
};

Просмотреть файл

@@ -3,6 +3,7 @@
import EmailToSSO from './email_to_sso.jsx';
import SSOToEmail from './sso_to_email.jsx';
import TeamStore from '../../stores/team_store.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -10,11 +11,46 @@ export default class ClaimAccount extends React.Component {
constructor(props) {
super(props);
this.onTeamChange = this.onTeamChange.bind(this);
this.updateStateFromStores = this.updateStateFromStores.bind(this);
this.state = {};
}
componentWillMount() {
this.setState({
email: this.props.location.query.email,
newType: this.props.location.query.new_type,
oldType: this.props.location.query.old_type,
teamName: this.props.params.team,
teamDisplayName: ''
});
this.updateStateFromStores();
}
componentDidMount() {
TeamStore.addChangeListener(this.onTeamChange);
}
componentWillUnmount() {
TeamStore.removeChangeListener(this.onTeamChange);
}
updateStateFromStores() {
const team = TeamStore.getByName(this.state.teamName);
let displayName = '';
if (team) {
displayName = team.displayName;
}
this.setState({
teamDisplayName: displayName
});
}
onTeamChange() {
this.updateStateFromStores();
}
render() {
if (this.state.teamDisplayName === '') {
return (<div/>);
}
let content;
if (this.props.email === '') {
if (this.state.email === '') {
content = (
<p>
<FormattedMessage
@@ -23,36 +59,55 @@ export default class ClaimAccount extends React.Component {
/>
</p>
);
} else if (this.props.currentType === '' && this.props.newType !== '') {
} else if (this.state.oldType === '' && this.state.newType !== '') {
content = (
<EmailToSSO
email={this.props.email}
type={this.props.newType}
teamName={this.props.teamName}
teamDisplayName={this.props.teamDisplayName}
email={this.state.email}
type={this.state.newType}
teamName={this.state.teamName}
teamDisplayName={this.state.teamDisplayName}
/>
);
} else {
content = (
<SSOToEmail
email={this.props.email}
currentType={this.props.currentType}
teamName={this.props.teamName}
teamDisplayName={this.props.teamDisplayName}
email={this.state.email}
currentType={this.state.oldType}
teamName={this.state.teamName}
teamDisplayName={this.state.teamDisplayName}
/>
);
}
return content;
return (
<div>
<div className='signup-header'>
<a href='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
/>
</a>
</div>
<div className='col-sm-12'>
<div className='signup-team__container'>
<img
className='signup-team-logo'
src='/static/images/logo.png'
/>
<div id='claim'>
{content}
</div>
</div>
</div>
</div>
);
}
}
ClaimAccount.defaultProps = {
};
ClaimAccount.propTypes = {
currentType: React.PropTypes.string.isRequired,
newType: React.PropTypes.string.isRequired,
email: React.PropTypes.string.isRequired,
teamName: React.PropTypes.string.isRequired,
teamDisplayName: React.PropTypes.string.isRequired
params: React.PropTypes.object.isRequired,
location: React.PropTypes.object.isRequired
};

Просмотреть файл

@@ -159,7 +159,7 @@ SSOToEmail.propTypes = {
currentType: React.PropTypes.string.isRequired,
email: React.PropTypes.string.isRequired,
teamName: React.PropTypes.string.isRequired,
teamDisplayName: React.PropTypes.string.isRequired
teamDisplayName: React.PropTypes.string
};
export default injectIntl(SSOToEmail);

Просмотреть файл

@@ -9,7 +9,7 @@ import PostDeletedModal from './post_deleted_modal.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import * as Utils from '../utils/utils.jsx';
@@ -165,7 +165,7 @@ class CreatePost extends React.Component {
const channel = ChannelStore.get(this.state.channelId);
EventHelpers.emitUserPostedEvent(post);
GlobalActions.emitUserPostedEvent(post);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel,
@@ -177,7 +177,7 @@ class CreatePost extends React.Component {
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
EventHelpers.emitPostRecievedEvent(data);
GlobalActions.emitPostRecievedEvent(data);
},
(err) => {
if (err.id === 'api.post.create_post.root_id.app_error') {

Просмотреть файл

@@ -9,6 +9,8 @@ import Constants from '../utils/constants.jsx';
import {FormattedMessage} from 'mm-intl';
import {browserHistory} from 'react-router';
export default class DeleteChannelModal extends React.Component {
constructor(props) {
super(props);
@@ -21,11 +23,11 @@ export default class DeleteChannelModal extends React.Component {
return;
}
browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square');
Client.deleteChannel(
this.props.channel.id,
() => {
AsyncClient.getChannels(true);
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
},
(err) => {
AsyncClient.dispatchError(err, 'handleDelete');

82
web/react/components/do_verify_email.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,82 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {FormattedMessage} from 'mm-intl';
import * as Client from '../utils/client.jsx';
import LoadingScreen from './loading_screen.jsx';
import {browserHistory} from 'react-router';
export default class DoVerifyEmail extends React.Component {
constructor(props) {
super(props);
this.state = {
verifyStatus: 'pending',
serverError: ''
};
}
componentWillMount() {
const uid = this.props.location.query.uid;
const hid = this.props.location.query.hid;
const teamName = this.props.location.query.teamname;
const email = this.props.location.query.email;
Client.verifyEmail(
() => {
browserHistory.push('/' + teamName + '/login?extra=verified&email=' + email);
},
(err) => {
this.setState({verifyStatus: 'failure', serverError: err.message});
},
uid,
hid
);
}
render() {
if (this.state.verifyStatus !== 'failure') {
return (<LoadingScreen/>);
}
return (
<div>
<div className='signup-header'>
<a href='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
/>
</a>
</div>
<div className='col-sm-12'>
<div className='signup-team__container'>
<h3>
<FormattedMessage
id='email_verify.almost'
defaultMessage='{siteName}: You are almost done'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
</h3>
<div>
<p>
<FormattedMessage id='email_verify.verifyFailed'/>
</p>
<p className='alert alert-danger'>
<i className='fa fa-times'/>
{this.state.serverError}
</p>
</div>
</div>
</div>
</div>
);
}
}
DoVerifyEmail.defaultProps = {
};
DoVerifyEmail.propTypes = {
location: React.PropTypes.object.isRequired
};

Просмотреть файл

@@ -1,41 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as TextFormatting from '../utils/text_formatting.jsx';
import UserStore from '../stores/user_store.jsx';
export default class Docs extends React.Component {
constructor(props) {
super(props);
UserStore.setCurrentUser(global.window.mm_user || {});
this.state = {text: ''};
const errorState = {text: '## 404'};
if (props.site) {
$.get(`/static/help/${props.site}_${global.window.mm_locale}.md`).then((response) => {
this.setState({text: response});
}, () => {
this.setState(errorState);
});
} else {
this.setState(errorState);
}
}
render() {
return (
<div
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.text)}}
>
</div>
);
}
}
Docs.defaultProps = {
site: ''
};
Docs.propTypes = {
site: React.PropTypes.string
};

Просмотреть файл

@@ -3,7 +3,7 @@
import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import Textbox from './textbox.jsx';
import BrowserStore from '../stores/browser_store.jsx';
import PostStore from '../stores/post_store.jsx';
@@ -45,7 +45,7 @@ class EditPostModal extends React.Component {
delete tempState.editText;
BrowserStore.setItem('edit_state_transfer', tempState);
$('#edit_post').modal('hide');
EventHelpers.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
GlobalActions.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
return;
}

Просмотреть файл

@@ -1,108 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
export default class EmailVerify extends React.Component {
constructor(props) {
super(props);
this.handleResend = this.handleResend.bind(this);
this.state = {};
}
handleResend() {
const newAddress = window.location.href.replace('&resend_success=true', '');
window.location.href = newAddress + '&resend=true';
}
render() {
var title = '';
var body = '';
var resend = '';
var resendConfirm = '';
if (this.props.isVerified === 'true') {
title = (
<FormattedMessage
id='email_verify.verified'
defaultMessage='{siteName} Email Verified'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
);
body = (
<FormattedHTMLMessage
id='email_verify.verifiedBody'
defaultMessage='<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>'
values={{
url: this.props.teamURL + '?email=' + this.props.userEmail
}}
/>
);
} else {
title = (
<FormattedMessage
id='email_verify.almost'
defaultMessage='{siteName}: You are almost done'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
);
body = (
<p>
<FormattedMessage
id='email_verify.notVerifiedBody'
defaultMessage='Please verify your email address. Check your inbox for an email.'
/>
</p>
);
resend = (
<button
onClick={this.handleResend}
className='btn btn-primary'
>
<FormattedMessage
id='email_verify.resend'
defaultMessage='Resend Email'
/>
</button>
);
if (this.props.resendSuccess) {
resendConfirm = (
<div><br/><p className='alert alert-success'><i className='fa fa-check'></i>
<FormattedMessage
id='email_verify.sent'
defaultMessage=' Verification email sent.'
/>
</p></div>);
}
}
return (
<div className='col-sm-12'>
<div className='signup-team__container'>
<h3>{title}</h3>
<div>
{body}
{resend}
{resendConfirm}
</div>
</div>
</div>
);
}
}
EmailVerify.defaultProps = {
isVerified: 'false',
teamURL: '',
userEmail: '',
resendSuccess: 'false'
};
EmailVerify.propTypes = {
isVerified: React.PropTypes.string,
teamURL: React.PropTypes.string,
userEmail: React.PropTypes.string,
resendSuccess: React.PropTypes.string
};

Просмотреть файл

@@ -43,7 +43,7 @@ class FileAttachment extends React.Component {
if (type === 'image') {
var self = this; // Need this reference since we use the given "this"
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) {
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) {
return function loader() {
$(this).remove();
if (name in self.refs) {
@@ -114,7 +114,7 @@ class FileAttachment extends React.Component {
var re3 = new RegExp('\\)', 'g');
var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')');
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)');
}
}
removeBackgroundImage(name) {

Просмотреть файл

@@ -1,135 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as utils from '../utils/utils.jsx';
import * as client from '../utils/client.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
var holders = defineMessages({
submitError: {
id: 'find_team.submitError',
defaultMessage: 'Please enter a valid email address'
},
placeholder: {
id: 'find_team.placeholder',
defaultMessage: 'you@domain.com'
}
});
class FindTeam extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
var state = { };
var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
if (!email || !utils.isEmail(email)) {
state.email_error = this.props.intl.formatMessage(holders.submitError);
this.setState(state);
return;
}
state.email_error = '';
client.findTeamsSendEmail(email,
function success() {
state.sent = true;
this.setState(state);
}.bind(this),
function fail(err) {
state.email_error = err.message;
this.setState(state);
}.bind(this)
);
}
render() {
var emailError = null;
var emailErrorClass = 'form-group';
if (this.state.email_error) {
emailError = <label className='control-label'>{this.state.email_error}</label>;
emailErrorClass = 'form-group has-error';
}
if (this.state.sent) {
return (
<div>
<h4>
<FormattedMessage
id='find_team.findTitle'
defaultMessage='Find Your Team'
/>
</h4>
<p>
<FormattedMessage
id='find_team.findDescription'
defaultMessage='An email was sent with links to any teams to which you are a member.'
/>
</p>
</div>
);
}
return (
<div>
<h4>
<FormattedMessage
id='find_team.findTitle'
defaultMessage='Find Your Team'
/>
</h4>
<form onSubmit={this.handleSubmit}>
<p>
<FormattedMessage
id='find_team.getLinks'
defaultMessage='Get an email with links to any teams to which you are a member.'
/>
</p>
<div className='form-group'>
<label className='control-label'>
<FormattedMessage
id='find_team.email'
defaultMessage='Email'
/>
</label>
<div className={emailErrorClass}>
<input
type='text'
ref='email'
className='form-control'
placeholder={this.props.intl.formatMessage(holders.placeholder)}
maxLength='128'
spellCheck='false'
/>
{emailError}
</div>
</div>
<button
className='btn btn-md btn-primary'
type='submit'
>
<FormattedMessage
id='find_team.send'
defaultMessage='Send'
/>
</button>
</form>
</div>
);
}
}
FindTeam.propTypes = {
intl: intlShape.isRequired
};
export default injectIntl(FindTeam);

Просмотреть файл

@@ -5,7 +5,7 @@ import * as utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import * as Client from '../utils/client.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import ModalStore from '../stores/modal_store.jsx';
import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
@@ -223,7 +223,7 @@ class InviteMemberModal extends React.Component {
showGetTeamInviteLinkModal() {
this.handleHide(false);
EventHelpers.showGetTeamInviteLinkModal();
GlobalActions.showGetTeamInviteLinkModal();
}
render() {

224
web/react/components/logged_in.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,224 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as AsyncClient from '../utils/async_client.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import UserStore from '../stores/user_store.jsx';
import SocketStore from '../stores/socket_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
import ErrorBar from '../components/error_bar.jsx';
import {browserHistory} from 'react-router';
import SidebarRight from '../components/sidebar_right.jsx';
import SidebarRightMenu from '../components/sidebar_right_menu.jsx';
// Modals
import GetPostLinkModal from '../components/get_post_link_modal.jsx';
import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx';
import EditPostModal from '../components/edit_post_modal.jsx';
import DeletePostModal from '../components/delete_post_modal.jsx';
import MoreChannelsModal from '../components/more_channels.jsx';
import TeamSettingsModal from '../components/team_settings_modal.jsx';
import RemovedFromChannelModal from '../components/removed_from_channel_modal.jsx';
import RegisterAppModal from '../components/register_app_modal.jsx';
import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx';
import InviteMemberModal from '../components/invite_member_modal.jsx';
import SelectTeamModal from '../components/admin_console/select_team_modal.jsx';
const CLIENT_STATUS_INTERVAL = 30000;
const BACKSPACE_CHAR = 8;
export default class LoggedIn extends React.Component {
constructor(params) {
super(params);
this.onUserChanged = this.onUserChanged.bind(this);
}
onUserChanged() {
// Grab the current user
const user = UserStore.getCurrentUser();
// Update segment indentify
if (global.window.mm_config.SegmentDeveloperKey != null && global.window.mm_config.SegmentDeveloperKey !== '') {
global.window.analytics.identify(user.id, {
name: user.nickname,
email: user.email,
createdAt: user.create_at,
username: user.username,
team_id: user.team_id,
id: user.id
});
}
// Update CSS classes to match user theme
if (user) {
if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
Utils.applyTheme(user.theme_props);
} else {
Utils.applyTheme(Constants.THEMES.default);
}
}
}
onSocketChange(msg) {
if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
UserStore.setStatus(msg.user_id, 'online');
}
}
componentWillMount() {
// Emit view action
GlobalActions.viewLoggedIn();
// Listen for user
UserStore.addChangeListener(this.onUserChanged);
// Add listner for socker store
SocketStore.addChangeListener(this.onSocketChange);
// Get all statuses regularally. (Soon to be switched to websocket)
this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL);
// Force logout of all tabs if one tab is logged out
$(window).bind('storage', (e) => {
// when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
// make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
return;
}
console.log('detected logout from a different tab'); //eslint-disable-line no-console
browserHistory.push('/' + this.props.params.team);
}
if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
// make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
return;
}
console.log('detected login from a different tab'); //eslint-disable-line no-console
location.reload();
}
});
// Because current CSS requires the root tag to have specific stuff
$('#root').attr('class', 'channel-view');
// ???
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
} else {
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
}
});
$('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after');
$(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before');
} else {
$(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after');
$(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before');
}
});
$('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
} else {
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
}
});
// Device tracking setup
var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
if (iOS) {
$('body').addClass('ios');
}
// Set up tracking for whether the window is active
window.isActive = true;
$(window).on('focus', () => {
AsyncClient.updateLastViewedAt();
ChannelStore.resetCounts(ChannelStore.getCurrentId());
ChannelStore.emitChange();
window.isActive = true;
});
$(window).on('blur', () => {
window.isActive = false;
});
// if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx
const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT);
Utils.applyFont(selectedFont);
// Pervent backspace from navigating back a page
$(window).on('keydown.preventBackspace', (e) => {
if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) {
e.preventDefault();
}
});
}
componentWillUnmount() {
$('#root').attr('class', '');
clearInterval(this.intervalId);
$(window).off('focus');
$(window).off('blur');
SocketStore.removeChangeListener(this.onSocketChange);
UserStore.removeChangeListener(this.onUserChanged);
$('body').off('click.userpopover');
$('body').off('mouseenter mouseleave', '.post');
$('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
$('.modal').off('show.bs.modal');
$(window).off('keydown.preventBackspace');
}
render() {
return (
<div className='channel-view'>
<ErrorBar/>
<div className='container-fluid'>
<SidebarRight/>
<SidebarRightMenu/>
{this.props.sidebar}
{this.props.center}
<GetPostLinkModal/>
<GetTeamInviteLinkModal/>
<InviteMemberModal/>
<ImportThemeModal/>
<TeamSettingsModal/>
<MoreChannelsModal/>
<EditPostModal/>
<DeletePostModal/>
<RemovedFromChannelModal/>
<RegisterAppModal/>
<SelectTeamModal/>
</div>
</div>
);
}
}
LoggedIn.defaultProps = {
};
LoggedIn.propTypes = {
children: React.PropTypes.object,
sidebar: React.PropTypes.object,
center: React.PropTypes.object,
params: React.PropTypes.object
};

Просмотреть файл

@@ -6,82 +6,118 @@ import LoginUsername from './login_username.jsx';
import LoginLdap from './login_ldap.jsx';
import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
import Constants from '../utils/constants.jsx';
import TeamStore from '../stores/team_store.jsx';
import {FormattedMessage} from 'mm-intl';
import {browserHistory} from 'react-router';
export default class Login extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
this.state = this.getStateFromStores();
}
componentDidMount() {
TeamStore.addChangeListener(this.onTeamChange);
Client.getMeLoggedIn((data) => {
if (data && data.logged_in !== 'false') {
browserHistory.push('/' + this.props.params.team + '/channels/town-square');
}
});
}
componentWillUnmount() {
TeamStore.removeChangeListener(this.onTeamChange);
}
getStateFromStores() {
return {
currentTeam: TeamStore.getByName(this.props.params.team)
};
}
onTeamChange() {
this.setState(this.getStateFromStores());
}
render() {
const teamDisplayName = this.props.teamDisplayName;
const teamName = this.props.teamName;
const currentTeam = this.state.currentTeam;
if (currentTeam == null) {
return <div/>;
}
const teamDisplayName = currentTeam.display_name;
const teamName = currentTeam.name;
let loginMessage = [];
if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
loginMessage.push(
<a
className='btn btn-custom-login gitlab'
key='gitlab'
href={'/' + teamName + '/login/gitlab'}
>
<span className='icon'/>
<span>
<FormattedMessage
id='login.gitlab'
defaultMessage='with GitLab'
/>
</span>
</a>
<a
className='btn btn-custom-login gitlab'
key='gitlab'
href={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)}
>
<span className='icon'/>
<span>
<FormattedMessage
id='login.gitlab'
defaultMessage='with GitLab'
/>
</span>
</a>
);
}
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
loginMessage.push(
<a
className='btn btn-custom-login google'
key='google'
href={'/' + teamName + '/login/google'}
>
<span className='icon'/>
<span>
<FormattedMessage
id='login.google'
defaultMessage='with Google Apps'
/>
</span>
</a>
);
<a
className='btn btn-custom-login google'
key='google'
href={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)}
>
<span className='icon'/>
<span>
<FormattedMessage
id='login.google'
defaultMessage='with Google Apps'
/>
</span>
</a>
);
}
const extraParam = Utils.getUrlParameter('extra');
let extraBox = '';
if (extraParam) {
let msg;
if (extraParam === Constants.SIGNIN_CHANGE) {
msg = (
<FormattedMessage
id='login.changed'
defaultMessage=' Sign-in method changed successfully'
/>
);
} else if (extraParam === Constants.SIGNIN_VERIFIED) {
msg = (
<FormattedMessage
id='login.verified'
defaultMessage=' Email Verified'
/>
);
}
if (msg != null) {
extraBox = (
<div className='alert alert-success'>
<i className='fa fa-check'/>
{msg}
<FormattedMessage
id='login.changed'
defaultMessage=' Sign-in method changed successfully'
/>
</div>
);
} else if (extraParam === Constants.SIGNIN_VERIFIED) {
extraBox = (
<div className='alert alert-success'>
<i className='fa fa-check'/>
<FormattedMessage
id='login.verified'
defaultMessage=' Email Verified'
/>
</div>
);
} else if (extraParam === Constants.SESSION_EXPIRED) {
extraBox = (
<div className='alert alert-warning'>
<i className='fa fa-exclamation-triangle'/>
<FormattedMessage
id='login.session_expired'
defaultMessage=' Your session has expired. Please login again.'
/>
</div>
);
}
@@ -91,7 +127,7 @@ export default class Login extends React.Component {
if (global.window.mm_config.EnableSignInWithEmail === 'true') {
emailSignup = (
<LoginEmail
teamName={this.props.teamName}
teamName={teamName}
/>
);
}
@@ -125,7 +161,7 @@ export default class Login extends React.Component {
}
let userSignUp = null;
if (this.props.inviteId) {
if (currentTeam.allow_open_invite) {
userSignUp = (
<div>
<span>
@@ -134,7 +170,7 @@ export default class Login extends React.Component {
defaultMessage="Don't have an account? "
/>
<a
href={'/signup_user_complete/?id=' + this.props.inviteId}
href={'/signup_user_complete/?id=' + currentTeam.invite_id}
className='signup-team-login'
>
<FormattedMessage
@@ -168,73 +204,65 @@ export default class Login extends React.Component {
if (global.window.mm_config.EnableLdap === 'true') {
ldapLogin = (
<LoginLdap
teamName={this.props.teamName}
teamName={teamName}
/>
);
}
let findTeams = null;
if (!Utils.isMobileApp()) {
findTeams = (
<div className='form-group margin--extra form-group--small'>
<span>
<a href='/find_team'>
<FormattedMessage
id='login.find'
defaultMessage='Find your other teams'
/>
</a></span>
</div>
);
}
let usernameLogin = null;
if (global.window.mm_config.EnableSignInWithUsername === 'true') {
usernameLogin = (
<LoginUsername
teamName={this.props.teamName}
teamName={teamName}
/>
);
}
return (
<div className='signup-team__container'>
<h5 className='margin--less'>
<FormattedMessage
id='login.signTo'
defaultMessage='Sign in to:'
/>
</h5>
<h2 className='signup-team__name'>{teamDisplayName}</h2>
<h2 className='signup-team__subdomain'>
<FormattedMessage
id='login.on'
defaultMessage='on {siteName}'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
</h2>
{extraBox}
{loginMessage}
{emailSignup}
{usernameLogin}
{ldapLogin}
{userSignUp}
{findTeams}
{forgotPassword}
{teamSignUp}
<div>
<div className='signup-header'>
<a href='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
/>
</a>
</div>
<div className='col-sm-12'>
<div className='signup-team__container'>
<h5 className='margin--less'>
<FormattedMessage
id='login.signTo'
defaultMessage='Sign in to:'
/>
</h5>
<h2 className='signup-team__name'>{teamDisplayName}</h2>
<h2 className='signup-team__subdomain'>
<FormattedMessage
id='login.on'
defaultMessage='on {siteName}'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
</h2>
{extraBox}
{loginMessage}
{emailSignup}
{usernameLogin}
{ldapLogin}
{userSignUp}
{forgotPassword}
{teamSignUp}
</div>
</div>
</div>
);
}
}
Login.defaultProps = {
teamName: '',
teamDisplayName: ''
};
Login.propTypes = {
teamName: React.PropTypes.string,
teamDisplayName: React.PropTypes.string,
inviteId: React.PropTypes.string
params: React.PropTypes.object.isRequired
};

Просмотреть файл

@@ -4,6 +4,7 @@
import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
import {browserHistory} from 'react-router';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
@@ -72,13 +73,7 @@ class LoginEmail extends React.Component {
Client.loginByEmail(name, email, password,
() => {
UserStore.setLastEmail(email);
const redirect = Utils.getUrlParameter('redirect');
if (redirect) {
window.location.href = decodeURIComponent(redirect);
} else {
window.location.href = '/' + name + '/channels/town-square';
}
browserHistory.push('/' + name + '/channels/town-square');
},
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
@@ -167,4 +162,4 @@ LoginEmail.propTypes = {
teamName: React.PropTypes.string.isRequired
};
export default injectIntl(LoginEmail);
export default injectIntl(LoginEmail);

Просмотреть файл

@@ -56,9 +56,13 @@ export default class Navbar extends React.Component {
return {
channel: ChannelStore.getCurrent(),
member: ChannelStore.getCurrentMember(),
users: ChannelStore.getCurrentExtraInfo().members
users: ChannelStore.getCurrentExtraInfo().members,
currentUser: UserStore.getCurrentUser()
};
}
stateValid() {
return this.state.channel && this.state.member && this.state.users && this.state.currentUser;
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addExtraInfoChangeListener(this.onChange);
@@ -201,7 +205,7 @@ export default class Navbar extends React.Component {
<ToggleModalButton
role='menuitem'
dialogType={ChannelInviteModal}
dialogProps={{channel}}
dialogProps={{channel, currentUser: this.state.currentUser}}
>
<FormattedMessage
id='navbar.addMembers'
@@ -286,7 +290,11 @@ export default class Navbar extends React.Component {
<ToggleModalButton
role='menuitem'
dialogType={ChannelNotificationsModal}
dialogProps={{channel}}
dialogProps={{
channel,
channelMember: this.state.member,
currentUser: this.state.currentUser
}}
>
<FormattedMessage
id='navbar.preferences'
@@ -412,7 +420,11 @@ export default class Navbar extends React.Component {
return buttons;
}
render() {
var currentId = UserStore.getCurrentId();
if (!this.stateValid()) {
return null;
}
var currentId = this.state.currentUser.id;
var channel = this.state.channel;
var channelTitle = this.props.teamDisplayName;
var popoverContent;

Просмотреть файл

@@ -2,10 +2,7 @@
// See License.txt for license information.
import * as Utils from '../utils/utils.jsx';
import * as client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
import TeamStore from '../stores/team_store.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import AboutBuildModal from './about_build_modal.jsx';
import TeamMembersModal from './team_members_modal.jsx';
@@ -15,38 +12,20 @@ import UserSettingsModal from './user_settings/user_settings_modal.jsx';
import Constants from '../utils/constants.jsx';
import {FormattedMessage} from 'mm-intl';
function getStateFromStores() {
const teams = [];
const teamsObject = UserStore.getTeams();
for (const teamId in teamsObject) {
if (teamsObject.hasOwnProperty(teamId)) {
teams.push(teamsObject[teamId]);
}
}
teams.sort(Utils.sortByDisplayName);
return {teams};
}
import {Link} from 'react-router';
export default class NavbarDropdown extends React.Component {
constructor(props) {
super(props);
this.blockToggle = false;
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.handleAboutModal = this.handleAboutModal.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
this.aboutModalDismissed = this.aboutModalDismissed.bind(this);
const state = getStateFromStores();
state.showUserSettingsModal = false;
state.showAboutModal = false;
this.state = state;
}
handleLogoutClick(e) {
e.preventDefault();
client.logout();
this.state = {
showUserSettingsModal: false,
showAboutModal: false
};
}
handleAboutModal() {
this.setState({showAboutModal: true});
@@ -55,9 +34,6 @@ export default class NavbarDropdown extends React.Component {
this.setState({showAboutModal: false});
}
componentDidMount() {
UserStore.addTeamsChangeListener(this.onListenerChange);
TeamStore.addChangeListener(this.onListenerChange);
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
$('.sidebar--left .dropdown-menu').scrollTop(0);
this.blockToggle = true;
@@ -67,24 +43,15 @@ export default class NavbarDropdown extends React.Component {
});
}
componentWillUnmount() {
UserStore.removeTeamsChangeListener(this.onListenerChange);
TeamStore.removeChangeListener(this.onListenerChange);
$(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
}
onListenerChange() {
var newState = getStateFromStores();
if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
render() {
var teamLink = '';
var inviteLink = '';
var manageLink = '';
var sysAdminLink = '';
var adminDivider = '';
var currentUser = UserStore.getCurrentUser();
var currentUser = this.props.currentUser;
var isAdmin = false;
var isSystemAdmin = false;
var teamSettings = null;
@@ -97,7 +64,7 @@ export default class NavbarDropdown extends React.Component {
<li>
<a
href='#'
onClick={EventHelpers.showInviteMemberModal}
onClick={GlobalActions.showInviteMemberModal}
>
<FormattedMessage
id='navbar_dropdown.inviteMember'
@@ -112,7 +79,7 @@ export default class NavbarDropdown extends React.Component {
<li>
<a
href='#'
onClick={EventHelpers.showGetTeamInviteLinkModal}
onClick={GlobalActions.showGetTeamInviteLinkModal}
>
<FormattedMessage
id='navbar_dropdown.teamLink'
@@ -158,7 +125,7 @@ export default class NavbarDropdown extends React.Component {
sysAdminLink = (
<li>
<a
href={'/admin_console?' + Utils.getSessionIndex()}
href={'/admin_console'}
>
<FormattedMessage
id='navbar_dropdown.console'
@@ -171,31 +138,6 @@ export default class NavbarDropdown extends React.Component {
var teams = [];
if (this.state.teams.length > 1) {
teams.push(
<li
className='divider'
key='div'
>
</li>
);
this.state.teams.forEach((team) => {
if (team.name !== this.props.teamName) {
teams.push(
<li key={team.name}><a href={Utils.getWindowLocationOrigin() + '/' + team.name}>
<FormattedMessage
id='navbar_dropdown.switchTeam'
defaultMessage='Switch to {team}'
values={{
team: team.display_name
}}
/>
</a></li>);
}
});
}
if (global.window.mm_config.EnableTeamCreation === 'true') {
teams.push(
<li key='newTeam_li'>
@@ -283,15 +225,12 @@ export default class NavbarDropdown extends React.Component {
{inviteLink}
{teamLink}
<li>
<a
href='#'
onClick={this.handleLogoutClick}
>
<Link to={'/' + this.props.teamName + '/logout'}>
<FormattedMessage
id='navbar_dropdown.logout'
defaultMessage='Logout'
/>
</a>
</Link>
</li>
{adminDivider}
{teamSettings}
@@ -333,5 +272,6 @@ NavbarDropdown.defaultProps = {
NavbarDropdown.propTypes = {
teamType: React.PropTypes.string,
teamDisplayName: React.PropTypes.string,
teamName: React.PropTypes.string
teamName: React.PropTypes.string,
currentUser: React.PropTypes.object
};

20
web/react/components/needs_team.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,20 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as GlobalActions from '../action_creators/global_actions.jsx';
export default class NeedsTeam extends React.Component {
componentWillMount() {
GlobalActions.loadTeamRequiredPage();
}
render() {
return this.props.children;
}
}
NeedsTeam.defaultProps = {
};
NeedsTeam.propTypes = {
children: React.PropTypes.object
};

70
web/react/components/not_logged_in.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,70 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {FormattedMessage} from 'mm-intl';
export default class NotLoggedIn extends React.Component {
componentDidMount() {
$('body').attr('class', 'white');
$('#root').attr('class', 'container-fluid');
}
componentWillUnmount() {
$('body').attr('class', '');
$('#root').attr('class', '');
}
render() {
return (
<div className='inner__wrap'>
<div className='row content'>
{this.props.children}
<div className='footer-push'></div>
</div>
<div className='row footer'>
<div className='footer-pane col-xs-12'>
<div className='col-xs-12'>
<span className='pull-right footer-site-name'>{global.window.mm_config.SiteName}</span>
</div>
<div className='col-xs-12'>
<span className='pull-right footer-link copyright'>{'© 2015 Mattermost, Inc.'}</span>
<a
id='help_link'
className='pull-right footer-link'
href={global.window.mm_config.HelpLink}
>
<FormattedMessage id='web.footer.help'/>
</a>
<a
id='terms_link'
className='pull-right footer-link'
href={global.window.mm_config.TermsOfServiceLink}
>
<FormattedMessage id='web.footer.terms'/>
</a>
<a
id='privacy_link'
className='pull-right footer-link'
href={global.window.mm_config.PrivacyPolicyLink}
>
<FormattedMessage id='web.footer.privacy'/>
</a>
<a
id='about_link'
className='pull-right footer-link'
href={global.window.mm_config.AboutLink}
>
<FormattedMessage id='web.footer.about'/>
</a>
</div>
</div>
</div>
</div>
);
}
}
NotLoggedIn.defaultProps = {
};
NotLoggedIn.propTypes = {
children: React.PropTypes.object
};

Просмотреть файл

@@ -1,47 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PasswordResetSendLink from './password_reset_send_link.jsx';
import PasswordResetForm from './password_reset_form.jsx';
export default class PasswordReset extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
if (this.props.isReset === 'false') {
return (
<PasswordResetSendLink
teamDisplayName={this.props.teamDisplayName}
teamName={this.props.teamName}
/>
);
}
return (
<PasswordResetForm
teamDisplayName={this.props.teamDisplayName}
teamName={this.props.teamName}
hash={this.props.hash}
data={this.props.data}
/>
);
}
}
PasswordReset.defaultProps = {
isReset: '',
teamName: '',
teamDisplayName: '',
hash: '',
data: ''
};
PasswordReset.propTypes = {
isReset: React.PropTypes.string,
teamName: React.PropTypes.string,
teamDisplayName: React.PropTypes.string,
hash: React.PropTypes.string,
data: React.PropTypes.string
};

Просмотреть файл

@@ -2,24 +2,11 @@
// See License.txt for license information.
import * as Client from '../utils/client.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
const holders = defineMessages({
error: {
id: 'password_form.error',
defaultMessage: 'Please enter at least {chars} characters.'
},
update: {
id: 'password_form.update',
defaultMessage: 'Your password has been updated successfully.'
},
pwd: {
id: 'password_form.pwd',
defaultMessage: 'Password'
}
});
import {FormattedMessage} from 'mm-intl';
import {browserHistory} from 'react-router';
class PasswordResetForm extends React.Component {
constructor(props) {
@@ -32,51 +19,50 @@ class PasswordResetForm extends React.Component {
handlePasswordReset(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
var state = {};
var password = ReactDOM.findDOMNode(this.refs.password).value.trim();
const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) {
state.error = formatMessage(holders.error, {chars: Constants.MIN_PASSWORD_LENGTH});
this.setState(state);
this.setState({
error: (
<FormattedMessage
id='password_form.error'
defaultMessage='Please enter at least {chars} characters.'
chars={Constants.MIN_PASSWORD_LENGTH}
/>
)
});
return;
}
state.error = null;
this.setState(state);
this.setState({
error: null
});
var data = {};
const data = {};
data.new_password = password;
data.hash = this.props.hash;
data.data = this.props.data;
data.name = this.props.teamName;
data.hash = this.props.location.query.h;
data.data = this.props.location.query.d;
data.name = this.props.params.team;
Client.resetPassword(data,
function resetSuccess() {
this.setState({error: null, updateText: formatMessage(holders.update)});
}.bind(this),
function resetFailure(err) {
this.setState({error: err.message, updateText: null});
}.bind(this)
() => {
this.setState({error: null});
browserHistory.push('/' + this.props.params.team + '/login');
},
(err) => {
this.setState({error: err.message});
}
);
}
render() {
var updateText = null;
if (this.state.updateText) {
updateText = (<div className='form-group'><br/><label className='control-label reset-form'>{this.state.updateText}
<FormattedHTMLMessage
id='password_form.click'
defaultMessage='Click <a href={url}>here</a> to log in.'
values={{
url: '/' + this.props.teamName + '/login'
}}
/>
</label></div>);
}
var error = null;
if (this.state.error) {
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
error = (
<div className='form-group has-error'>
<label className='control-label'>
{this.state.error}
</label>
</div>
);
}
var formClass = 'form-group';
@@ -84,7 +70,6 @@ class PasswordResetForm extends React.Component {
formClass += ' has-error';
}
const {formatMessage} = this.props.intl;
return (
<div className='col-sm-12'>
<div className='signup-team__container'>
@@ -98,9 +83,8 @@ class PasswordResetForm extends React.Component {
<p>
<FormattedMessage
id='password_form.enter'
defaultMessage='Enter a new password for your {teamDisplayName} {siteName} account.'
defaultMessage='Enter a new password for your {siteName} account.'
values={{
teamDisplayName: this.props.teamDisplayName,
siteName: global.window.mm_config.SiteName
}}
/>
@@ -111,7 +95,10 @@ class PasswordResetForm extends React.Component {
className='form-control'
name='password'
ref='password'
placeholder={formatMessage(holders.pwd)}
placeholder={Utils.localizeMessage(
'password_form.pwd',
'Password'
)}
spellCheck='false'
/>
</div>
@@ -125,7 +112,6 @@ class PasswordResetForm extends React.Component {
defaultMessage='Change my password'
/>
</button>
{updateText}
</form>
</div>
</div>
@@ -134,17 +120,10 @@ class PasswordResetForm extends React.Component {
}
PasswordResetForm.defaultProps = {
teamName: '',
teamDisplayName: '',
hash: '',
data: ''
};
PasswordResetForm.propTypes = {
intl: intlShape.isRequired,
teamName: React.PropTypes.string,
teamDisplayName: React.PropTypes.string,
hash: React.PropTypes.string,
data: React.PropTypes.string
params: React.PropTypes.object.isRequired,
location: React.PropTypes.object.isRequired
};
export default injectIntl(PasswordResetForm);
export default PasswordResetForm;

Просмотреть файл

@@ -4,26 +4,7 @@
import * as Utils from '../utils/utils.jsx';
import * as client from '../utils/client.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
const holders = defineMessages({
error: {
id: 'password_send.error',
defaultMessage: 'Please enter a valid email address.'
},
link: {
id: 'password_send.link',
defaultMessage: '<p>A password reset link has been sent to <b>{email}</b> for your <b>{teamDisplayName}</b> team on {hostname}.</p>'
},
checkInbox: {
id: 'password_send.checkInbox',
defaultMessage: 'Please check your inbox.'
},
email: {
id: 'password_send.email',
defaultMessage: 'Email'
}
});
import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
class PasswordResetSendLink extends React.Component {
constructor(props) {
@@ -31,48 +12,64 @@ class PasswordResetSendLink extends React.Component {
this.handleSendLink = this.handleSendLink.bind(this);
this.state = {};
this.state = {
error: '',
updateText: ''
};
}
handleSendLink(e) {
e.preventDefault();
var state = {};
const {formatMessage} = this.props.intl;
var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
if (!email || !Utils.isEmail(email)) {
state.error = formatMessage(holders.error);
this.setState(state);
this.setState({
error: (
<FormattedMessage
id={'password_send.error'}
defaultMessage={'Please enter a valid email address.'}
/>
)
});
return;
}
state.error = null;
this.setState(state);
// End of error checking clear error
this.setState({
error: ''
});
var data = {};
data.email = email;
data.name = this.props.teamName;
data.name = this.props.params.team;
client.sendPasswordReset(data,
function passwordResetSent() {
this.setState({error: null, updateText: formatMessage(holders.link, {email: email, teamDisplayName: this.props.teamDisplayName, hostname: window.location.hostname}), moreUpdateText: formatMessage(holders.checkInbox)});
$(ReactDOM.findDOMNode(this.refs.reset_form)).hide();
}.bind(this),
function passwordResetFailedToSend(err) {
this.setState({error: err.message, update_text: null, moreUpdateText: null});
}.bind(this)
);
() => {
this.setState({
error: null,
updateText: (
<div className='reset-form alert alert-success'>
<FormattedHTMLMessage
id='password_send.link'
defaultMessage='<p>A password reset link has been sent to <b>{email}</b></p>'
email={email}
/>
<FormattedMessage
id={'password_send.checkInbox'}
defaultMessage={'Please check your inbox.'}
/>
</div>
)
});
$(ReactDOM.findDOMNode(this.refs.reset_form)).hide();
},
(err) => {
this.setState({
error: err.message,
update_text: null
});
}
);
}
render() {
var updateText = null;
if (this.state.updateText) {
updateText = (
<div className='reset-form alert alert-success'
dangerouslySetInnerHTML={{__html: this.state.updateText + this.state.moreUpdateText}}
>
</div>
);
}
var error = null;
if (this.state.error) {
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
@@ -83,51 +80,60 @@ class PasswordResetSendLink extends React.Component {
formClass += ' has-error';
}
const {formatMessage} = this.props.intl;
return (
<div className='col-sm-12'>
<div className='signup-team__container'>
<h3>
<div>
<div className='signup-header'>
<a href='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='password_send.title'
defaultMessage='Password Reset'
id='web.header.back'
/>
</h3>
{updateText}
<form
onSubmit={this.handleSendLink}
ref='reset_form'
>
<p>
</a>
</div>
<div className='col-sm-12'>
<div className='signup-team__container'>
<h3>
<FormattedMessage
id='password_send.description'
defaultMessage='To reset your password, enter the email address you used to sign up for {teamName}.'
values={{
teamName: this.props.teamDisplayName
}}
id='password_send.title'
defaultMessage='Password Reset'
/>
</p>
<div className={formClass}>
<input
type='email'
className='form-control'
name='email'
ref='email'
placeholder={formatMessage(holders.email)}
spellCheck='false'
/>
</div>
{error}
<button
type='submit'
className='btn btn-primary'
</h3>
{this.state.updateText}
<form
onSubmit={this.handleSendLink}
ref='reset_form'
>
<FormattedMessage
id='password_send.reset'
defaultMessage='Reset my password'
/>
</button>
</form>
<p>
<FormattedMessage
id='password_send.description'
defaultMessage='To reset your password, enter the email address you used to sign up'
/>
</p>
<div className={formClass}>
<input
type='email'
className='form-control'
name='email'
ref='email'
placeholder={Utils.localizeMessage(
'password_send.email',
'Email'
)}
spellCheck='false'
/>
</div>
{error}
<button
type='submit'
className='btn btn-primary'
>
<FormattedMessage
id='password_send.reset'
defaultMessage='Reset my password'
/>
</button>
</form>
</div>
</div>
</div>
);
@@ -135,13 +141,9 @@ class PasswordResetSendLink extends React.Component {
}
PasswordResetSendLink.defaultProps = {
teamName: '',
teamDisplayName: ''
};
PasswordResetSendLink.propTypes = {
intl: intlShape.isRequired,
teamName: React.PropTypes.string,
teamDisplayName: React.PropTypes.string
params: React.PropTypes.object.isRequired
};
export default injectIntl(PasswordResetSendLink);
export default PasswordResetSendLink;

Просмотреть файл

@@ -118,7 +118,7 @@ export default class PopoverListMembers extends React.Component {
className='profile-img rounded pull-left'
width='26px'
height='26px'
src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`}
src={`/api/v1/users/${m.id}/image?time=${m.update_at}`}
/>
<div className='pull-left'>
<div

Просмотреть файл

@@ -4,7 +4,6 @@
import PostHeader from './post_header.jsx';
import PostBody from './post_body.jsx';
import UserStore from '../stores/user_store.jsx';
import PostStore from '../stores/post_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
@@ -128,7 +127,6 @@ export default class Post extends React.Component {
const post = this.props.post;
const parentPost = this.props.parentPost;
const posts = this.props.posts;
const user = this.props.user || {};
if (!post.props) {
post.props = {};
@@ -156,13 +154,15 @@ export default class Post extends React.Component {
}
let currentUserCss = '';
if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook && !Utils.isSystemMessage(post)) {
if (this.props.currentUser.id === post.user_id && !post.props.from_webhook && !Utils.isSystemMessage(post)) {
currentUserCss = 'current--user';
}
let timestamp = user.update_at;
if (timestamp == null) {
timestamp = UserStore.getCurrentUser().update_at;
let timestamp = 0;
if (!this.props.user || this.props.user.update_at == null) {
timestamp = this.props.currentUser.update_at;
} else {
timestamp = this.props.user.update_at;
}
let sameUserClass = '';
@@ -182,7 +182,7 @@ export default class Post extends React.Component {
let profilePic = null;
if (!this.props.hideProfilePic) {
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex();
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp;
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
if (post.props.override_icon_url) {
src = post.props.override_icon_url;
@@ -218,6 +218,7 @@ export default class Post extends React.Component {
isLastComment={this.props.isLastComment}
sameUser={this.props.sameUser}
user={this.props.user}
currentUser={this.props.currentUser}
/>
<PostBody
post={post}
@@ -245,5 +246,7 @@ Post.propTypes = {
hideProfilePic: React.PropTypes.bool,
isLastComment: React.PropTypes.bool,
shouldHighlight: React.PropTypes.bool,
displayNameType: React.PropTypes.string
displayNameType: React.PropTypes.string,
hasProfiles: React.PropTypes.bool,
currentUser: React.PropTypes.object.isRequired
};

Просмотреть файл

@@ -80,12 +80,10 @@ class PostBody extends React.Component {
username = parentPost.props.override_username;
}
if (global.window.mm_locale === 'en') {
if (username.slice(-1) === 's') {
apostrophe = '\'';
} else {
apostrophe = '\'s';
}
if (username.slice(-1) === 's') {
apostrophe = '\'';
} else {
apostrophe = '\'s';
}
name = (
<a

Просмотреть файл

@@ -5,7 +5,8 @@ import PostsView from './posts_view.jsx';
import PostStore from '../stores/post_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import UserStore from '../stores/user_store.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -15,6 +16,7 @@ export default class PostFocusView extends React.Component {
this.onChannelChange = this.onChannelChange.bind(this);
this.onPostsChange = this.onPostsChange.bind(this);
this.onUserChange = this.onUserChange.bind(this);
this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
@@ -26,18 +28,21 @@ export default class PostFocusView extends React.Component {
scrollPostId: focusedPostId,
postList: PostStore.getVisiblePosts(focusedPostId),
atTop: PostStore.getVisibilityAtTop(focusedPostId),
atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
atBottom: PostStore.getVisibilityAtBottom(focusedPostId),
currentUser: UserStore.getCurrentUser()
};
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChannelChange);
PostStore.addChangeListener(this.onPostsChange);
UserStore.addChangeListener(this.onUserChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChannelChange);
PostStore.removeChangeListener(this.onPostsChange);
UserStore.removeChangeListener(this.onUserChange);
}
onChannelChange() {
@@ -46,6 +51,10 @@ export default class PostFocusView extends React.Component {
});
}
onUserChange() {
this.setState({currentUser: UserStore.getCurrentUser()});
}
onPostsChange() {
const focusedPostId = PostStore.getFocusedPostId();
if (focusedPostId == null) {
@@ -65,11 +74,11 @@ export default class PostFocusView extends React.Component {
}
loadMorePostsTop() {
EventHelpers.emitLoadMorePostsFocusedTopEvent();
GlobalActions.emitLoadMorePostsFocusedTopEvent();
}
loadMorePostsBottom() {
EventHelpers.emitLoadMorePostsFocusedBottomEvent();
GlobalActions.emitLoadMorePostsFocusedBottomEvent();
}
getIntroMessage() {
@@ -89,6 +98,10 @@ export default class PostFocusView extends React.Component {
const postsToHighlight = {};
postsToHighlight[this.state.scrollPostId] = true;
if (!this.state.currentUser || !this.state.postList) {
return null;
}
return (
<div id='post-list'>
<PostsView
@@ -106,6 +119,7 @@ export default class PostFocusView extends React.Component {
messageSeparatorTime={0}
postsToHighlight={postsToHighlight}
profiles={this.props.profiles}
currentUser={this.state.currentUser}
/>
</div>
);

Просмотреть файл

@@ -14,16 +14,15 @@ export default class PostHeader extends React.Component {
}
render() {
const post = this.props.post;
const user = this.props.user;
let userProfile = <UserProfile user={user}/>;
let userProfile = <UserProfile user={this.props.user}/>;
let botIndicator;
if (post.props && post.props.from_webhook) {
if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') {
userProfile = (
<UserProfile
user={user}
user={this.props.user}
overwriteName={post.props.override_username}
disablePopover={true}
/>
@@ -54,6 +53,7 @@ export default class PostHeader extends React.Component {
allowReply='true'
isLastComment={this.props.isLastComment}
sameUser={this.props.sameUser}
currentUser={this.props.currentUser}
/>
</li>
</ul>
@@ -68,10 +68,11 @@ PostHeader.defaultProps = {
sameUser: false
};
PostHeader.propTypes = {
post: React.PropTypes.object,
post: React.PropTypes.object.isRequired,
user: React.PropTypes.object,
commentCount: React.PropTypes.number,
isLastComment: React.PropTypes.bool,
handleCommentClick: React.PropTypes.func,
sameUser: React.PropTypes.bool
currentUser: React.PropTypes.object.isRequired,
commentCount: React.PropTypes.number.isRequired,
isLastComment: React.PropTypes.bool.isRequired,
handleCommentClick: React.PropTypes.func.isRequired,
sameUser: React.PropTypes.bool.isRequired
};

Просмотреть файл

@@ -1,10 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import UserStore from '../stores/user_store.jsx';
import * as Utils from '../utils/utils.jsx';
import TimeSince from './time_since.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import Constants from '../utils/constants.jsx';
@@ -27,8 +26,8 @@ export default class PostInfo extends React.Component {
}
createDropdown() {
var post = this.props.post;
var isOwner = UserStore.getCurrentId() === post.user_id;
var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles);
var isOwner = this.props.currentUser.id === post.user_id;
var isAdmin = Utils.isAdmin(this.props.currentUser.roles);
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) {
return '';
@@ -47,21 +46,21 @@ export default class PostInfo extends React.Component {
if (this.props.allowReply === 'true') {
dropdownContents.push(
<li
key='replyLink'
role='presentation'
>
<a
className='link__reply theme'
href='#'
onClick={this.props.handleCommentClick}
>
<FormattedMessage
id='post_info.reply'
defaultMessage='Reply'
/>
</a>
</li>
<li
key='replyLink'
role='presentation'
>
<a
className='link__reply theme'
href='#'
onClick={this.props.handleCommentClick}
>
<FormattedMessage
id='post_info.reply'
defaultMessage='Reply'
/>
</a>
</li>
);
}
@@ -93,7 +92,7 @@ export default class PostInfo extends React.Component {
<a
href='#'
role='menuitem'
onClick={() => EventHelpers.showDeletePostModal(post, dataComments)}
onClick={() => GlobalActions.showDeletePostModal(post, dataComments)}
>
<FormattedMessage
id='post_info.del'
@@ -157,11 +156,11 @@ export default class PostInfo extends React.Component {
handlePermalink(e) {
e.preventDefault();
EventHelpers.showGetPostLinkModal(this.props.post);
GlobalActions.showGetPostLinkModal(this.props.post);
}
removePost() {
EventHelpers.emitRemovePost(this.props.post);
GlobalActions.emitRemovePost(this.props.post);
}
createRemovePostButton(post) {
if (!Utils.isPostEphemeral(post)) {
@@ -240,10 +239,11 @@ PostInfo.defaultProps = {
sameUser: false
};
PostInfo.propTypes = {
post: React.PropTypes.object,
commentCount: React.PropTypes.number,
isLastComment: React.PropTypes.bool,
allowReply: React.PropTypes.string,
handleCommentClick: React.PropTypes.func,
sameUser: React.PropTypes.bool
post: React.PropTypes.object.isRequired,
commentCount: React.PropTypes.number.isRequired,
isLastComment: React.PropTypes.bool.isRequired,
allowReply: React.PropTypes.string.isRequired,
handleCommentClick: React.PropTypes.func.isRequired,
sameUser: React.PropTypes.bool.isRequired,
currentUser: React.PropTypes.object.isRequired
};

Просмотреть файл

@@ -1,9 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import UserStore from '../stores/user_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import * as Utils from '../utils/utils.jsx';
import Post from './post.jsx';
import Constants from '../utils/constants.jsx';
@@ -144,7 +143,7 @@ export default class PostsView extends React.Component {
createPosts(posts, order) {
const postCtls = [];
let previousPostDay = new Date(0);
const userId = UserStore.getCurrentId();
const userId = this.props.currentUser.id;
const profiles = this.props.profiles || {};
let renderedLastViewed = false;
@@ -230,8 +229,8 @@ export default class PostsView extends React.Component {
const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id);
let profile;
if (UserStore.getCurrentId() === post.user_id) {
profile = UserStore.getCurrentUser();
if (this.props.currentUser.id === post.user_id) {
profile = this.props.currentUser;
} else {
profile = profiles[post.user_id];
}
@@ -248,9 +247,10 @@ export default class PostsView extends React.Component {
hideProfilePic={hideProfilePic}
isLastComment={isLastComment}
shouldHighlight={shouldHighlight}
onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func
onClick={() => GlobalActions.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func
displayNameType={this.state.displayNameType}
user={profile}
currentUser={this.props.currentUser}
/>
);
@@ -525,7 +525,7 @@ PostsView.defaultProps = {
PostsView.propTypes = {
isActive: React.PropTypes.bool,
postList: React.PropTypes.object,
profiles: React.PropTypes.object,
profiles: React.PropTypes.object.isRequired,
scrollPostId: React.PropTypes.string,
scrollType: React.PropTypes.number,
postViewScrolled: React.PropTypes.func.isRequired,
@@ -535,7 +535,8 @@ PostsView.propTypes = {
showMoreMessagesBottom: React.PropTypes.bool,
introText: React.PropTypes.element,
messageSeparatorTime: React.PropTypes.number,
postsToHighlight: React.PropTypes.object
postsToHighlight: React.PropTypes.object,
currentUser: React.PropTypes.object.isRequired
};
function FloatingTimestamp({isScrolling, post}) {

Просмотреть файл

@@ -6,9 +6,10 @@ import LoadingScreen from './loading_screen.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import PostStore from '../stores/post_store.jsx';
import UserStore from '../stores/user_store.jsx';
import * as Utils from '../utils/utils.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import Constants from '../utils/constants.jsx';
@@ -21,6 +22,7 @@ export default class PostsViewContainer extends React.Component {
this.onChannelChange = this.onChannelChange.bind(this);
this.onChannelLeave = this.onChannelLeave.bind(this);
this.onPostsChange = this.onPostsChange.bind(this);
this.onUserChange = this.onUserChange.bind(this);
this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this);
@@ -28,7 +30,8 @@ export default class PostsViewContainer extends React.Component {
const currentChannelId = ChannelStore.getCurrentId();
const state = {
scrollType: PostsView.SCROLL_TYPE_BOTTOM,
scrollPost: null
scrollPost: null,
currentUser: UserStore.getCurrentUser()
};
if (currentChannelId) {
Object.assign(state, {
@@ -54,12 +57,17 @@ export default class PostsViewContainer extends React.Component {
ChannelStore.addLeaveListener(this.onChannelLeave);
PostStore.addChangeListener(this.onPostsChange);
PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest);
UserStore.addChangeListener(this.onUserChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChannelChange);
ChannelStore.removeLeaveListener(this.onChannelLeave);
PostStore.removeChangeListener(this.onPostsChange);
PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest);
UserStore.removeChangeListener(this.onUserChange);
}
onUserChange() {
this.setState({currentUser: UserStore.getCurrentUser()});
}
handlePostsViewJumpRequest(type, post) {
switch (type) {
@@ -139,7 +147,7 @@ export default class PostsViewContainer extends React.Component {
return PostStore.getVisiblePosts(id);
}
loadMorePostsTop() {
EventHelpers.emitLoadMorePostsEvent();
GlobalActions.emitLoadMorePostsEvent();
}
handlePostsViewScroll(atBottom) {
if (atBottom) {
@@ -165,6 +173,10 @@ export default class PostsViewContainer extends React.Component {
const currentChannelId = channels[this.state.currentChannelIndex];
const channel = ChannelStore.get(currentChannelId);
if (!this.state.currentUser || !channel) {
return null;
}
const postListCtls = [];
for (let i = 0; i < channels.length; i++) {
const isActive = (channels[i] === currentChannelId);
@@ -185,6 +197,7 @@ export default class PostsViewContainer extends React.Component {
introText={channel ? createChannelIntroMessage(channel) : null}
messageSeparatorTime={this.state.currentLastViewed}
profiles={this.props.profiles}
currentUser={this.state.currentUser}
/>
);
if (!postLists[i] && isActive) {

Просмотреть файл

@@ -14,7 +14,7 @@ import * as AsyncClient from '../utils/async_client.jsx';
var ActionTypes = Constants.ActionTypes;
import * as TextFormatting from '../utils/text_formatting.jsx';
import twemoji from 'twemoji';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl';
@@ -70,7 +70,7 @@ class RhsComment extends React.Component {
}
handlePermalink(e) {
e.preventDefault();
EventHelpers.showGetPostLinkModal(this.props.post);
GlobalActions.showGetPostLinkModal(this.props.post);
}
componentDidMount() {
this.parseEmojis();
@@ -151,7 +151,7 @@ class RhsComment extends React.Component {
<a
href='#'
role='menuitem'
onClick={() => EventHelpers.showDeletePostModal(post, 0)}
onClick={() => GlobalActions.showDeletePostModal(post, 0)}
>
<FormattedMessage
id='rhs_comment.del'
@@ -253,7 +253,7 @@ class RhsComment extends React.Component {
<div className='post__content'>
<div className='post__img'>
<img
src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp}
height='36'
width='36'
/>

Просмотреть файл

@@ -10,7 +10,7 @@ import * as Emoji from '../utils/emoticons.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
import twemoji from 'twemoji';
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import Constants from '../utils/constants.jsx';
@@ -34,7 +34,7 @@ export default class RhsRootPost extends React.Component {
}
handlePermalink(e) {
e.preventDefault();
EventHelpers.showGetPostLinkModal(this.props.post);
GlobalActions.showGetPostLinkModal(this.props.post);
}
componentDidMount() {
this.parseEmojis();
@@ -142,7 +142,7 @@ export default class RhsRootPost extends React.Component {
<a
href='#'
role='menuitem'
onClick={() => EventHelpers.showDeletePostModal(post, this.props.commentCount)}
onClick={() => GlobalActions.showDeletePostModal(post, this.props.commentCount)}
>
<FormattedMessage
id='rhs_root.del'
@@ -211,7 +211,7 @@ export default class RhsRootPost extends React.Component {
);
}
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex();
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp;
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
if (post.props.override_icon_url) {
src = post.props.override_icon_url;

90
web/react/components/root.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,90 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as GlobalActions from '../action_creators/global_actions.jsx';
import BrowserStore from '../stores/browser_store.jsx';
import LocalizationStore from '../stores/localization_store.jsx';
var IntlProvider = ReactIntl.IntlProvider;
export default class Root extends React.Component {
constructor(props) {
super(props);
this.state = {
locale: 'en',
translations: null
};
this.localizationChanged = this.localizationChanged.bind(this);
}
localizationChanged() {
this.setState({locale: LocalizationStore.getLocale(), translations: LocalizationStore.getTranslations()});
}
componentWillMount() {
// Setup localization listener
LocalizationStore.addChangeListener(this.localizationChanged);
// Browser store check version
BrowserStore.checkVersion();
window.onerror = (msg, url, line, column, stack) => {
var l = {};
l.level = 'ERROR';
l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url;
$.ajax({
url: '/api/v1/admin/log_client',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(l)
});
if (window.mm_config.EnableDeveloper === 'true') {
window.ErrorStore.storeLastError({message: 'DEVELOPER MODE: A javascript error has occured. Please use the javascript console to capture and report the error (row: ' + line + ' col: ' + column + ').'});
window.ErrorStore.emitChange();
}
};
// Ya....
/*eslint-disable */
if (window.mm_config.SegmentDeveloperKey != null && window.mm_config.SegmentDeveloperKey !== "") {
!function(){var analytics=global.window.analytics=global.window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1";
analytics.load(window.mm_config.SegmentDeveloperKey);
analytics.page();
}}();
} else {
global.window.analytics = {};
global.window.analytics.page = function(){};
global.window.analytics.track = function(){};
}
/*eslint-enable */
// Get our localizaiton
GlobalActions.newLocalizationSelected('en');
}
componentWillUnmount() {
LocalizationStore.removeChangeListener(this.localizationChanged);
}
render() {
if (this.state.translations == null) {
return <div/>;
}
return (
<IntlProvider
locale={this.state.locale}
messages={this.state.translations}
key={this.state.locale}
>
{this.props.children}
</IntlProvider>
);
}
}
Root.defaultProps = {
};
Root.propTypes = {
children: React.PropTypes.object
};

Просмотреть файл

@@ -3,8 +3,7 @@
import UserStore from '../stores/user_store.jsx';
import UserProfile from './user_profile.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as utils from '../utils/utils.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import * as TextFormatting from '../utils/text_formatting.jsx';
import Constants from '../utils/constants.jsx';
@@ -22,7 +21,7 @@ export default class SearchResultsItem extends React.Component {
handleClick(e) {
e.preventDefault();
EventHelpers.emitPostFocusEvent(this.props.post.id);
GlobalActions.emitPostFocusEvent(this.props.post.id);
if ($(window).width() < 768) {
$('.sidebar--right').removeClass('move--left');
@@ -32,7 +31,7 @@ export default class SearchResultsItem extends React.Component {
handleFocusRHSClick(e) {
e.preventDefault();
EventHelpers.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch);
GlobalActions.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch);
}
render() {
@@ -78,7 +77,7 @@ export default class SearchResultsItem extends React.Component {
<div className='post__content'>
<div className='post__img'>
<img
src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex()}
src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp}
height='36'
width='36'
/>

111
web/react/components/should_verify_email.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,111 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {FormattedMessage} from 'mm-intl';
import * as Client from '../utils/client.jsx';
export default class ShouldVerifyEmail extends React.Component {
constructor(props) {
super(props);
this.handleResend = this.handleResend.bind(this);
this.state = {
resendStatus: 'none'
};
}
handleResend() {
const teamName = this.props.location.query.teamname;
const email = this.props.location.query.email;
this.setState({resendStatus: 'sending'});
Client.resendVerification(() => {
this.setState({resendStatus: 'success'});
},
() => {
this.setState({resendStatus: 'failure'});
},
teamName,
email);
}
render() {
let resendConfirm = '';
if (this.state.resendStatus === 'success') {
resendConfirm = (
<div>
<br/>
<p className='alert alert-success'>
<i className='fa fa-check'/>
<FormattedMessage
id='email_verify.sent'
defaultMessage=' Verification email sent.'
/>
</p>
</div>
);
}
if (this.state.resendStatus === 'failure') {
resendConfirm = (
<div>
<br/>
<p className='alert alert-danger'>
<i className='fa fa-times'/>
<FormattedMessage id='email_verify.failed'/>
</p>
</div>
);
}
return (
<div>
<div className='signup-header'>
<a href='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
/>
</a>
</div>
<div className='col-sm-12'>
<div className='signup-team__container'>
<h3>
<FormattedMessage
id='email_verify.almost'
defaultMessage='{siteName}: You are almost done'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
</h3>
<div>
<p>
<FormattedMessage
id='email_verify.notVerifiedBody'
defaultMessage='Please verify your email address. Check your inbox for an email.'
/>
</p>
<button
onClick={this.handleResend}
className='btn btn-primary'
>
<FormattedMessage
id='email_verify.resend'
defaultMessage='Resend Email'
/>
</button>
{resendConfirm}
</div>
</div>
</div>
</div>
);
}
}
ShouldVerifyEmail.defaultProps = {
};
ShouldVerifyEmail.propTypes = {
location: React.PropTypes.object.isRequired
};

Просмотреть файл

@@ -129,7 +129,9 @@ export default class Sidebar extends React.Component {
directChannels,
hiddenDirectChannelCount,
unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER
showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER,
currentTeam: TeamStore.getCurrent(),
currentUser: UserStore.getCurrentUser()
};
}
@@ -179,7 +181,7 @@ export default class Sidebar extends React.Component {
}
updateTitle() {
const channel = ChannelStore.getCurrent();
if (channel) {
if (channel && this.state.currentTeam) {
let currentSiteName = '';
if (global.window.mm_config.SiteName != null) {
currentSiteName = global.window.mm_config.SiteName;
@@ -196,7 +198,7 @@ export default class Sidebar extends React.Component {
const unread = this.getTotalUnreadCount();
const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : '';
const unreadTitle = unread.msgs > 0 ? '* ' : '';
document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().display_name + ' ' + currentSiteName;
document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.state.currentTeam.display_name + ' ' + currentSiteName;
}
}
onScroll() {
@@ -401,7 +403,6 @@ export default class Sidebar extends React.Component {
// set up click handler to switch channels (or create a new channel for non-existant ones)
var handleClick = null;
var href = '#';
var teamURL = TeamStore.getCurrentTeamUrl();
if (!channel.fake) {
handleClick = function clickHandler(e) {
@@ -413,7 +414,7 @@ export default class Sidebar extends React.Component {
e.preventDefault();
};
} else if (channel.fake && teamURL) {
} else if (channel.fake) {
// It's a direct message channel that doesn't exist yet so let's create it now
var otherUserId = Utils.getUserIdFromChannelName(channel);
@@ -434,7 +435,7 @@ export default class Sidebar extends React.Component {
},
() => {
this.setState({loadingDMChannel: -1});
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name;
window.location.href = '/' + this.state.currentTeam.name;
}
);
}
@@ -497,6 +498,11 @@ export default class Sidebar extends React.Component {
);
}
render() {
// Check if we have all info needed to render
if (this.state.currentTeam == null || this.state.currentUser == null) {
return (<div/>);
}
this.badgesActive = false;
// keep track of the first and last unread channels so we can use them to set the unread indicators
@@ -586,7 +592,10 @@ export default class Sidebar extends React.Component {
);
return (
<div>
<div
className='sidebar--left'
id='sidebar-left'
>
<NewChannelFlow
show={showChannelModal}
channelType={this.state.newChannelModalType}
@@ -598,9 +607,10 @@ export default class Sidebar extends React.Component {
/>
<SidebarHeader
teamDisplayName={TeamStore.getCurrent().display_name}
teamName={TeamStore.getCurrent().name}
teamType={TeamStore.getCurrent().type}
teamDisplayName={this.state.currentTeam.display_name}
teamName={this.state.currentTeam.name}
teamType={this.state.currentTeam.type}
currentUser={this.state.currentUser}
/>
<UnreadChannelIndicator

Просмотреть файл

@@ -4,10 +4,8 @@
import NavbarDropdown from './navbar_dropdown.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
import UserStore from '../stores/user_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
import {FormattedHTMLMessage} from 'mm-intl';
@@ -34,7 +32,7 @@ export default class SidebarHeader extends React.Component {
PreferenceStore.removeChangeListener(this.onPreferenceChange);
}
getStateFromStores() {
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, this.props.currentUser.id, 999);
return {showTutorialTip: tutorialStep === TutorialSteps.MENU_POPOVER};
}
@@ -77,7 +75,7 @@ export default class SidebarHeader extends React.Component {
);
}
render() {
var me = UserStore.getCurrentUser();
var me = this.props.currentUser;
var profilePicture = null;
if (!me) {
@@ -88,7 +86,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()}
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
/>
);
}
@@ -124,6 +122,7 @@ export default class SidebarHeader extends React.Component {
teamType={this.props.teamType}
teamDisplayName={this.props.teamDisplayName}
teamName={this.props.teamName}
currentUser={this.props.currentUser}
/>
</div>
);
@@ -131,11 +130,12 @@ export default class SidebarHeader extends React.Component {
}
SidebarHeader.defaultProps = {
teamDisplayName: global.window.mm_config.SiteName,
teamDisplayName: '',
teamType: ''
};
SidebarHeader.propTypes = {
teamDisplayName: React.PropTypes.string,
teamName: React.PropTypes.string,
teamType: React.PropTypes.string
teamType: React.PropTypes.string,
currentUser: React.PropTypes.object
};

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше