Convering client to react-router.
Этот коммит содержится в:
25
Makefile
25
Makefile
@@ -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'
|
||||
|
||||
|
||||
43
api/api.go
43
api/api.go
@@ -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 {
|
||||
|
||||
137
api/context.go
137
api/context.go
@@ -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
Обычный файл
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
343
api/oauth.go
343
api/oauth.go
@@ -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
|
||||
|
||||
114
api/team.go
114
api/team.go
@@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
119
api/team_test.go
119
api/team_test.go
@@ -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}}
|
||||
146
api/user.go
146
api/user.go
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
i18n/en.json
28
i18n/en.json
@@ -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."
|
||||
}
|
||||
]
|
||||
|
||||
20
i18n/es.json
20
i18n/es.json
@@ -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
Обычный файл
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
Обычный файл
1
templates/email_change_verify_subject.html
Обычный файл
@@ -0,0 +1 @@
|
||||
{{define "email_change_verify_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
|
||||
24
templates/error.html
Обычный файл
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
Обычный файл
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
Обычный файл
1
templates/post_subject.html
Обычный файл
@@ -0,0 +1 @@
|
||||
{{define "post_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}
|
||||
12
templates/root.html
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
|
||||
};
|
||||
|
||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user